@emulsify/core 3.0.4 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -17,5 +17,5 @@ jobs:
17
17
  with:
18
18
  # You can target a repository in a different organization
19
19
  # to the issue
20
- project-url: https://github.com/orgs/emulsify-ds/projects/6
20
+ project-url: https://github.com/emulsify-ds/emulsify-core
21
21
  github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
@@ -9,13 +9,13 @@ jobs:
9
9
  runs-on: ubuntu-latest
10
10
  steps:
11
11
  - name: Checkout
12
- uses: actions/checkout@v2
12
+ uses: actions/checkout@v4
13
13
  with:
14
14
  fetch-depth: 0
15
15
  - name: Install Node.js
16
- uses: actions/setup-node@v2
16
+ uses: actions/setup-node@v4
17
17
  with:
18
- node-version: 20
18
+ node-version: "24"
19
19
  - name: Install
20
20
  run: npm install
21
21
  - name: Lint
@@ -1,42 +1,65 @@
1
1
  /**
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.
3
16
  */
4
17
 
18
+ import { createRequire } from 'module';
5
19
  import MiniCssExtractPlugin from 'mini-css-extract-plugin';
6
20
  import globImporter from 'node-sass-glob-importer';
7
21
  import fs from 'fs-extra';
22
+ import path from 'path';
8
23
 
9
- let babelConfig;
10
- let postcssConfig;
24
+ const require = createRequire(import.meta.url);
11
25
 
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');
18
32
 
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');
25
39
 
40
+ /**
41
+ * @type {import('webpack').RuleSetRule}
42
+ * JavaScript loader: transpile with Babel.
43
+ */
26
44
  const JSLoader = {
27
45
  test: /^(?!.*\.(stories|component)\.js$).*\.js$/,
28
46
  exclude: /node_modules/,
29
- loader: 'babel-loader',
30
- options: {
31
- configFile: babelConfig,
47
+ use: {
48
+ loader: 'babel-loader',
49
+ options: {
50
+ configFile: babelConfig,
51
+ },
32
52
  },
33
53
  };
34
54
 
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
+ */
40
63
  const CSSLoader = {
41
64
  test: /\.s[ac]ss$/i,
42
65
  exclude: /node_modules/,
@@ -54,8 +77,7 @@ const CSSLoader = {
54
77
  options: {
55
78
  sourceMap: true,
56
79
  postcssOptions: {
57
- config: postcssConfig,
58
- plugins: [['autoprefixer']],
80
+ config: postcssConfigPath,
59
81
  },
60
82
  },
61
83
  },
@@ -64,16 +86,33 @@ const CSSLoader = {
64
86
  options: {
65
87
  api: 'legacy',
66
88
  sourceMap: true,
89
+ implementation: require('sass'),
90
+ webpackImporter: true,
67
91
  sassOptions: {
68
92
  importer: globImporter(),
93
+ legacyImporter: true,
69
94
  outputStyle: 'compressed',
70
95
  silenceDeprecations: ['legacy-js-api'],
96
+ quietDeps: true,
71
97
  },
72
98
  },
73
99
  },
74
100
  ],
75
101
  };
76
102
 
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
+ */
77
116
  const SVGSpriteLoader = {
78
117
  test: /icons\/.*\.svg$/,
79
118
  use: [
@@ -90,6 +129,10 @@ const SVGSpriteLoader = {
90
129
  ],
91
130
  };
92
131
 
132
+ /**
133
+ * @type {import('webpack').RuleSetRule}
134
+ * Twig.js loader for .twig templates.
135
+ */
93
136
  const TwigLoader = {
94
137
  test: /\.twig$/,
95
138
  use: {
@@ -97,10 +140,14 @@ const TwigLoader = {
97
140
  },
98
141
  };
99
142
 
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
+ */
100
147
  export default {
101
148
  JSLoader,
102
149
  CSSLoader,
103
- SVGSpriteLoader,
104
150
  ImageLoader,
151
+ SVGSpriteLoader,
105
152
  TwigLoader,
106
153
  };
@@ -1,95 +1,136 @@
1
- /**
2
- * @fileoverview Configures Webpack plugins.
3
- */
4
-
5
1
  import { resolve, dirname } from 'path';
6
2
  import webpack from 'webpack';
7
3
  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';
10
6
  import CopyPlugin from 'copy-webpack-plugin';
11
7
  import { sync as globSync } from 'glob';
12
8
  import fs from 'fs-extra';
13
9
  import emulsifyConfig from '../../../../../project.emulsify.json' with { type: 'json' };
14
10
 
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);
21
18
  }
19
+ const __dirname = dirname(__filename);
22
20
 
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, '../../../../..');
32
26
 
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');
36
35
 
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');
38
44
 
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
+ */
39
50
  const componentFilesPattern = resolve(
40
51
  srcDir,
41
52
  '**/*.{twig,component.yml,component.json}',
42
53
  );
43
54
 
44
55
  /**
45
- * Prepare a list of patterns for copying Twig and component files.
56
+ * Turn a globbed source list into copy patterns.
46
57
  *
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}>}
49
60
  */
50
61
  function getPatterns(filesMatcher) {
51
- const patterns = [];
52
- globSync(filesMatcher).forEach((file) => {
62
+ return globSync(filesMatcher).map((file) => {
53
63
  const projectPath = file.split('/src/')[0];
54
64
  const srcStructure = file.split(`${srcDir}/`)[1];
55
65
  const parentDir = srcStructure.split('/')[0];
56
- const filePath = file.split(/(foundation\/|components\/|layout\/)/)[2];
66
+ // Consolidate foundation/layout under components for Drupal.
57
67
  const consolidateDirs =
58
68
  parentDir === 'layout' || parentDir === 'foundation'
59
69
  ? '/components/'
60
70
  : '/';
61
- const newfilePath =
71
+ const filePath = file.split(/(foundation\/|components\/|layout\/)/)[2];
72
+ const destDir =
62
73
  emulsifyConfig.project.platform === 'drupal'
63
74
  ? `${projectPath}${consolidateDirs}${parentDir}/${filePath}`
64
75
  : `${projectPath}/dist/${parentDir}/${filePath}`;
65
- patterns.push({
66
- from: file,
67
- to: newfilePath,
68
- });
76
+ return { from: file, to: destDir };
69
77
  });
70
- return patterns;
71
78
  }
72
79
 
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
74
85
  ? 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}/**/*.css`,
98
+ `${distPath}/**/*.js`,
99
+ // but keep any images
100
+ `!${distPath}/**/*.png`,
101
+ `!${distPath}/**/*.jpg`,
102
+ `!${distPath}/**/*.gif`,
103
+ `!${distPath}/**/*.svg`,
104
+ ],
105
+ });
106
+
107
+ /**
108
+ * MiniCssExtractPlugin instance: writes `[name].css` into your dist.
109
+ */
110
+ const CssExtractPlugin = new MiniCssExtractPlugin({
111
+ filename: '[name].css',
112
+ chunkFilename: '[id].css',
113
+ });
76
114
 
77
- const pluginConfig = {
115
+ /**
116
+ * svg-sprite-loader plugin: bundles all /icons/*.svg.
117
+ */
118
+ const SpritePlugin = new SpriteLoaderPlugin({
119
+ plainSprite: true,
120
+ });
121
+
122
+ /**
123
+ * webpack.ProgressPlugin for nice build progress output.
124
+ */
125
+ const ProgressPlugin = new webpack.ProgressPlugin();
126
+
127
+ /**
128
+ * Export all plugins keyed for easy inclusion in your final Webpack config.
129
+ */
130
+ export default {
78
131
  ProgressPlugin,
79
- MiniCssExtractPlugin,
80
- SpriteLoaderPlugin,
132
+ CleanWebpackPlugin: CleanPlugin,
133
+ MiniCssExtractPlugin: CssExtractPlugin,
134
+ SpriteLoaderPlugin: SpritePlugin,
81
135
  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
- }),
93
136
  };
94
-
95
- export default pluginConfig;
@@ -23,6 +23,10 @@ const srcDir = fs.pathExistsSync(resolve(projectDir, 'src'))
23
23
  ? resolve(projectDir, 'src')
24
24
  : resolve(projectDir, 'components');
25
25
 
26
+ if (!fs.pathExistsSync(resolve(srcDir))) {
27
+ fs.mkdirSync(srcDir, { recursive: true });
28
+ }
29
+
26
30
  const aliasPattern = resolve(srcDir, '**/!(_*).twig');
27
31
 
28
32
  /**
@@ -217,4 +217,9 @@ export default {
217
217
  },
218
218
  resolve: resolves.TwigResolve,
219
219
  optimization: optimizers,
220
+ ignoreWarnings: [
221
+ (warning) =>
222
+ warning.message &&
223
+ /Sass @import rules are deprecated/.test(warning.message),
224
+ ],
220
225
  };
@@ -1,4 +1,4 @@
1
- import fs from 'fs';
1
+ import fs from 'fs-extra';
2
2
  import { resolve, dirname } from 'path';
3
3
  import { merge } from 'webpack-merge';
4
4
  import common from './webpack.common.js';
@@ -16,16 +16,18 @@ if (process.platform === 'win32' && _filename.startsWith('/')) {
16
16
 
17
17
  const _dirname = dirname(_filename);
18
18
 
19
- const isDrupal = emulsifyConfig.project.platform === 'drupal';
19
+ // Get directories for file contexts.
20
+ const projectDir = resolve(_dirname, '../../../../..');
20
21
 
21
- // Resolve the src directory alongside how you’re already locating project.emulsify.json
22
- const srcDir = resolve(_dirname, '../../../../../src');
22
+ const srcPath = resolve(projectDir, 'src');
23
+ const srcExists = fs.pathExistsSync(srcPath);
24
+ const isDrupal = emulsifyConfig.project.platform === 'drupal';
23
25
 
24
26
  // Always ignore dist
25
27
  const ignored = ['**/dist/**'];
26
28
 
27
29
  // If it’s Drupal and there is no src/, also ignore components
28
- if (isDrupal && !fs.existsSync(srcDir)) {
30
+ if (isDrupal && srcExists) {
29
31
  ignored.push('**/components/**');
30
32
  }
31
33
 
@@ -33,7 +35,6 @@ export default merge(common, {
33
35
  mode: 'development',
34
36
  devtool: 'source-map',
35
37
  watchOptions: {
36
- // You can supply a RegExp, glob strings, or an array thereof
37
38
  ignored,
38
39
  },
39
40
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emulsify/core",
3
- "version": "3.0.4",
3
+ "version": "3.1.1",
4
4
  "description": "Bundled tooling for Storybook development + Webpack Build",
5
5
  "keywords": [
6
6
  "component library",
@@ -50,11 +50,11 @@
50
50
  "twatch": "jest --no-coverage --watch --verbose"
51
51
  },
52
52
  "dependencies": {
53
- "@babel/core": "^7.27.4",
53
+ "@babel/core": "^7.27.7",
54
54
  "@babel/eslint-parser": "^7.27.5",
55
55
  "@babel/preset-env": "^7.27.2",
56
56
  "@emulsify/cli": "^1.11.4",
57
- "@eslint/js": "^9.28.0",
57
+ "@eslint/js": "^9.30.0",
58
58
  "@storybook/addon-a11y": "^8.6.14",
59
59
  "@storybook/addon-actions": "^8.6.14",
60
60
  "@storybook/addon-essentials": "^8.6.14",
@@ -74,14 +74,14 @@
74
74
  "breakpoint-sass": "^3.0.0",
75
75
  "chalk": "^5.4.1",
76
76
  "clean-webpack-plugin": "^4.0.0",
77
- "concurrently": "^9.1.2",
77
+ "concurrently": "^9.2.0",
78
78
  "copy-webpack-plugin": "^13.0.0",
79
79
  "css-loader": "^7.1.1",
80
- "eslint": "^9.28.0",
80
+ "eslint": "^9.30.0",
81
81
  "eslint-config-prettier": "^10.1.5",
82
- "eslint-plugin-import": "^2.31.0",
83
- "eslint-plugin-jest": "^28.13.5",
84
- "eslint-plugin-prettier": "^5.4.1",
82
+ "eslint-plugin-import": "^2.32.0",
83
+ "eslint-plugin-jest": "^29.0.1",
84
+ "eslint-plugin-prettier": "^5.5.1",
85
85
  "eslint-plugin-security": "^3.0.1",
86
86
  "eslint-plugin-storybook": "^0.12.0",
87
87
  "eslint-webpack-plugin": "^5.0.2",
@@ -95,8 +95,8 @@
95
95
  "imagemin-gifsicle": "^7.0.0",
96
96
  "imagemin-jpegtran": "^8.0.0",
97
97
  "imagemin-optipng": "^8.0.0",
98
- "jest": "^30.0.0",
99
- "jest-environment-jsdom": "^30.0.0",
98
+ "jest": "^30.0.3",
99
+ "jest-environment-jsdom": "^30.0.2",
100
100
  "js-yaml": "^4.1.0",
101
101
  "js-yaml-loader": "^1.2.2",
102
102
  "mini-css-extract-plugin": "^2.9.2",
@@ -104,16 +104,16 @@
104
104
  "normalize.css": "^8.0.1",
105
105
  "open-cli": "^8.0.0",
106
106
  "pa11y": "^9.0.0",
107
- "postcss": "^8.5.5",
107
+ "postcss": "^8.5.6",
108
108
  "postcss-loader": "^8.1.1",
109
109
  "postcss-scss": "^4.0.9",
110
- "ramda": "^0.30.1",
110
+ "ramda": "^0.31.3",
111
111
  "regenerator-runtime": "^0.14.1",
112
112
  "sass": "^1.89.2",
113
113
  "sass-loader": "^16.0.5",
114
114
  "storybook": "^8.6.14",
115
115
  "style-dictionary": "^4.4.0",
116
- "stylelint": "^16.20.0",
116
+ "stylelint": "^16.21.0",
117
117
  "stylelint-config-standard-scss": "^15.0.1",
118
118
  "stylelint-prettier": "^5.0.3",
119
119
  "stylelint-selector-bem-pattern": "^4.0.1",
@@ -138,7 +138,7 @@
138
138
  "@semantic-release/release-notes-generator": "^14.0.3",
139
139
  "all-contributors-cli": "^6.26.1",
140
140
  "husky": "^9.1.7",
141
- "lint-staged": "^16.1.0",
141
+ "lint-staged": "^16.1.2",
142
142
  "semantic-release": "^24.2.5"
143
143
  },
144
144
  "overrides": {