Skip to content

Commit 01ca8de

Browse files
Merge pull request #181 from emulsify-ds/develop
Release: Refine PostCSS integration and project-specific override
2 parents 74270b0 + 5aed09a commit 01ca8de

6 files changed

Lines changed: 733 additions & 926 deletions

File tree

.github/workflows/lint.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ jobs:
99
runs-on: ubuntu-latest
1010
steps:
1111
- name: Checkout
12-
uses: actions/checkout@v2
12+
uses: actions/checkout@v4
1313
with:
1414
fetch-depth: 0
1515
- name: Install Node.js
16-
uses: actions/setup-node@v2
16+
uses: actions/setup-node@v4
1717
with:
18-
node-version: 20
18+
node-version: "24"
1919
- name: Install
2020
run: npm install
2121
- name: Lint

config/webpack/loaders.js

Lines changed: 73 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,65 @@
11
/**
2-
* @fileoverview Configures Webpack loaders.
2+
* @fileoverview Webpack loader configurations for Emulsify Core and per-project overrides.
3+
*
4+
* This module exports a single default object containing loader definitions for:
5+
* - JavaScript (with Babel)
6+
* - Sass/CSS (with PostCSS + Autoprefixer or project overrides)
7+
* - Images
8+
* - SVG sprites
9+
* - Twig templates
10+
*
11+
* It will look for these override files in your project:
12+
* - ./config/emulsify-core/webpack/babel.config.cjs
13+
* - ./config/emulsify-core/webpack/postcss.config.cjs
14+
*
15+
* If not found, it falls back to the package defaults.
316
*/
417

18+
import { createRequire } from 'module';
519
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
620
import globImporter from 'node-sass-glob-importer';
721
import fs from 'fs-extra';
22+
import path from 'path';
823

9-
let babelConfig;
10-
let postcssConfig;
24+
const require = createRequire(import.meta.url);
1125

12-
// Check if custom babel config is available.
13-
if (fs.existsSync('./config/emulsify-core/webpack/babel.config.cjs')) {
14-
babelConfig = './config/emulsify-core/webpack/babel.config.cjs';
15-
} else {
16-
babelConfig = './node_modules/@emulsify/core/config/babel.config.js';
17-
}
26+
/** @type {string} Path to the active Babel config file. */
27+
const babelConfig = fs.existsSync(
28+
'./config/emulsify-core/webpack/babel.config.cjs',
29+
)
30+
? './config/emulsify-core/webpack/babel.config.cjs'
31+
: require.resolve('@emulsify/core/config/babel.config.js');
1832

19-
// Check if custom postcss config is available.
20-
if (fs.existsSync('./config/emulsify-core/webpack/postcss.config.cjs')) {
21-
postcssConfig = './config/emulsify-core/webpack/postcss.config.cjs';
22-
} else {
23-
postcssConfig = './node_modules/@emulsify/core/config/postcss.config.js';
24-
}
33+
/** @type {string} Path to the active PostCSS config file. */
34+
const postcssConfigPath = fs.existsSync(
35+
'./config/emulsify-core/webpack/postcss.config.cjs',
36+
)
37+
? path.resolve('config/emulsify-core/webpack/postcss.config.cjs')
38+
: require.resolve('@emulsify/core/config/postcss.config.js');
2539

40+
/**
41+
* @type {import('webpack').RuleSetRule}
42+
* JavaScript loader: transpile with Babel.
43+
*/
2644
const JSLoader = {
2745
test: /^(?!.*\.(stories|component)\.js$).*\.js$/,
2846
exclude: /node_modules/,
29-
loader: 'babel-loader',
30-
options: {
31-
configFile: babelConfig,
47+
use: {
48+
loader: 'babel-loader',
49+
options: {
50+
configFile: babelConfig,
51+
},
3252
},
3353
};
3454

35-
const ImageLoader = {
36-
test: /\.(png|jpe?g|gif)$/i,
37-
type: 'asset',
38-
};
39-
55+
/**
56+
* @type {import('webpack').RuleSetRule}
57+
* CSS/Sass loader chain:
58+
* - extract to file
59+
* - css-loader (no URL rewriting)
60+
* - postcss-loader (project or default)
61+
* - sass-loader (with glob importer + compressed output)
62+
*/
4063
const CSSLoader = {
4164
test: /\.s[ac]ss$/i,
4265
exclude: /node_modules/,
@@ -54,8 +77,7 @@ const CSSLoader = {
5477
options: {
5578
sourceMap: true,
5679
postcssOptions: {
57-
config: postcssConfig,
58-
plugins: [['autoprefixer']],
80+
config: postcssConfigPath,
5981
},
6082
},
6183
},
@@ -64,16 +86,33 @@ const CSSLoader = {
6486
options: {
6587
api: 'legacy',
6688
sourceMap: true,
89+
implementation: require('sass'),
90+
webpackImporter: true,
6791
sassOptions: {
6892
importer: globImporter(),
93+
legacyImporter: true,
6994
outputStyle: 'compressed',
7095
silenceDeprecations: ['legacy-js-api'],
96+
quietDeps: true,
7197
},
7298
},
7399
},
74100
],
75101
};
76102

103+
/**
104+
* @type {import('webpack').RuleSetRule}
105+
* Image loader: inlines small assets, emits larger ones.
106+
*/
107+
const ImageLoader = {
108+
test: /\.(png|jpe?g|gif)$/i,
109+
type: 'asset',
110+
};
111+
112+
/**
113+
* @type {import('webpack').RuleSetRule}
114+
* SVG sprite loader: collects all /icons/*.svg into one sprite.
115+
*/
77116
const SVGSpriteLoader = {
78117
test: /icons\/.*\.svg$/,
79118
use: [
@@ -90,17 +129,25 @@ const SVGSpriteLoader = {
90129
],
91130
};
92131

132+
/**
133+
* @type {import('webpack').RuleSetRule}
134+
* Twig.js loader for .twig templates.
135+
*/
93136
const TwigLoader = {
94137
test: /\.twig$/,
95138
use: {
96139
loader: 'twigjs-loader',
97140
},
98141
};
99142

143+
/**
144+
* Default export of all loader configurations.
145+
* @type {{ JSLoader: import('webpack').RuleSetRule, CSSLoader: import('webpack').RuleSetRule, ImageLoader: import('webpack').RuleSetRule, SVGSpriteLoader: import('webpack').RuleSetRule, TwigLoader: import('webpack').RuleSetRule }}
146+
*/
100147
export default {
101148
JSLoader,
102149
CSSLoader,
103-
SVGSpriteLoader,
104150
ImageLoader,
151+
SVGSpriteLoader,
105152
TwigLoader,
106153
};

config/webpack/plugins.js

Lines changed: 95 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,95 +1,135 @@
1-
/**
2-
* @fileoverview Configures Webpack plugins.
3-
*/
4-
51
import { resolve, dirname } from 'path';
62
import webpack from 'webpack';
73
import { CleanWebpackPlugin } from 'clean-webpack-plugin';
8-
import _MiniCssExtractPlugin from 'mini-css-extract-plugin';
9-
import _SpriteLoaderPlugin from 'svg-sprite-loader/plugin.js';
4+
import MiniCssExtractPlugin from 'mini-css-extract-plugin';
5+
import SpriteLoaderPlugin from 'svg-sprite-loader/plugin.js';
106
import CopyPlugin from 'copy-webpack-plugin';
117
import { sync as globSync } from 'glob';
128
import fs from 'fs-extra';
139
import emulsifyConfig from '../../../../../project.emulsify.json' with { type: 'json' };
1410

15-
// Create __filename from import.meta.url without fileURLToPath
16-
let _filename = decodeURIComponent(new URL(import.meta.url).pathname);
17-
18-
// On Windows, remove the leading slash (e.g. "/C:/path" -> "C:/path")
19-
if (process.platform === 'win32' && _filename.startsWith('/')) {
20-
_filename = _filename.slice(1);
11+
/**
12+
* Resolve the directory of this file (without fileURLToPath).
13+
* @type {string}
14+
*/
15+
let __filename = decodeURIComponent(new URL(import.meta.url).pathname);
16+
if (process.platform === 'win32' && __filename.startsWith('/')) {
17+
__filename = __filename.slice(1);
2118
}
19+
const __dirname = dirname(__filename);
2220

23-
const _dirname = dirname(_filename);
24-
25-
const projectDir = resolve(_dirname, '../../../../..');
26-
const srcDir = resolve(projectDir, 'src');
27-
28-
const MiniCssExtractPlugin = new _MiniCssExtractPlugin({
29-
filename: '[name].css',
30-
chunkFilename: '[id].css',
31-
});
21+
/**
22+
* Root of the project (three levels up from this file).
23+
* @type {string}
24+
*/
25+
const projectDir = resolve(__dirname, '../../../../..');
3226

33-
const SpriteLoaderPlugin = new _SpriteLoaderPlugin({
34-
plainSprite: true,
35-
});
27+
/**
28+
* Where your source files live (if you have a `/src` folder).
29+
* Falls back to `components/` if `src/` does not exist.
30+
* @type {string}
31+
*/
32+
const srcPath = resolve(projectDir, 'src');
33+
const isSrcExists = fs.pathExistsSync(srcPath);
34+
const srcDir = isSrcExists ? srcPath : resolve(projectDir, 'components');
3635

37-
const ProgressPlugin = new webpack.ProgressPlugin();
36+
/**
37+
* Where your built assets should live.
38+
* Mirrors the `srcDir` logic: prefer `dist/` if you have `src/`, else `components/`.
39+
* @type {string}
40+
*/
41+
const distPath = isSrcExists
42+
? resolve(projectDir, 'dist')
43+
: resolve(projectDir, 'components');
3844

45+
/**
46+
* Glob pattern for all Twig & component files in your source.
47+
* We copy these through CopyPlugin so your PHP/Drupal theme sees them.
48+
* @type {string}
49+
*/
3950
const componentFilesPattern = resolve(
4051
srcDir,
4152
'**/*.{twig,component.yml,component.json}',
4253
);
4354

4455
/**
45-
* Prepare a list of patterns for copying Twig and component files.
56+
* Turn a globbed source list into copy patterns.
4657
*
47-
* @param {string} filesMatcher - Glob pattern for matching files.
48-
* @returns {Array<Object>} Array of objects with `from` and `to` properties.
58+
* @param {string} filesMatcher Glob pattern.
59+
* @returns {Array<{from:string,to:string}>}
4960
*/
5061
function getPatterns(filesMatcher) {
51-
const patterns = [];
52-
globSync(filesMatcher).forEach((file) => {
62+
return globSync(filesMatcher).map((file) => {
5363
const projectPath = file.split('/src/')[0];
5464
const srcStructure = file.split(`${srcDir}/`)[1];
5565
const parentDir = srcStructure.split('/')[0];
56-
const filePath = file.split(/(foundation\/|components\/|layout\/)/)[2];
66+
// Consolidate foundation/layout under components for Drupal.
5767
const consolidateDirs =
5868
parentDir === 'layout' || parentDir === 'foundation'
5969
? '/components/'
6070
: '/';
61-
const newfilePath =
71+
const filePath = file.split(/(foundation\/|components\/|layout\/)/)[2];
72+
const destDir =
6273
emulsifyConfig.project.platform === 'drupal'
6374
? `${projectPath}${consolidateDirs}${parentDir}/${filePath}`
6475
: `${projectPath}/dist/${parentDir}/${filePath}`;
65-
patterns.push({
66-
from: file,
67-
to: newfilePath,
68-
});
76+
return { from: file, to: destDir };
6977
});
70-
return patterns;
7178
}
7279

73-
const CopyTwigPlugin = fs.pathExistsSync(resolve(projectDir, 'src'))
80+
/**
81+
* Only include CopyPlugin if we actually have a `src/` folder.
82+
* @type {CopyPlugin|false}
83+
*/
84+
const CopyTwigPlugin = isSrcExists
7485
? new CopyPlugin({ patterns: getPatterns(componentFilesPattern) })
75-
: '';
86+
: false;
87+
88+
/**
89+
* CleanWebpackPlugin configuration.
90+
* Wipes out everything in `distPath` before a build,
91+
* except image files (we whitelist common image extensions).
92+
*/
93+
const CleanPlugin = new CleanWebpackPlugin({
94+
protectWebpackAssets: false,
95+
cleanOnceBeforeBuildPatterns: [
96+
// wipe all compiled assets
97+
`${distPath}/**/*`,
98+
// but keep any images
99+
`!${distPath}/**/*.png`,
100+
`!${distPath}/**/*.jpg`,
101+
`!${distPath}/**/*.gif`,
102+
`!${distPath}/**/*.svg`,
103+
],
104+
});
105+
106+
/**
107+
* MiniCssExtractPlugin instance: writes `[name].css` into your dist.
108+
*/
109+
const CssExtractPlugin = new MiniCssExtractPlugin({
110+
filename: '[name].css',
111+
chunkFilename: '[id].css',
112+
});
76113

77-
const pluginConfig = {
114+
/**
115+
* svg-sprite-loader plugin: bundles all /icons/*.svg.
116+
*/
117+
const SpritePlugin = new SpriteLoaderPlugin({
118+
plainSprite: true,
119+
});
120+
121+
/**
122+
* webpack.ProgressPlugin for nice build progress output.
123+
*/
124+
const ProgressPlugin = new webpack.ProgressPlugin();
125+
126+
/**
127+
* Export all plugins keyed for easy inclusion in your final Webpack config.
128+
*/
129+
export default {
78130
ProgressPlugin,
79-
MiniCssExtractPlugin,
80-
SpriteLoaderPlugin,
131+
CleanWebpackPlugin: CleanPlugin,
132+
MiniCssExtractPlugin: CssExtractPlugin,
133+
SpriteLoaderPlugin: SpritePlugin,
81134
CopyTwigPlugin,
82-
CleanWebpackPlugin: new CleanWebpackPlugin({
83-
protectWebpackAssets: false,
84-
cleanOnceBeforeBuildPatterns: ['!*.{png,jpg,gif,svg}'],
85-
cleanAfterEveryBuildPatterns: [
86-
'remove/**',
87-
'!js',
88-
'css/**/*.js',
89-
'css/**/*.js.map',
90-
'!*.{png,jpg,gif,svg}',
91-
],
92-
}),
93135
};
94-
95-
export default pluginConfig;

config/webpack/webpack.common.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,4 +217,9 @@ export default {
217217
},
218218
resolve: resolves.TwigResolve,
219219
optimization: optimizers,
220+
ignoreWarnings: [
221+
(warning) =>
222+
warning.message &&
223+
/Sass @import rules are deprecated/.test(warning.message),
224+
],
220225
};

0 commit comments

Comments
 (0)