1- import { resolve , dirname } from 'path' ;
1+ import { resolve , dirname , relative } from 'path' ;
22import webpack from 'webpack' ;
33import { CleanWebpackPlugin } from 'clean-webpack-plugin' ;
4+ import RemoveEmptyScriptsPlugin from 'webpack-remove-empty-scripts' ;
45import MiniCssExtractPlugin from 'mini-css-extract-plugin' ;
5- import SpriteLoaderPlugin from 'svg-sprite-loader/ plugin.js ' ;
6+ import SVGSpritemapPlugin from 'svg-spritemap-webpack- plugin' ;
67import CopyPlugin from 'copy-webpack-plugin' ;
78import { sync as globSync } from 'glob' ;
89import fs from 'fs-extra' ;
@@ -19,32 +20,56 @@ if (process.platform === 'win32' && _filename.startsWith('/')) {
1920const _dirname = dirname ( _filename ) ;
2021
2122/**
22- * Root of the project (three levels up from this file ).
23+ * Project root (five levels up).
2324 * @type {string }
2425 */
2526const projectDir = resolve ( _dirname , '../../../../..' ) ;
2627
2728/**
28- * Where your source files live (if you have a `/src` folder) .
29- * Falls back to `components/` if `src/` does not exist .
29+ * Where source files live.
30+ * Prefer `<project>/src`; fall back to `<project>/components` (legacy layout) .
3031 * @type {string }
3132 */
3233const srcPath = resolve ( projectDir , 'src' ) ;
3334const isSrcExists = fs . pathExistsSync ( srcPath ) ;
3435const srcDir = isSrcExists ? srcPath : resolve ( projectDir , 'components' ) ;
3536
3637/**
37- * Where your built assets should live.
38- * Mirrors the `srcDir` logic: prefer `dist/` if you have `src/`, else ` components/ `.
38+ * Where built assets live.
39+ * If `src/` exists, use `<project>/dist`; else write into `<project>/ components`.
3940 * @type {string }
4041 */
4142const distPath = isSrcExists
4243 ? resolve ( projectDir , 'dist' )
4344 : resolve ( projectDir , 'components' ) ;
4445
4546/**
46- * Glob pattern for all Twig & component files in your source.
47- * We copy these through CopyPlugin so your PHP/Drupal theme sees them.
47+ * Platform switch (affects component output roots).
48+ * @type {boolean }
49+ */
50+ const isDrupal = emulsifyConfig ?. project ?. platform === 'drupal' ;
51+
52+ /**
53+ * Component source root:
54+ * - with src/: `<project>/src/components`
55+ * - without src/: `<project>/components`
56+ * @type {string }
57+ */
58+ const componentsSrcRoot = isSrcExists ? resolve ( srcDir , 'components' ) : srcDir ;
59+
60+ /**
61+ * Component output root (where compiled component assets go):
62+ * - Drupal + src/: `components/…`
63+ * - Otherwise: `dist/components/…`
64+ * (Relative to `projectDir`; used by CopyPlugins `to:` path.)
65+ * @type {string }
66+ */
67+ const componentsOutRoot =
68+ isDrupal && isSrcExists ? 'components' : 'dist/components' ;
69+
70+ /**
71+ * Glob pattern for Twig & component meta files. These are copied as-is so
72+ * Drupal/WordPress themes can consume them alongside compiled assets.
4873 * @type {string }
4974 */
5075const componentFilesPattern = resolve (
@@ -53,84 +78,206 @@ const componentFilesPattern = resolve(
5378) ;
5479
5580/**
56- * Turn a globbed source list into copy patterns .
81+ * Build CopyPlugin patterns from a glob matcher, preserving source structure .
5782 *
58- * @param {string } filesMatcher Glob pattern .
59- * @returns {Array<{from:string,to:string}> }
83+ * @param {string } filesMatcher - Glob for files to mirror .
84+ * @returns {Array<{from:string,to:string}> } Copy patterns for CopyPlugin.
6085 */
6186function getPatterns ( filesMatcher ) {
6287 return globSync ( filesMatcher ) . map ( ( file ) => {
63- const projectPath = file . split ( '/src/' ) [ 0 ] ;
88+ const projectPath = file . split ( '/src/' ) [ 0 ] ; // base path before /src/
6489 const srcStructure = file . split ( `${ srcDir } /` ) [ 1 ] ;
6590 const parentDir = srcStructure . split ( '/' ) [ 0 ] ;
66- // Consolidate foundation/layout under components for Drupal.
91+
92+ // Consolidate foundation/layout under "components" for Drupal.
6793 const consolidateDirs =
6894 parentDir === 'layout' || parentDir === 'foundation'
6995 ? '/components/'
7096 : '/' ;
97+
7198 const filePath = file . split ( / ( f o u n d a t i o n \/ | c o m p o n e n t s \/ | l a y o u t \/ ) / ) [ 2 ] ;
72- const destDir =
73- emulsifyConfig . project . platform === 'drupal'
74- ? `${ projectPath } ${ consolidateDirs } ${ parentDir } /${ filePath } `
75- : `${ projectPath } /dist/${ parentDir } /${ filePath } ` ;
76- return { from : file , to : destDir } ;
99+
100+ const to = isDrupal
101+ ? `${ projectPath } ${ consolidateDirs } ${ parentDir } /${ filePath } `
102+ : `${ projectPath } /dist/${ parentDir } /${ filePath } ` ;
103+
104+ return { from : file , to } ;
77105 } ) ;
78106}
79107
80108/**
81- * Only include CopyPlugin if we actually have a `src/` folder.
109+ * CopyPlugin instance (only when `src/` exists):
110+ * copies Twig and component meta files 1:1 into their expected destinations.
82111 * @type {CopyPlugin|false }
83112 */
84113const CopyTwigPlugin = isSrcExists
85114 ? new CopyPlugin ( { patterns : getPatterns ( componentFilesPattern ) } )
86115 : false ;
87116
117+ /* -------------------------------------------------------------------------- */
118+ /* COMPONENT & GLOBAL ASSETS */
119+ /* -------------------------------------------------------------------------- */
120+
121+ /**
122+ * Asset allow-list (extensions we consider "static assets" to mirror).
123+ * Extend to suit your project (e.g., add `pdf`, `txt`, `xml`, etc.).
124+ * NOTE: We purposefully exclude code-like files via the filter below.
125+ * @type {RegExp }
126+ */
127+ const ASSET_EXT_RE =
128+ / \. (?: p n g | j p e ? g | g i f | s v g | w e b p | a v i f | i c o | b m p | h e i c | h e i f | m p 4 | w e b m | m p 3 | o g g | w a v | a a c | w o f f 2 ? | t t f | o t f | e o t | j s o n | w e b m a n i f e s t | m a n i f e s t | p d f ) $ / i;
129+
130+ /**
131+ * Exclude code & tooling files (don’t mirror these).
132+ * @type {RegExp }
133+ */
134+ const EXCLUDE_CODE_RE =
135+ / \. (?: j s x ? | t s x ? | m j s | c j s | v u e | s v e l t e | s c s s | s a s s | l e s s | s t y l | c s s | m a p | t w i g | p h p | y m l | y a m l | m d | m a r k d o w n | s t o r y (?: b o o k ) ? \. [ j t ] s x ? | s t o r i e s \. [ j t ] s x ? | t e s t \. [ j t ] s x ? ) $ / i;
136+
137+ /**
138+ * Shared filter for CopyPlugin patterns.
139+ * Decides whether a file should be copied as a "static asset".
140+ *
141+ * @param {string } resourcePath - Absolute file path on disk.
142+ * @param {string } base - The context directory for the pattern.
143+ * @returns {boolean } True if we should copy the file.
144+ */
145+ const assetFilter = ( resourcePath , base ) => {
146+ const rel = relative ( base , resourcePath ) ;
147+ // Guard: stay inside context
148+ if ( rel . startsWith ( '..' ) ) return false ;
149+ // Exclude typical code/tooling files
150+ if ( EXCLUDE_CODE_RE . test ( rel ) ) return false ;
151+ // Include known asset extensions
152+ return ASSET_EXT_RE . test ( rel ) ;
153+ } ;
154+
155+ /**
156+ * Copy **all static assets inside components**, regardless of folder labels.
157+ *
158+ * Examples (all preserved under the component’s output root):
159+ * src/components/accordion/assets/dropdown-icon.svg
160+ * src/components/accordion/images/icons/chevron.svg
161+ * src/components/accordion/icon.svg (root-level asset)
162+ *
163+ * @type {CopyPlugin }
164+ */
165+ const CopyComponentAssetsPlugin = new CopyPlugin ( {
166+ patterns : [
167+ {
168+ // Start at the components root and evaluate every file
169+ from : '**/*' ,
170+ context : componentsSrcRoot ,
171+ to : resolve ( projectDir , componentsOutRoot , '[path][name][ext]' ) ,
172+ noErrorOnMissing : true ,
173+ globOptions : {
174+ dot : false ,
175+ ignore : [
176+ '**/.DS_Store' ,
177+ '**/Thumbs.db' ,
178+ '**/node_modules/**' ,
179+ '**/dist/**' ,
180+ ] ,
181+ } ,
182+ // Only copy files that match our asset allow-list and are not code
183+ filter : ( resourcePath ) => assetFilter ( resourcePath , componentsSrcRoot ) ,
184+ } ,
185+ ] ,
186+ } ) ;
187+
188+ /**
189+ * OPTIONAL: Copy **global (non-component) assets** that live under `src/`
190+ * but outside `src/components/` (e.g. layout/site assets).
191+ *
192+ * Mirrors them under `dist/global/…`.
193+ * Disabled when there is no `src/` directory.
194+ *
195+ * @type {CopyPlugin|false }
196+ */
197+ const CopyGlobalAssetsPlugin = isSrcExists
198+ ? new CopyPlugin ( {
199+ patterns : [
200+ {
201+ from : '!(components|util)/**/*' ,
202+ context : srcDir ,
203+ to : resolve ( projectDir , 'dist' , 'global' , '[path][name][ext]' ) ,
204+ noErrorOnMissing : true ,
205+ globOptions : {
206+ dot : false ,
207+ ignore : [
208+ '**/.DS_Store' ,
209+ '**/Thumbs.db' ,
210+ '**/node_modules/**' ,
211+ '**/dist/**' ,
212+ ] ,
213+ } ,
214+ filter : ( resourcePath ) => assetFilter ( resourcePath , srcDir ) ,
215+ } ,
216+ ] ,
217+ } )
218+ : false ;
219+
220+ /* -------------------------------------------------------------------------- */
221+ /* OTHER PLUGINS */
222+ /* -------------------------------------------------------------------------- */
223+
88224/**
89225 * CleanWebpackPlugin configuration.
90- * Wipes out everything in `distPath` before a build,
91- * except image files (we whitelist common image extensions).
226+ * Wipes out compiled CSS/JS in `distPath` before a build; keeps images.
92227 */
93228const CleanPlugin = new CleanWebpackPlugin ( {
94229 protectWebpackAssets : false ,
95230 cleanOnceBeforeBuildPatterns : [
96- // wipe all compiled assets
97231 `${ distPath } /**/*.css` ,
98232 `${ distPath } /**/*.js` ,
99- // but keep any images
100233 `!${ distPath } /**/*.png` ,
101234 `!${ distPath } /**/*.jpg` ,
102235 `!${ distPath } /**/*.gif` ,
103236 `!${ distPath } /**/*.svg` ,
104237 ] ,
105238} ) ;
106239
240+ /** Removes empty JS files generated for style-only entries. */
241+ const RemoveEmptyJS = new RemoveEmptyScriptsPlugin ( ) ;
242+
107243/**
108- * MiniCssExtractPlugin instance: writes `[name].css` into your dist.
244+ * MiniCssExtractPlugin: emit CSS next to the entry key path (no hard-coded dist/) .
109245 */
110246const CssExtractPlugin = new MiniCssExtractPlugin ( {
111- filename : '[ name] .css' ,
112- chunkFilename : '[id]. css' ,
247+ filename : ( { chunk } ) => ` ${ chunk . name } .css` ,
248+ chunkFilename : ( { chunk } ) => ` ${ chunk . name } . css` ,
113249} ) ;
114250
115251/**
116- * svg-sprite-loader plugin: bundles all /icons/* .svg.
252+ * Generate a single SVG spritemap at `dist /icons.svg` .
117253 */
118- const SpritePlugin = new SpriteLoaderPlugin ( {
119- plainSprite : true ,
120- } ) ;
254+ const SpritePlugin = new SVGSpritemapPlugin (
255+ resolve ( projectDir , 'assets/icons/**/*.svg' ) ,
256+ {
257+ output : {
258+ filename : 'dist/icons.svg' ,
259+ chunk : { keep : true } ,
260+ } ,
261+ sprite : {
262+ prefix : '' ,
263+ generate : { title : false } ,
264+ } ,
265+ } ,
266+ ) ;
121267
122- /**
123- * webpack.ProgressPlugin for nice build progress output.
124- */
268+ /** Build progress output. */
125269const ProgressPlugin = new webpack . ProgressPlugin ( ) ;
126270
127271/**
128- * Export all plugins keyed for easy inclusion in your final Webpack config.
272+ * Export plugin instances keyed for easy inclusion in your Webpack config.
129273 */
130274export default {
131275 ProgressPlugin,
132276 CleanWebpackPlugin : CleanPlugin ,
277+ RemoveEmptyJS,
133278 MiniCssExtractPlugin : CssExtractPlugin ,
134- SpriteLoaderPlugin : SpritePlugin ,
279+ SpritePlugin,
135280 CopyTwigPlugin,
281+ CopyComponentAssetsPlugin,
282+ CopyGlobalAssetsPlugin,
136283} ;
0 commit comments