From dc092e3fa55f45c1e1b8df3cc7d000527d3a27fa Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 29 Apr 2026 13:00:44 +0530 Subject: [PATCH 01/16] Add Component based rendering with priority --- inc/Framework/ComponentLoader.php | 165 ++++++++++++ inc/helpers/custom-functions.php | 22 +- src/Components/Button/Button.php | 43 +++ src/Components/Card/Card.php | 63 +++++ .../php/inc/Framework/ComponentLoaderTest.php | 247 ++++++++++++++++++ 5 files changed, 539 insertions(+), 1 deletion(-) create mode 100644 inc/Framework/ComponentLoader.php create mode 100644 src/Components/Button/Button.php create mode 100644 src/Components/Card/Card.php create mode 100644 tests/php/inc/Framework/ComponentLoaderTest.php diff --git a/inc/Framework/ComponentLoader.php b/inc/Framework/ComponentLoader.php new file mode 100644 index 00000000..fb5faa68 --- /dev/null +++ b/inc/Framework/ComponentLoader.php @@ -0,0 +1,165 @@ + directory path. + * @param string $name Component name being resolved. + * @param array $options Options passed to render(). + */ + $paths = apply_filters( + 'elementary_theme_component_paths', + [ + 'theme' => ELEMENTARY_THEME_TEMP_DIR . '/src/Components', + ], + $name, + $options + ); + + // Order sources based on priority. + $order = self::get_source_order( $priority, $paths ); + + foreach ( $order as $source ) { + + if ( empty( $paths[ $source ] ) ) { + continue; + } + + $file = trailingslashit( $paths[ $source ] ) . $name . '/' . $name . '.php'; + + if ( file_exists( $file ) && is_readable( $file ) ) { + return $file; + } + } + + return false; + } + + /** + * Get the resolution priority. + * + * @param array $options Options array potentially containing 'priority'. + * + * @return string 'theme' or 'plugin'. + */ + private static function get_priority( array $options ): string { + + if ( ! empty( $options['priority'] ) && in_array( $options['priority'], [ 'theme', 'plugin' ], true ) ) { + return $options['priority']; + } + + /** + * Filters the default component resolution priority. + * + * @since 1.0.0 + * + * @param string $priority Default priority. Accepts 'theme' or 'plugin'. + */ + $default = apply_filters( 'elementary_theme_component_default_priority', 'theme' ); + + if ( in_array( $default, [ 'theme', 'plugin' ], true ) ) { + return $default; + } + + return 'theme'; + } + + /** + * Get the source resolution order based on priority. + * + * @param string $priority 'theme' or 'plugin'. + * @param array $paths Registered paths keyed by source. + * + * @return array Ordered list of source keys to check. + */ + private static function get_source_order( string $priority, array $paths ): array { + + $sources = array_keys( $paths ); + + if ( 'plugin' === $priority ) { + // Move 'plugin' to front if it exists. + $key = array_search( 'plugin', $sources, true ); + + if ( false !== $key ) { + unset( $sources[ $key ] ); + array_unshift( $sources, 'plugin' ); + } + } else { + // Move 'theme' to front if it exists. + $key = array_search( 'theme', $sources, true ); + + if ( false !== $key ) { + unset( $sources[ $key ] ); + array_unshift( $sources, 'theme' ); + } + } + + return array_values( $sources ); + } +} diff --git a/inc/helpers/custom-functions.php b/inc/helpers/custom-functions.php index 98e7996f..a973f7bb 100644 --- a/inc/helpers/custom-functions.php +++ b/inc/helpers/custom-functions.php @@ -7,4 +7,24 @@ declare( strict_types = 1 ); -// Define custom functions here. +use rtCamp\Theme\Elementary\Framework\ComponentLoader; + +if ( ! function_exists( 'elementary_theme_component' ) ) { + + /** + * Render a component by name. + * + * Global convenience wrapper for ComponentLoader::render(). + * + * @since 1.0.0 + * + * @param string $name Component name (e.g. 'Button', 'Card'). + * @param array $args Arguments to pass to the component. + * @param array $options Optional. Resolution options. See ComponentLoader::render(). + * + * @return void + */ + function elementary_theme_component( string $name, array $args = [], array $options = [] ): void { + ComponentLoader::render( $name, $args, $options ); + } +} diff --git a/src/Components/Button/Button.php b/src/Components/Button/Button.php new file mode 100644 index 00000000..4b0f682e --- /dev/null +++ b/src/Components/Button/Button.php @@ -0,0 +1,43 @@ +%s', + esc_url( $url ), + esc_attr( $css_class ), + esc_html( $label ) + ); +} else { + printf( + '', + esc_attr( $css_class ), + esc_html( $label ) + ); +} diff --git a/src/Components/Card/Card.php b/src/Components/Card/Card.php new file mode 100644 index 00000000..8e0cd48e --- /dev/null +++ b/src/Components/Card/Card.php @@ -0,0 +1,63 @@ + +
+ +
+ <?php echo esc_attr( $title ); ?> +
+ + +
+

+ + +

+ + + +
+ $title, + 'url' => $url, + 'class' => 'elementary-card__button', + ] + ); + ?> +
+ +
+
+assertTrue( class_exists( ComponentLoader::class ) ); + } + + /** + * Test render outputs component HTML for a known component. + */ + public function test_render_outputs_button_component(): void { + ob_start(); + ComponentLoader::render( 'Button', [ 'label' => 'Test Button' ] ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'Test Button', $output ); + $this->assertStringContainsString( 'assertStringContainsString( 'elementary-button', $output ); + } + + /** + * Test render outputs nothing for a missing component. + */ + public function test_render_missing_component_outputs_nothing(): void { + ob_start(); + ComponentLoader::render( 'NonExistentComponent', [ 'label' => 'Test' ] ); + $output = ob_get_clean(); + + $this->assertEmpty( $output ); + } + + /** + * Test Button component renders a link when url is provided. + */ + public function test_button_with_url_renders_link(): void { + ob_start(); + ComponentLoader::render( + 'Button', + [ + 'label' => 'Click Me', + 'url' => 'https://example.com', + ] + ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'assertStringContainsString( 'Click Me', $output ); + } + + /** + * Test Button component renders nothing when label is empty. + */ + public function test_button_empty_label_renders_nothing(): void { + ob_start(); + ComponentLoader::render( 'Button', [] ); + $output = ob_get_clean(); + + $this->assertEmpty( $output ); + } + + /** + * Test Card component renders with title and description. + */ + public function test_card_renders_with_content(): void { + ob_start(); + ComponentLoader::render( + 'Card', + [ + 'title' => 'Test Card', + 'description' => 'A test description.', + ] + ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'elementary-card', $output ); + $this->assertStringContainsString( 'Test Card', $output ); + $this->assertStringContainsString( 'A test description.', $output ); + } + + /** + * Test Card component renders Button when url is provided. + */ + public function test_card_with_url_renders_button(): void { + ob_start(); + ComponentLoader::render( + 'Card', + [ + 'title' => 'Linked Card', + 'url' => 'https://example.com', + ] + ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'elementary-card__button', $output ); + $this->assertStringContainsString( 'https://example.com', $output ); + } + + /** + * Test the elementary_theme_component_paths filter is applied. + */ + public function test_component_paths_filter_is_applied(): void { + $filter_called = false; + + $callback = function ( $paths ) use ( &$filter_called ) { + $filter_called = true; + return $paths; + }; + + add_filter( 'elementary_theme_component_paths', $callback ); + + ob_start(); + ComponentLoader::render( 'Button', [ 'label' => 'Test' ] ); + ob_end_clean(); + + remove_filter( 'elementary_theme_component_paths', $callback ); + + $this->assertTrue( $filter_called ); + } + + /** + * Test the elementary_theme_component_default_priority filter is applied. + */ + public function test_default_priority_filter_is_applied(): void { + $filter_called = false; + + $callback = function ( $priority ) use ( &$filter_called ) { + $filter_called = true; + return $priority; + }; + + add_filter( 'elementary_theme_component_default_priority', $callback ); + + ob_start(); + ComponentLoader::render( 'Button', [ 'label' => 'Test' ] ); + ob_end_clean(); + + remove_filter( 'elementary_theme_component_default_priority', $callback ); + + $this->assertTrue( $filter_called ); + } + + /** + * Test that priority option 'plugin' is accepted. + */ + public function test_plugin_priority_resolves_correctly(): void { + // With only theme paths registered and priority='plugin', it should + // still fall back to the theme path and render the component. + ob_start(); + ComponentLoader::render( 'Button', [ 'label' => 'Fallback' ], [ 'priority' => 'plugin' ] ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'Fallback', $output ); + } + + /** + * Test that invalid priority falls back to theme. + */ + public function test_invalid_priority_falls_back_to_theme(): void { + ob_start(); + ComponentLoader::render( 'Button', [ 'label' => 'Valid' ], [ 'priority' => 'invalid' ] ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'Valid', $output ); + } + + /** + * Test that plugin paths are checked first when priority is 'plugin'. + */ + public function test_plugin_priority_checks_plugin_path_first(): void { + + // Create a temporary plugin component directory with a custom Button. + $tmp_dir = sys_get_temp_dir() . '/elementary-test-plugin-components'; + + if ( ! is_dir( $tmp_dir . '/Button' ) ) { + mkdir( $tmp_dir . '/Button', 0755, true ); // phpcs:ignore + } + + file_put_contents( // phpcs:ignore + $tmp_dir . '/Button/Button.php', + ' 'Test' ], [ 'priority' => 'plugin' ] ); + $plugin_output = ob_get_clean(); + + // With priority='theme', the theme Button should be used. + ob_start(); + ComponentLoader::render( 'Button', [ 'label' => 'Test' ], [ 'priority' => 'theme' ] ); + $theme_output = ob_get_clean(); + + remove_filter( 'elementary_theme_component_paths', $callback ); + + // Clean up. + unlink( $tmp_dir . '/Button/Button.php' ); // phpcs:ignore + rmdir( $tmp_dir . '/Button' ); // phpcs:ignore + rmdir( $tmp_dir ); // phpcs:ignore + + $this->assertStringContainsString( 'plugin-button', $plugin_output ); + $this->assertStringContainsString( 'elementary-button', $theme_output ); + } + + /** + * Test that the global wrapper function exists. + */ + public function test_global_wrapper_function_exists(): void { + $this->assertTrue( function_exists( 'elementary_theme_component' ) ); + } + + /** + * Test that the global wrapper delegates to ComponentLoader. + */ + public function test_global_wrapper_renders_component(): void { + ob_start(); + elementary_theme_component( 'Button', [ 'label' => 'Global Test' ] ); + $output = ob_get_clean(); + + $this->assertStringContainsString( 'Global Test', $output ); + } +} From fd4b542a6e4fb03eb5f3df366b20ef05022f9906 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 29 Apr 2026 13:14:50 +0530 Subject: [PATCH 02/16] Add output buffered get static method --- inc/Framework/ComponentLoader.php | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/inc/Framework/ComponentLoader.php b/inc/Framework/ComponentLoader.php index fb5faa68..c882ced7 100644 --- a/inc/Framework/ComponentLoader.php +++ b/inc/Framework/ComponentLoader.php @@ -46,6 +46,30 @@ public static function render( string $name, array $args = [], array $options = require $file; } + /** + * Get the rendered HTML of a component as a string. + * + * Uses output buffering to capture the component output instead of + * sending it directly to the browser. + * + * @param string $name Component name (e.g. 'Button', 'Card'). + * @param array $args Arguments to pass to the component. + * @param array $options { + * Optional. Resolution options. + * + * @type string $priority Resolution priority: 'theme' or 'plugin'. Default determined by filter. + * } + * + * @return string Rendered component HTML, or empty string if not found. + */ + public static function get( string $name, array $args = [], array $options = [] ): string { + + ob_start(); + self::render( $name, $args, $options ); + + return (string) ob_get_clean(); + } + /** * Resolve the component file path. * From c60aa58a6d78fc377aeebdebadd381ebd2e4e59d Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Wed, 29 Apr 2026 13:16:27 +0530 Subject: [PATCH 03/16] Add convenience wrapper for get method --- inc/helpers/custom-functions.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/inc/helpers/custom-functions.php b/inc/helpers/custom-functions.php index a973f7bb..0952dfcc 100644 --- a/inc/helpers/custom-functions.php +++ b/inc/helpers/custom-functions.php @@ -28,3 +28,23 @@ function elementary_theme_component( string $name, array $args = [], array $opti ComponentLoader::render( $name, $args, $options ); } } + +if ( ! function_exists( 'elementary_theme_get_component' ) ) { + + /** + * Get the rendered HTML of a component as a string. + * + * Global convenience wrapper for ComponentLoader::get(). + * + * @since 1.0.0 + * + * @param string $name Component name (e.g. 'Button', 'Card'). + * @param array $args Arguments to pass to the component. + * @param array $options Optional. Resolution options. See ComponentLoader::get(). + * + * @return string Rendered component HTML. + */ + function elementary_theme_get_component( string $name, array $args = [], array $options = [] ): string { + return ComponentLoader::get( $name, $args, $options ); + } +} From 4512e3a47d973e5b5833849cc4bcb967af49a405 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 30 Apr 2026 12:18:43 +0530 Subject: [PATCH 04/16] Fix global variable prefix phpcs error --- phpcs.xml.dist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index bb96a577..7a52901f 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -100,6 +100,8 @@ tests/bootstrap.php + + src/Components/* From 223b9ab264c6e2e8f25223e6891a04491be44be6 Mon Sep 17 00:00:00 2001 From: Abhishek Singh Date: Thu, 30 Apr 2026 12:21:50 +0530 Subject: [PATCH 05/16] Fix WP global phpcs errors --- phpcs.xml.dist | 2 ++ 1 file changed, 2 insertions(+) diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 7a52901f..a8d65e27 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -114,6 +114,8 @@ tests/* + + src/Components/* - src/Components/* + src/components/* @@ -115,7 +115,7 @@ tests/* - src/Components/* + src/components/* - src/components/* + src/components/* diff --git a/src/components/button/button.php b/src/components/button/button.php index 4b0f682e..25dac49c 100644 --- a/src/components/button/button.php +++ b/src/components/button/button.php @@ -16,6 +16,8 @@ * } */ +declare( strict_types = 1 ); + $label = $args['label'] ?? ''; $url = $args['url'] ?? ''; $class = $args['class'] ?? ''; diff --git a/src/components/card/card.php b/src/components/card/card.php index 8e0cd48e..fffb5ebf 100644 --- a/src/components/card/card.php +++ b/src/components/card/card.php @@ -18,7 +18,7 @@ * } */ -use rtCamp\Theme\Elementary\Framework\ComponentLoader; +declare( strict_types = 1 ); $title = $args['title'] ?? ''; $description = $args['description'] ?? ''; @@ -47,13 +47,20 @@
$title, 'url' => $url, 'class' => 'elementary-card__button', - ] + ], + array_intersect_key( + $options, + [ + 'script' => true, + 'style' => true, + ] + ) ); ?>
diff --git a/tests/js/webpack-config.test.js b/tests/js/webpack-config.test.js new file mode 100644 index 00000000..7cec8e2c --- /dev/null +++ b/tests/js/webpack-config.test.js @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +const fs = require( 'fs' ); +const os = require( 'os' ); +const path = require( 'path' ); + +jest.mock( '@wordpress/scripts/config/webpack.config', () => [ + { + module: {}, + optimization: { + minimizer: [], + splitChunks: {}, + }, + output: {}, + plugins: [], + }, + { + output: {}, + }, +] ); + +const { getComponentEntries } = require( '../../webpack.config' ); + +describe( 'webpack component entries', () => { + let tmpDir; + + afterEach( () => { + if ( tmpDir ) { + fs.rmSync( tmpDir, { recursive: true, force: true } ); + tmpDir = undefined; + } + } ); + + it( 'only matches files with the exact component basename', () => { + tmpDir = fs.mkdtempSync( path.join( os.tmpdir(), 'elementary-webpack-components-' ) ); + const buttonDir = path.join( tmpDir, 'button' ); + + fs.mkdirSync( buttonDir ); + fs.writeFileSync( path.join( buttonDir, 'button.js' ), '' ); + fs.writeFileSync( path.join( buttonDir, 'button-extra.js' ), '' ); + fs.writeFileSync( path.join( buttonDir, 'button.test.js' ), '' ); + fs.writeFileSync( path.join( buttonDir, 'button_utils.js' ), '' ); + + expect( getComponentEntries( tmpDir, /\.js$/ ) ).toEqual( { + 'components/button': path.join( buttonDir, 'button.js' ), + } ); + } ); +} ); diff --git a/tests/php/inc/Framework/ComponentLoaderTest.php b/tests/php/inc/Framework/ComponentLoaderTest.php index 5209e501..2fc0b0dd 100644 --- a/tests/php/inc/Framework/ComponentLoaderTest.php +++ b/tests/php/inc/Framework/ComponentLoaderTest.php @@ -63,6 +63,8 @@ public function test_render_outputs_button_component(): void { * Test render outputs nothing for a missing component. */ public function test_render_missing_component_outputs_nothing(): void { + $this->setExpectedIncorrectUsage( ComponentLoader::class . '::render' ); + ob_start(); ComponentLoader::render( 'NonExistentComponent', [ 'label' => 'Test' ] ); $output = ob_get_clean(); @@ -74,6 +76,8 @@ public function test_render_missing_component_outputs_nothing(): void { * Test render rejects unsafe names containing path separators. */ public function test_render_rejects_component_name_with_slash(): void { + $this->setExpectedIncorrectUsage( ComponentLoader::class . '::render' ); + ob_start(); ComponentLoader::render( '../Button', [ 'label' => 'Test' ] ); $output = ob_get_clean(); @@ -85,6 +89,8 @@ public function test_render_rejects_component_name_with_slash(): void { * Test get() rejects unsafe names containing directory traversal tokens. */ public function test_get_rejects_component_name_with_dot_dot(): void { + $this->setExpectedIncorrectUsage( ComponentLoader::class . '::render' ); + $output = ComponentLoader::get( '..' ); $this->assertSame( '', $output ); @@ -105,6 +111,8 @@ public function test_get_returns_markup_without_direct_output(): void { * Test render rejects names containing backslashes. */ public function test_render_rejects_component_name_with_backslash(): void { + $this->setExpectedIncorrectUsage( ComponentLoader::class . '::render' ); + ob_start(); ComponentLoader::render( '..\\Button', [ 'label' => 'Test' ] ); $output = ob_get_clean(); @@ -127,6 +135,8 @@ public function test_render_trims_component_name_before_resolving(): void { * Test render rejects empty names after normalization. */ public function test_render_rejects_empty_component_name(): void { + $this->setExpectedIncorrectUsage( ComponentLoader::class . '::render' ); + ob_start(); ComponentLoader::render( ' ', [ 'label' => 'Test' ] ); $output = ob_get_clean(); @@ -226,6 +236,8 @@ public function test_component_paths_filter_is_applied(): void { * Test non-array component paths filter return is handled safely. */ public function test_component_paths_filter_non_array_return_is_handled(): void { + $this->setExpectedIncorrectUsage( ComponentLoader::class . '::render' ); + $callback = function () { return 'invalid-paths'; }; @@ -264,139 +276,6 @@ public function test_component_paths_filter_malformed_entries_are_ignored(): voi $this->assertStringContainsString( 'Sanitized Paths', $output ); } - /** - * Test the default priority filter no longer overrides theme-first resolution. - */ - public function test_default_priority_filter_is_ignored_by_theme_first_resolution(): void { - $tmp_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/elementary-test-priority-filter-' . uniqid( '', true ); - $button_dir = $tmp_dir . '/Button'; - $button_file = $button_dir . '/Button.php'; - - mkdir( $button_dir, 0755, true ); // phpcs:ignore - - file_put_contents( // phpcs:ignore - $button_file, - ' $tmp_dir, - ]; - return $paths; - }; - - $priority_callback = function () { - return 'plugin'; - }; - - add_filter( 'elementary_theme_component_paths', $paths_callback ); - add_filter( 'elementary_theme_component_default_priority', $priority_callback ); - - try { - ob_start(); - ComponentLoader::render( 'Button', [ 'label' => 'Test' ] ); - $output = ob_get_clean(); - - $this->assertStringContainsString( 'Test', $output ); - $this->assertStringContainsString( 'elementary-button', $output ); - $this->assertStringNotContainsString( 'priority-filter-plugin-button', $output ); - } finally { - remove_filter( 'elementary_theme_component_paths', $paths_callback ); - remove_filter( 'elementary_theme_component_default_priority', $priority_callback ); - - if ( is_file( $button_file ) ) { - unlink( $button_file ); // phpcs:ignore - } - - if ( is_dir( $button_dir ) ) { - rmdir( $button_dir ); // phpcs:ignore - } - - if ( is_dir( $tmp_dir ) ) { - rmdir( $tmp_dir ); // phpcs:ignore - } - } - } - - /** - * Test that priority option 'plugin' is accepted. - */ - public function test_plugin_priority_resolves_correctly(): void { - // With only theme paths registered and priority='plugin', it should - // still fall back to the theme path and render the component. - ob_start(); - ComponentLoader::render( 'Button', [ 'label' => 'Fallback' ], [ 'priority' => 'plugin' ] ); - $output = ob_get_clean(); - - $this->assertStringContainsString( 'Fallback', $output ); - } - - /** - * Test that invalid priority falls back to theme. - */ - public function test_invalid_priority_falls_back_to_theme(): void { - ob_start(); - ComponentLoader::render( 'Button', [ 'label' => 'Valid' ], [ 'priority' => 'invalid' ] ); - $output = ob_get_clean(); - - $this->assertStringContainsString( 'Valid', $output ); - } - - /** - * Test that plugin priority does not override theme components. - */ - public function test_plugin_priority_does_not_override_theme_component(): void { - $tmp_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/elementary-test-plugin-components-' . uniqid( '', true ); - $button_dir = $tmp_dir . '/Button'; - $button_file = $button_dir . '/Button.php'; - $plugin_output = ''; - $theme_output = ''; - - mkdir( $button_dir, 0755, true ); // phpcs:ignore - - file_put_contents( // phpcs:ignore - $button_file, - ' $tmp_dir, - ]; - return $paths; - }; - - add_filter( 'elementary_theme_component_paths', $callback ); - - try { - ob_start(); - ComponentLoader::render( 'Button', [ 'label' => 'Test' ], [ 'priority' => 'plugin' ] ); - $plugin_output = ob_get_clean(); - - ob_start(); - ComponentLoader::render( 'Button', [ 'label' => 'Test' ], [ 'priority' => 'theme' ] ); - $theme_output = ob_get_clean(); - } finally { - remove_filter( 'elementary_theme_component_paths', $callback ); - - if ( is_file( $button_file ) ) { - unlink( $button_file ); // phpcs:ignore - } - - if ( is_dir( $button_dir ) ) { - rmdir( $button_dir ); // phpcs:ignore - } - - if ( is_dir( $tmp_dir ) ) { - rmdir( $tmp_dir ); // phpcs:ignore - } - } - - $this->assertStringContainsString( 'elementary-button', $plugin_output ); - $this->assertStringNotContainsString( 'plugin-button', $plugin_output ); - $this->assertStringContainsString( 'elementary-button', $theme_output ); - } /** * Test child theme templates resolve before plugin components. @@ -437,7 +316,7 @@ public function test_child_theme_component_resolves_before_plugin_component(): v add_filter( 'template_directory', $template_callback ); try { - $output = ComponentLoader::get( 'ChildFirst', [], [ 'priority' => 'plugin' ] ); + $output = ComponentLoader::get( 'ChildFirst' ); } finally { remove_filter( 'elementary_theme_component_paths', $paths_callback ); remove_filter( 'stylesheet_directory', $stylesheet_callback ); @@ -499,7 +378,7 @@ public function test_parent_theme_component_resolves_before_plugin_component(): add_filter( 'template_directory', $template_callback ); try { - $output = ComponentLoader::get( 'ParentFirst', [], [ 'priority' => 'plugin' ] ); + $output = ComponentLoader::get( 'ParentFirst' ); } finally { remove_filter( 'elementary_theme_component_paths', $paths_callback ); remove_filter( 'stylesheet_directory', $stylesheet_callback ); @@ -578,222 +457,6 @@ public function test_plugin_component_resolves_when_theme_template_is_absent(): $this->assertStringContainsString( 'plugin-fallback-component', $output ); } - /** - * Test fixture PromoBanner resolves from theme override before plugin-key source. - */ - public function test_fixture_promo_banner_resolves_from_theme_override(): void { - $component_meta = null; - $paths_callback = function ( $paths ) { - $paths['plugin'] = [ - 'php' => ELEMENTARY_THEME_TEMP_DIR . '/plugins-theme/Components', - 'style' => [ - 'dir' => ELEMENTARY_THEME_BUILD_DIR . '/css/plugin-components', - 'url' => ELEMENTARY_THEME_BUILD_URI . '/css/plugin-components', - ], - 'script' => [ - 'dir' => ELEMENTARY_THEME_BUILD_DIR . '/js/plugin-components', - 'url' => ELEMENTARY_THEME_BUILD_URI . '/js/plugin-components', - ], - ]; - return $paths; - }; - $action_callback = function ( $name, $args, $options ) use ( &$component_meta ) { - $component_meta = $options['component'] ?? null; - }; - - add_filter( 'elementary_theme_component_paths', $paths_callback ); - add_action( 'elementary_theme_before_get_component', $action_callback, 10, 3 ); - - try { - $output = ComponentLoader::get( - 'PromoBanner', - [ - 'title' => 'Theme Promo', - 'description' => 'Theme override wins.', - ] - ); - } finally { - remove_filter( 'elementary_theme_component_paths', $paths_callback ); - remove_action( 'elementary_theme_before_get_component', $action_callback ); - } - - $this->assertStringContainsString( 'elementary-promo-banner--theme', $output ); - $this->assertStringContainsString( 'data-component-source="theme"', $output ); - $this->assertIsArray( $component_meta ); - $this->assertSame( 'theme', $component_meta['source'] ); - } - - /** - * Test fixture PromoBanner falls back to plugin-key source when theme path is absent. - */ - public function test_fixture_promo_banner_falls_back_to_plugin_key_source(): void { - $component_meta = null; - $paths_callback = function ( $paths ) { - $paths['theme']['php'] = 'src/MissingComponents'; - $paths['plugin'] = [ - 'php' => ELEMENTARY_THEME_TEMP_DIR . '/plugins-theme/Components', - 'style' => [ - 'dir' => ELEMENTARY_THEME_BUILD_DIR . '/css/plugin-components', - 'url' => ELEMENTARY_THEME_BUILD_URI . '/css/plugin-components', - ], - 'script' => [ - 'dir' => ELEMENTARY_THEME_BUILD_DIR . '/js/plugin-components', - 'url' => ELEMENTARY_THEME_BUILD_URI . '/js/plugin-components', - ], - ]; - return $paths; - }; - $action_callback = function ( $name, $args, $options ) use ( &$component_meta ) { - $component_meta = $options['component'] ?? null; - }; - - add_filter( 'elementary_theme_component_paths', $paths_callback ); - add_action( 'elementary_theme_before_get_component', $action_callback, 10, 3 ); - - try { - $output = ComponentLoader::get( - 'PromoBanner', - [ - 'title' => 'Plugin Promo', - 'description' => 'Plugin fixture wins when theme is absent.', - ] - ); - } finally { - remove_filter( 'elementary_theme_component_paths', $paths_callback ); - remove_action( 'elementary_theme_before_get_component', $action_callback ); - } - - $this->assertStringContainsString( 'plugin-promo-banner', $output ); - $this->assertStringContainsString( 'data-component-source="plugin"', $output ); - $this->assertIsArray( $component_meta ); - $this->assertSame( 'plugin', $component_meta['source'] ); - } - - /** - * Test fixture FeaturePanel resolves from plugin-key source and shared asset cascade. - */ - public function test_fixture_feature_panel_resolves_from_plugin_key_source_with_shared_asset_cascade(): void { - $component_assets = null; - $paths_callback = function ( $paths ) { - $paths['plugin'] = [ - 'php' => ELEMENTARY_THEME_TEMP_DIR . '/plugins-theme/Components', - 'style' => [ - 'dir' => ELEMENTARY_THEME_BUILD_DIR . '/css/plugin-components', - 'url' => ELEMENTARY_THEME_BUILD_URI . '/css/plugin-components', - ], - 'script' => [ - 'dir' => ELEMENTARY_THEME_BUILD_DIR . '/js/plugin-components', - 'url' => ELEMENTARY_THEME_BUILD_URI . '/js/plugin-components', - ], - ]; - return $paths; - }; - $action_callback = function ( $name, $args, $options ) use ( &$component_assets ) { - $component_assets = $options['component']['assets'] ?? null; - }; - - add_filter( 'elementary_theme_component_paths', $paths_callback ); - add_action( 'elementary_theme_before_get_component', $action_callback, 10, 3 ); - - try { - $output = ComponentLoader::get( - 'FeaturePanel', - [ - 'title' => 'Plugin Feature', - 'description' => 'Plugin-key fixture.', - ] - ); - } finally { - remove_filter( 'elementary_theme_component_paths', $paths_callback ); - remove_action( 'elementary_theme_before_get_component', $action_callback ); - } - - $this->assertStringContainsString( 'plugin-feature-panel', $output ); - $this->assertIsArray( $component_assets ); - $this->assertSame( ELEMENTARY_THEME_BUILD_DIR . '/css/plugin-components/featurepanel.css', $component_assets['style']['file'] ); - $this->assertSame( ELEMENTARY_THEME_BUILD_DIR . '/js/plugin-components/featurepanel.js', $component_assets['script']['file'] ); - } - - /** - * Test child theme built assets override parent theme assets. - */ - public function test_child_theme_asset_overrides_are_independent(): void { - $tmp_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/elementary-test-child-theme-assets-' . uniqid( '', true ); - $child_root = $tmp_dir . '/child'; - $child_css_dir = $child_root . '/assets/build/css/components'; - $child_js_dir = $child_root . '/assets/build/js/components'; - $component_assets = null; - - foreach ( [ $child_css_dir, $child_js_dir ] as $dir ) { - mkdir( $dir, 0755, true ); // phpcs:ignore - } - - file_put_contents( $child_css_dir . '/featurepanel.css', '.child-feature-panel { color: red; }' ); // phpcs:ignore - file_put_contents( $child_js_dir . '/featurepanel.js', 'window.childFeaturePanel = true;' ); // phpcs:ignore - - $stylesheet_callback = function () use ( $child_root ) { - return $child_root; - }; - $stylesheet_uri_callback = function () { - return 'https://child.example'; - }; - $before_component_callback = function ( $name, $args, $options ) use ( &$component_assets ) { - $component_assets = $options['component']['assets'] ?? null; - }; - - add_filter( 'stylesheet_directory', $stylesheet_callback ); - add_filter( 'stylesheet_directory_uri', $stylesheet_uri_callback ); - add_action( 'elementary_theme_before_get_component', $before_component_callback, 10, 3 ); - - try { - $output = ComponentLoader::get( - 'FeaturePanel', - [ - 'title' => 'Child Asset Feature', - 'description' => 'Child assets should win.', - ] - ); - } finally { - remove_filter( 'stylesheet_directory', $stylesheet_callback ); - remove_filter( 'stylesheet_directory_uri', $stylesheet_uri_callback ); - remove_action( 'elementary_theme_before_get_component', $before_component_callback ); - - foreach ( - [ - $child_css_dir . '/featurepanel.css', - $child_js_dir . '/featurepanel.js', - ] as $file - ) { - if ( is_file( $file ) ) { - unlink( $file ); // phpcs:ignore - } - } - - foreach ( - [ - $child_css_dir, - $child_root . '/assets/build/css', - $child_js_dir, - $child_root . '/assets/build/js', - $child_root . '/assets/build', - $child_root . '/assets', - $child_root, - $tmp_dir, - ] as $dir - ) { - if ( is_dir( $dir ) ) { - rmdir( $dir ); // phpcs:ignore - } - } - } - - $this->assertStringContainsString( 'Child Asset Feature', $output ); - $this->assertIsArray( $component_assets ); - $this->assertSame( $child_css_dir . '/featurepanel.css', $component_assets['style']['file'] ); - $this->assertSame( 'https://child.example/assets/build/css/components/featurepanel.css', $component_assets['style']['url'] ); - $this->assertSame( $child_js_dir . '/featurepanel.js', $component_assets['script']['file'] ); - $this->assertSame( 'https://child.example/assets/build/js/components/featurepanel.js', $component_assets['script']['url'] ); - } /** * Test child theme asset lookup derives from the configured theme asset directory. @@ -893,92 +556,6 @@ public function test_child_theme_asset_lookup_uses_theme_asset_config(): void { $this->assertArrayNotHasKey( 'script', $component_assets ); } - /** - * Test plugin assets backfill missing theme assets for theme-resolved components. - */ - public function test_plugin_asset_backfills_missing_theme_asset(): void { - $tmp_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/elementary-test-plugin-asset-backfill-' . uniqid( '', true ); - $parent_root = $tmp_dir . '/parent'; - $child_root = $tmp_dir . '/child'; - $plugin_script_dir = $tmp_dir . '/plugin-js'; - $component_assets = null; - - mkdir( $parent_root, 0755, true ); // phpcs:ignore - mkdir( $child_root, 0755, true ); // phpcs:ignore - mkdir( $plugin_script_dir, 0755, true ); // phpcs:ignore - - file_put_contents( $plugin_script_dir . '/featurepanel.js', 'window.pluginFeaturePanelBackfill = true;' ); // phpcs:ignore - - $template_callback = function () use ( $parent_root ) { - return $parent_root; - }; - $template_uri_callback = function () { - return 'https://parent.example'; - }; - $stylesheet_callback = function () use ( $child_root ) { - return $child_root; - }; - $stylesheet_uri_callback = function () { - return 'https://child.example'; - }; - $paths_callback = function ( $paths ) use ( $plugin_script_dir ) { - $paths['theme']['script'] = 'theme-js'; - $paths['plugin'] = [ - 'php' => ELEMENTARY_THEME_TEMP_DIR . '/plugins-theme/Components', - 'script' => [ - 'dir' => $plugin_script_dir, - 'url' => 'https://plugin.example/js/plugin-components', - ], - ]; - return $paths; - }; - $action_callback = function ( $name, $args, $options ) use ( &$component_assets ) { - $component_assets = $options['component']['assets'] ?? null; - }; - - add_filter( 'template_directory', $template_callback ); - add_filter( 'template_directory_uri', $template_uri_callback ); - add_filter( 'stylesheet_directory', $stylesheet_callback ); - add_filter( 'stylesheet_directory_uri', $stylesheet_uri_callback ); - add_filter( 'elementary_theme_component_paths', $paths_callback ); - add_action( 'elementary_theme_before_get_component', $action_callback, 10, 3 ); - - try { - $output = ComponentLoader::get( - 'FeaturePanel', - [ 'title' => 'Plugin Asset Backfill' ], - [ - 'script' => true, - 'style' => false, - ] - ); - } finally { - remove_filter( 'template_directory', $template_callback ); - remove_filter( 'template_directory_uri', $template_uri_callback ); - remove_filter( 'stylesheet_directory', $stylesheet_callback ); - remove_filter( 'stylesheet_directory_uri', $stylesheet_uri_callback ); - remove_filter( 'elementary_theme_component_paths', $paths_callback ); - remove_action( 'elementary_theme_before_get_component', $action_callback ); - - if ( is_file( $plugin_script_dir . '/featurepanel.js' ) ) { - unlink( $plugin_script_dir . '/featurepanel.js' ); // phpcs:ignore - } - - foreach ( [ $parent_root, $child_root, $plugin_script_dir, $tmp_dir ] as $dir ) { - if ( is_dir( $dir ) ) { - rmdir( $dir ); // phpcs:ignore - } - } - } - - $this->assertStringContainsString( 'Plugin Asset Backfill', $output ); - $this->assertIsArray( $component_assets ); - $this->assertArrayHasKey( 'script', $component_assets ); - $this->assertArrayNotHasKey( 'style', $component_assets ); - $this->assertSame( $plugin_script_dir . '/featurepanel.js', $component_assets['script']['file'] ); - $this->assertSame( 'https://plugin.example/js/plugin-components/featurepanel.js', $component_assets['script']['url'] ); - } - /** * Test PHP-only path configs render without asset config. */ @@ -1005,7 +582,7 @@ public function test_php_only_component_path_config_renders_without_assets(): vo try { ob_start(); - ComponentLoader::render( 'PhpOnly', [ 'label' => 'Test' ], [ 'priority' => 'plugin' ] ); + ComponentLoader::render( 'PhpOnly', [ 'label' => 'Test' ] ); $output = ob_get_clean(); } finally { remove_filter( 'elementary_theme_component_paths', $callback ); @@ -1071,7 +648,7 @@ public function test_malformed_component_asset_metadata_falls_back_safely(): voi try { ob_start(); - ComponentLoader::render( 'MalformedAsset', [ 'label' => 'Test' ], [ 'priority' => 'plugin' ] ); + ComponentLoader::render( 'MalformedAsset', [ 'label' => 'Test' ] ); $output = ob_get_clean(); } finally { remove_filter( 'elementary_theme_component_paths', $callback ); @@ -1092,6 +669,89 @@ public function test_malformed_component_asset_metadata_falls_back_safely(): voi $this->assertStringContainsString( 'malformed-asset-meta-button', $output ); } + /** + * Test component asset metadata files are required only once per request. + */ + public function test_component_asset_metadata_is_cached_between_repeated_renders(): void { + $tmp_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/elementary-test-cached-asset-meta-' . uniqid( '', true ); + $component_root = $tmp_dir . '/components'; + $style_root = $tmp_dir . '/css'; + $button_dir = $component_root . '/CachedMeta'; + $button_file = $button_dir . '/CachedMeta.php'; + $button_css_file = $style_root . '/cachedmeta.css'; + $button_asset = $style_root . '/cachedmeta.asset.php'; + + mkdir( $button_dir, 0755, true ); // phpcs:ignore + mkdir( $style_root, 0755, true ); // phpcs:ignore + + file_put_contents( $button_file, ' [], "version" => "test-version" ];' + ); + + $callback = function ( $paths ) use ( $component_root, $style_root ) { + $paths['plugin'] = [ + 'php' => $component_root, + 'style' => [ + 'dir' => $style_root, + 'url' => 'https://example.com/css', + ], + ]; + return $paths; + }; + + add_filter( 'elementary_theme_component_paths', $callback ); + self::reset_component_asset_handles( 'cachedmeta' ); + $require_count = 0; + + $GLOBALS['elementary_test_asset_meta_require_count'] = 0; + + try { + $first_output = ComponentLoader::get( 'CachedMeta', [], [ 'style' => true ] ); + $second_output = ComponentLoader::get( 'CachedMeta', [], [ 'style' => true ] ); + $require_count = $GLOBALS['elementary_test_asset_meta_require_count']; + } finally { + remove_filter( 'elementary_theme_component_paths', $callback ); + self::reset_component_asset_handles( 'cachedmeta' ); + unset( $GLOBALS['elementary_test_asset_meta_require_count'] ); + + foreach ( [ $button_asset, $button_css_file, $button_file ] as $file ) { + if ( is_file( $file ) ) { + unlink( $file ); // phpcs:ignore + } + } + + foreach ( [ $button_dir, $style_root, $component_root, $tmp_dir ] as $dir ) { + if ( is_dir( $dir ) ) { + rmdir( $dir ); // phpcs:ignore + } + } + } + + $this->assertStringContainsString( 'cached-asset-meta-button', $first_output ); + $this->assertStringContainsString( 'cached-asset-meta-button', $second_output ); + $this->assertSame( 1, $require_count ); + } + + /** + * Test Windows-style asset paths are not prefixed with an extra slash. + */ + public function test_component_asset_metadata_path_preserves_windows_drive_prefix(): void { + $method = new ReflectionMethod( ComponentLoader::class, 'get_component_asset_meta' ); + $method->setAccessible( true ); + $method->invoke( null, 'C:\\theme\\assets\\button.js' ); + + $reflection = new ReflectionClass( ComponentLoader::class ); + $property = $reflection->getProperty( 'asset_meta_cache' ); + $property->setAccessible( true ); + $cache = $property->getValue(); + + $this->assertArrayHasKey( 'C:\\theme\\assets\\button.asset.php', $cache ); + $this->assertArrayNotHasKey( '/C:/theme/assets/button.asset.php', $cache ); + } + /** * Test enqueue defaults prevent disabled assets from being collected. */ @@ -1243,7 +903,7 @@ public function test_enqueue_defaults_disable_asset_enqueueing(): void { self::reset_component_asset_handles( 'disabledasset' ); try { - $output = ComponentLoader::get( 'DisabledAsset', [ 'label' => 'No Enqueue' ], [ 'priority' => 'plugin' ] ); + $output = ComponentLoader::get( 'DisabledAsset', [ 'label' => 'No Enqueue' ] ); } finally { remove_filter( 'elementary_theme_component_paths', $paths_callback ); remove_filter( 'elementary_theme_component_enqueue_defaults', $enqueue_callback ); @@ -1321,9 +981,8 @@ public function test_enqueue_options_override_defaults_before_enqueueing_assets( 'OverrideAsset', [ 'label' => 'Script Override' ], [ - 'priority' => 'plugin', - 'script' => true, - 'style' => false, + 'script' => true, + 'style' => false, ] ); @@ -1350,16 +1009,134 @@ public function test_enqueue_options_override_defaults_before_enqueueing_assets( } } + /** + * Test registered-but-dequeued component asset handles are enqueued again. + */ + public function test_registered_dequeued_component_assets_are_enqueued_on_later_render(): void { + $tmp_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/elementary-test-reenqueue-asset-' . uniqid( '', true ); + $component_root = $tmp_dir . '/components'; + $style_root = $tmp_dir . '/css'; + $script_root = $tmp_dir . '/js'; + $button_dir = $component_root . '/ReenqueueAsset'; + $button_file = $button_dir . '/ReenqueueAsset.php'; + $button_css = $style_root . '/reenqueueasset.css'; + $button_js = $script_root . '/reenqueueasset.js'; + $style_handle = 'elementary-theme-component-reenqueueasset-style'; + $script_handle = 'elementary-theme-component-reenqueueasset-script'; + + mkdir( $button_dir, 0755, true ); // phpcs:ignore + mkdir( $style_root, 0755, true ); // phpcs:ignore + mkdir( $script_root, 0755, true ); // phpcs:ignore + + file_put_contents( $button_file, ' $component_root, + 'style' => [ + 'dir' => $style_root, + 'url' => 'https://example.com/css', + ], + 'script' => [ + 'dir' => $script_root, + 'url' => 'https://example.com/js', + ], + ]; + return $paths; + }; + + add_filter( 'elementary_theme_component_paths', $paths_callback ); + self::reset_component_asset_handles( 'reenqueueasset' ); + + try { + ComponentLoader::get( + 'ReenqueueAsset', + [], + [ + 'style' => true, + 'script' => true, + ] + ); + + wp_dequeue_style( $style_handle ); + wp_dequeue_script( $script_handle ); + + $this->assertTrue( wp_style_is( $style_handle, 'registered' ) ); + $this->assertTrue( wp_script_is( $script_handle, 'registered' ) ); + $this->assertFalse( wp_style_is( $style_handle, 'enqueued' ) ); + $this->assertFalse( wp_script_is( $script_handle, 'enqueued' ) ); + + $output = ComponentLoader::get( + 'ReenqueueAsset', + [], + [ + 'style' => true, + 'script' => true, + ] + ); + + $this->assertStringContainsString( 'reenqueue-asset-button', $output ); + $this->assertTrue( wp_style_is( $style_handle, 'enqueued' ) ); + $this->assertTrue( wp_script_is( $script_handle, 'enqueued' ) ); + } finally { + remove_filter( 'elementary_theme_component_paths', $paths_callback ); + self::reset_component_asset_handles( 'reenqueueasset' ); + + foreach ( [ $button_js, $button_css, $button_file ] as $file ) { + if ( is_file( $file ) ) { + unlink( $file ); // phpcs:ignore + } + } + + foreach ( [ $button_dir, $style_root, $script_root, $component_root, $tmp_dir ] as $dir ) { + if ( is_dir( $dir ) ) { + rmdir( $dir ); // phpcs:ignore + } + } + } + } + /** * Test nested components inherit disabled enqueue options. */ public function test_nested_components_inherit_disabled_enqueue_options(): void { + $tmp_dir = rtrim( sys_get_temp_dir(), '/\\' ) . '/elementary-test-nested-disabled-assets-' . uniqid( '', true ); + $component_root = $tmp_dir . '/components'; + $style_root = $tmp_dir . '/css'; + $script_root = $tmp_dir . '/js'; + $button_css = $style_root . '/button.css'; + $button_js = $script_root . '/button.js'; $component_assets = []; + mkdir( $component_root, 0755, true ); // phpcs:ignore + mkdir( $style_root, 0755, true ); // phpcs:ignore + mkdir( $script_root, 0755, true ); // phpcs:ignore + + file_put_contents( $button_css, '.elementary-button { color: inherit; }' ); // phpcs:ignore + file_put_contents( $button_js, 'window.elementaryNestedButton = true;' ); // phpcs:ignore + + $paths_callback = function ( $paths ) use ( $component_root, $style_root, $script_root ) { + $paths['plugin'] = [ + 'php' => $component_root, + 'style' => [ + 'dir' => $style_root, + 'url' => 'https://example.com/css', + ], + 'script' => [ + 'dir' => $script_root, + 'url' => 'https://example.com/js', + ], + ]; + return $paths; + }; + $action_callback = function ( $name, $args, $options ) use ( &$component_assets ) { $component_assets[ $name ] = $options['component']['assets'] ?? null; }; + add_filter( 'elementary_theme_component_paths', $paths_callback ); add_action( 'elementary_theme_before_get_component', $action_callback, 10, 3 ); try { @@ -1375,7 +1152,20 @@ public function test_nested_components_inherit_disabled_enqueue_options(): void ] ); } finally { + remove_filter( 'elementary_theme_component_paths', $paths_callback ); remove_action( 'elementary_theme_before_get_component', $action_callback ); + + foreach ( [ $button_js, $button_css ] as $file ) { + if ( is_file( $file ) ) { + unlink( $file ); // phpcs:ignore + } + } + + foreach ( [ $style_root, $script_root, $component_root, $tmp_dir ] as $dir ) { + if ( is_dir( $dir ) ) { + rmdir( $dir ); // phpcs:ignore + } + } } $this->assertStringContainsString( 'Nested Disabled Assets', $output ); @@ -1432,9 +1222,9 @@ public function test_component_lookup_cache_is_sensitive_to_filtered_paths(): vo add_filter( 'elementary_theme_component_paths', $callback ); try { - $first_output = ComponentLoader::get( 'CacheProbe', [ 'label' => 'Test' ], [ 'priority' => 'plugin' ] ); + $first_output = ComponentLoader::get( 'CacheProbe', [ 'label' => 'Test' ] ); $active_tmp_dir = $second_tmp_dir; - $second_output = ComponentLoader::get( 'CacheProbe', [ 'label' => 'Test' ], [ 'priority' => 'plugin' ] ); + $second_output = ComponentLoader::get( 'CacheProbe', [ 'label' => 'Test' ] ); } finally { remove_filter( 'elementary_theme_component_paths', $callback ); diff --git a/webpack.config.js b/webpack.config.js index 9807e8cc..b075b718 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -117,7 +117,7 @@ const getComponentEntries = ( dir, extFilter ) => { const compName = entry.name; const compDir = path.join( resolvedDir, compName ); fs.readdirSync( compDir ).forEach( ( file ) => { - if ( file.startsWith( compName ) && file.match( extFilter ) ) { + if ( file.match( extFilter ) && path.parse( file ).name === compName ) { const entryName = `components/${ compName.toLowerCase() }`; entries[ entryName ] = path.join( compDir, file ); } @@ -236,4 +236,10 @@ const moduleScripts = { }, }; -module.exports = [ scripts, styles, moduleScripts ]; +const configs = [ scripts, styles, moduleScripts ]; + +Object.defineProperty( configs, 'getComponentEntries', { + value: getComponentEntries, +} ); + +module.exports = configs;