Skip to content

Commit 80cd7c8

Browse files
Merge pull request #205 from emulsify-ds/develop
Release:
2 parents e0720a0 + 8583d3d commit 80cd7c8

8 files changed

Lines changed: 1053 additions & 2553 deletions

File tree

config/webpack/loaders.js

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,25 @@ const postcssConfigPath = fs.existsSync(
3737
? path.resolve('config/emulsify-core/webpack/postcss.config.cjs')
3838
: require.resolve('@emulsify/core/config/postcss.config.js');
3939

40+
/**
41+
* Resolve the directory of this file (without fileURLToPath).
42+
* @type {string}
43+
*/
44+
let _filename = decodeURIComponent(new URL(import.meta.url).pathname);
45+
if (process.platform === 'win32' && _filename.startsWith('/')) {
46+
_filename = _filename.slice(1);
47+
}
48+
const _dirname = path.dirname(_filename);
49+
50+
/**
51+
* Root of the project (three levels up from this file).
52+
* @type {string}
53+
*/
54+
const projectDir = path.resolve(_dirname, '../../../../..');
55+
56+
/** Absolute path to the folder that contains sprite source icons. */
57+
const ICONS_DIR = path.resolve(projectDir, 'assets/icons');
58+
4059
/**
4160
* @type {import('webpack').RuleSetRule}
4261
* JavaScript loader: transpile with Babel.
@@ -111,22 +130,17 @@ const ImageLoader = {
111130

112131
/**
113132
* @type {import('webpack').RuleSetRule}
114-
* SVG sprite loader: collects all /icons/*.svg into one sprite.
133+
* General SVG loader for non-sprite SVGs (logos, illustrations, etc.).
134+
* IMPORTANT: Excludes `assets/icons/` so `svg-spritemap-webpack-plugin`
135+
* can consume those files without being intercepted by this rule.
115136
*/
116-
const SVGSpriteLoader = {
137+
const SVGLoader = {
117138
test: /icons\/.*\.svg$/,
118-
use: [
119-
{
120-
loader: 'svg-sprite-loader',
121-
options: {
122-
extract: true,
123-
esModule: true,
124-
runtimeCompat: true,
125-
outputPath: 'dist/',
126-
spriteFilename: './icons.svg',
127-
},
128-
},
129-
],
139+
type: 'asset/resource',
140+
generator: {
141+
filename: 'icons.svg',
142+
},
143+
exclude: [ICONS_DIR],
130144
};
131145

132146
/**
@@ -148,6 +162,6 @@ export default {
148162
JSLoader,
149163
CSSLoader,
150164
ImageLoader,
151-
SVGSpriteLoader,
165+
SVGLoader,
152166
TwigLoader,
153167
};

config/webpack/plugins.js

Lines changed: 183 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { resolve, dirname } from 'path';
1+
import { resolve, dirname, relative } from 'path';
22
import webpack from 'webpack';
33
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
4+
import RemoveEmptyScriptsPlugin from 'webpack-remove-empty-scripts';
45
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
5-
import SpriteLoaderPlugin from 'svg-sprite-loader/plugin.js';
6+
import SVGSpritemapPlugin from 'svg-spritemap-webpack-plugin';
67
import CopyPlugin from 'copy-webpack-plugin';
78
import { sync as globSync } from 'glob';
89
import fs from 'fs-extra';
@@ -19,32 +20,56 @@ if (process.platform === 'win32' && _filename.startsWith('/')) {
1920
const _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
*/
2526
const 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
*/
3233
const srcPath = resolve(projectDir, 'src');
3334
const isSrcExists = fs.pathExistsSync(srcPath);
3435
const 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
*/
4142
const 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
*/
5075
const 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
*/
6186
function 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(/(foundation\/|components\/|layout\/)/)[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
*/
84113
const 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+
/\.(?:png|jpe?g|gif|svg|webp|avif|ico|bmp|heic|heif|mp4|webm|mp3|ogg|wav|aac|woff2?|ttf|otf|eot|json|webmanifest|manifest|pdf)$/i;
129+
130+
/**
131+
* Exclude code & tooling files (don’t mirror these).
132+
* @type {RegExp}
133+
*/
134+
const EXCLUDE_CODE_RE =
135+
/\.(?:jsx?|tsx?|mjs|cjs|vue|svelte|scss|sass|less|styl|css|map|twig|php|yml|yaml|md|markdown|story(?:book)?\.[jt]sx?|stories\.[jt]sx?|test\.[jt]sx?)$/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
*/
93228
const 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
*/
110246
const 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. */
125269
const 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
*/
130274
export 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

Comments
 (0)