@emulsify/core 3.2.0 → 3.3.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.
- package/config/webpack/loaders.js +29 -15
- package/config/webpack/plugins.js +183 -36
- package/config/webpack/resolves.js +110 -44
- package/config/webpack/webpack.common.js +197 -156
- package/config/webpack/webpack.dev.js +2 -1
- package/package.json +7 -7
- package/scripts/a11y.js +83 -23
- package/scripts/a11y.test.js +51 -38
|
@@ -1,26 +1,99 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @fileoverview Webpack
|
|
3
|
-
*
|
|
2
|
+
* @fileoverview Build Webpack entries and export the configuration.
|
|
3
|
+
* - Discovers JS/SCSS assets (base + component) via glob patterns
|
|
4
|
+
* - Shapes output paths based on platform and SDC (singleDirectoryComponents)
|
|
5
|
+
* - Wires up loaders, plugins, and optimizations
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
|
-
import {
|
|
8
|
+
import { posix as path } from 'node:path';
|
|
7
9
|
import { sync as globSync } from 'glob';
|
|
8
10
|
import fs from 'fs-extra';
|
|
11
|
+
|
|
9
12
|
import loaders from './loaders.js';
|
|
10
13
|
import plugins from './plugins.js';
|
|
11
14
|
import resolves from './resolves.js';
|
|
12
15
|
import optimizers from './optimizers.js';
|
|
13
16
|
import emulsifyConfig from '../../../../../project.emulsify.json' with { type: 'json' };
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Resolve the directory of this file (without fileURLToPath).
|
|
20
|
+
* @type {string}
|
|
21
|
+
*/
|
|
16
22
|
let _filename = decodeURIComponent(new URL(import.meta.url).pathname);
|
|
17
|
-
|
|
18
|
-
// On Windows, remove the leading slash (e.g. "/C:/path" -> "C:/path")
|
|
19
23
|
if (process.platform === 'win32' && _filename.startsWith('/')) {
|
|
20
24
|
_filename = _filename.slice(1);
|
|
21
25
|
}
|
|
26
|
+
const _dirname = path.dirname(_filename);
|
|
27
|
+
|
|
28
|
+
/** @type {string} Absolute project root (five levels up from this file). */
|
|
29
|
+
const projectDir = path.resolve(_dirname, '../../../../..');
|
|
30
|
+
|
|
31
|
+
/** @type {boolean} True when a "src/" directory exists (WP layout). */
|
|
32
|
+
const hasSrc = fs.pathExistsSync(path.resolve(projectDir, 'src'));
|
|
33
|
+
|
|
34
|
+
/** @type {string} The canonical source directory ("src" if present, else "components"). */
|
|
35
|
+
const srcDir = hasSrc
|
|
36
|
+
? path.resolve(projectDir, 'src')
|
|
37
|
+
: path.resolve(projectDir, 'components');
|
|
38
|
+
|
|
39
|
+
/** @type {boolean} True when platform is Drupal (affects component output root). */
|
|
40
|
+
const isDrupal = emulsifyConfig?.project?.platform === 'drupal';
|
|
41
|
+
|
|
42
|
+
/** @type {boolean} Respect SDC (single-directory-components) layout if explicitly true. */
|
|
43
|
+
const SDC = Boolean(emulsifyConfig?.project?.singleDirectoryComponents);
|
|
44
|
+
|
|
45
|
+
/** @type {string} Output base for "global" assets. */
|
|
46
|
+
const globalOutBase = hasSrc ? 'dist/global' : 'dist';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Create a path under the component output root.
|
|
50
|
+
* - In Drupal + src layout, components resolve to "components/…"
|
|
51
|
+
* - Otherwise, they resolve to "dist/components/…"
|
|
52
|
+
* @param {string} subpath - Component-local subpath (no extension).
|
|
53
|
+
* @returns {string} Component output path segment.
|
|
54
|
+
*/
|
|
55
|
+
const componentOutPath = (subpath) =>
|
|
56
|
+
(isDrupal && hasSrc ? 'components' : 'dist/components') + '/' + subpath;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Join segments with POSIX semantics (forward slashes), trimming empties.
|
|
60
|
+
* @param {...string} segs - Path segments.
|
|
61
|
+
* @returns {string} POSIX-joined path.
|
|
62
|
+
*/
|
|
63
|
+
const pj = (...segs) => path.join(...segs.filter(Boolean));
|
|
22
64
|
|
|
23
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Compute the “dist subpath” for a non-component asset.
|
|
67
|
+
* Inserts a type folder ("js" or "css") when SDC = false.
|
|
68
|
+
* Drops the original file extension.
|
|
69
|
+
* @param {string} absFile - Absolute file path.
|
|
70
|
+
* @param {'js'|'css'} type - Asset type.
|
|
71
|
+
* @returns {string} Subpath under the global output base (no extension).
|
|
72
|
+
*/
|
|
73
|
+
const distSubpathForBase = (absFile, type) => {
|
|
74
|
+
const rel = path.relative(srcDir, absFile);
|
|
75
|
+
const dir = path.dirname(rel);
|
|
76
|
+
const name = path.basename(rel, '.' + type);
|
|
77
|
+
return SDC ? pj(dir, name) : pj(dir, type, name);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Compute the “dist subpath” for a component asset located under "…/components".
|
|
82
|
+
* Inserts a type folder ("js" or "css") when SDC = false.
|
|
83
|
+
* Drops the original file extension.
|
|
84
|
+
* @param {string} absFile - Absolute file path.
|
|
85
|
+
* @param {'js'|'scss'} type - Source type (scss maps to 'css').
|
|
86
|
+
* @returns {string} Component-local subpath (no extension).
|
|
87
|
+
*/
|
|
88
|
+
const distSubpathForComponent = (absFile, type) => {
|
|
89
|
+
const relFromComponents = path.relative(pj(srcDir, 'components'), absFile);
|
|
90
|
+
const dir = path.dirname(relFromComponents);
|
|
91
|
+
const isStyle = type === 'scss';
|
|
92
|
+
const outTypeDir = isStyle ? 'css' : 'js';
|
|
93
|
+
const ext = isStyle ? '.scss' : '.js';
|
|
94
|
+
const name = path.basename(relFromComponents, ext);
|
|
95
|
+
return SDC ? pj(dir, name) : pj(dir, outTypeDir, name);
|
|
96
|
+
};
|
|
24
97
|
|
|
25
98
|
/**
|
|
26
99
|
* Sanitize a file path by removing unwanted characters.
|
|
@@ -30,170 +103,134 @@ const _dirname = dirname(_filename);
|
|
|
30
103
|
*/
|
|
31
104
|
const sanitizePath = (inputPath) => inputPath.replace(/[^a-zA-Z0-9/_-]/g, '');
|
|
32
105
|
|
|
33
|
-
|
|
34
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Reject keys that could touch object internals even after sanitization.
|
|
108
|
+
* @param {string} k
|
|
109
|
+
* @returns {boolean}
|
|
110
|
+
*/
|
|
111
|
+
const isDangerousKey = (k) =>
|
|
112
|
+
k.includes('__proto__') || k.includes('prototype') || k === 'constructor';
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Add a file under an entry key; if the key exists, merge to an array.
|
|
116
|
+
* Keeps JS before SCSS for deterministic order.
|
|
117
|
+
*
|
|
118
|
+
* @param {Map<string, string | string[]>} map
|
|
119
|
+
* @param {string} key
|
|
120
|
+
* @param {string} file
|
|
121
|
+
* @returns {void}
|
|
122
|
+
*/
|
|
123
|
+
const addEntry = (map, key, file) => {
|
|
124
|
+
const safeKey = sanitizePath(String(key));
|
|
125
|
+
if (!safeKey || isDangerousKey(safeKey)) return;
|
|
35
126
|
|
|
36
|
-
const
|
|
37
|
-
const isSrcExists = fs.pathExistsSync(srcPath);
|
|
38
|
-
const srcDir = isSrcExists ? srcPath : resolve(projectDir, 'components');
|
|
39
|
-
const isDrupal = emulsifyConfig.project.platform === 'drupal';
|
|
127
|
+
const current = map.get(safeKey);
|
|
40
128
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const ComponentScssPattern = fs.pathExistsSync(resolve(projectDir, 'src'))
|
|
46
|
-
? resolve(srcDir, 'components/**/!(_*|cl-*|sb-*).scss')
|
|
47
|
-
: resolve(srcDir, '**/!(_*|cl-*|sb-*).scss');
|
|
48
|
-
const ComponentLibraryScssPattern = resolve(srcDir, '**/*{cl-*,sb-*}.scss');
|
|
49
|
-
|
|
50
|
-
// Glob pattern for JS files.
|
|
51
|
-
const BaseJsPattern = fs.pathExistsSync(resolve(projectDir, 'src'))
|
|
52
|
-
? resolve(
|
|
53
|
-
srcDir,
|
|
54
|
-
'!(components|util)/**/!(*.stories|*.component|*.min|*.test).js',
|
|
55
|
-
)
|
|
56
|
-
: '';
|
|
57
|
-
const ComponentJsPattern = fs.pathExistsSync(resolve(projectDir, 'src'))
|
|
58
|
-
? resolve(srcDir, 'components/**/!(*.stories|*.component|*.min|*.test).js')
|
|
59
|
-
: resolve(srcDir, '**/!(*.stories|*.component|*.min|*.test).js');
|
|
129
|
+
if (!current) {
|
|
130
|
+
map.set(safeKey, file);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
60
133
|
|
|
61
|
-
|
|
62
|
-
|
|
134
|
+
const arr = Array.isArray(current) ? current : [current];
|
|
135
|
+
if (!arr.includes(file)) arr.push(file);
|
|
136
|
+
|
|
137
|
+
// Optional: ensure JS comes before SCSS
|
|
138
|
+
arr.sort((a, b) => {
|
|
139
|
+
const ax = a.endsWith('.js') ? 0 : 1;
|
|
140
|
+
const bx = b.endsWith('.js') ? 0 : 1;
|
|
141
|
+
return ax - bx || a.localeCompare(b);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
map.set(safeKey, arr);
|
|
145
|
+
};
|
|
63
146
|
|
|
64
147
|
/**
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* @
|
|
68
|
-
* @param {string} replacement - The string to replace the last slash with.
|
|
69
|
-
* @returns {string} The modified string.
|
|
148
|
+
* Safe glob wrapper: returns [] if the pattern is falsy.
|
|
149
|
+
* @param {string} pattern - Glob pattern.
|
|
150
|
+
* @returns {string[]} Matching file paths.
|
|
70
151
|
*/
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
)
|
|
79
|
-
|
|
152
|
+
const glob = (pattern) => (pattern ? globSync(pattern) : []);
|
|
153
|
+
|
|
154
|
+
/* -------------------------------------------------------------------------- */
|
|
155
|
+
/* GLOBS */
|
|
156
|
+
/* -------------------------------------------------------------------------- */
|
|
157
|
+
|
|
158
|
+
const BaseScssPattern = hasSrc
|
|
159
|
+
? pj(srcDir, '!(components|util)/**/!(_*|cl-*|sb-*).scss')
|
|
160
|
+
: '';
|
|
161
|
+
|
|
162
|
+
const ComponentScssPattern = hasSrc
|
|
163
|
+
? pj(srcDir, 'components/**/!(_*|cl-*|sb-*).scss')
|
|
164
|
+
: pj(srcDir, '**/!(_*|cl-*|sb-*).scss');
|
|
165
|
+
|
|
166
|
+
const ComponentLibraryScssPattern = pj(srcDir, '**/*{cl-*,sb-*}.scss');
|
|
167
|
+
|
|
168
|
+
const BaseJsPattern = hasSrc
|
|
169
|
+
? pj(srcDir, '!(components|util)/**/!(*.stories|*.component|*.min|*.test).js')
|
|
170
|
+
: '';
|
|
171
|
+
|
|
172
|
+
const ComponentJsPattern = hasSrc
|
|
173
|
+
? pj(srcDir, 'components/**/!(*.stories|*.component|*.min|*.test).js')
|
|
174
|
+
: pj(srcDir, '**/!(*.stories|*.component|*.min|*.test).js');
|
|
175
|
+
|
|
176
|
+
/* -------------------------------------------------------------------------- */
|
|
177
|
+
/* ENTRY BUILD */
|
|
178
|
+
/* -------------------------------------------------------------------------- */
|
|
80
179
|
|
|
81
180
|
/**
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
* @param {string} BaseJsMatcher - Glob pattern for base JS files.
|
|
85
|
-
* @param {string} jsMatcher - Glob pattern for component JS files.
|
|
86
|
-
* @param {string} BaseScssMatcher - Glob pattern for base SCSS files.
|
|
87
|
-
* @param {string} ComponentScssMatcher - Glob pattern for component SCSS files.
|
|
88
|
-
* @param {string} ComponentLibraryScssMatcher - Glob pattern for component library SCSS files.
|
|
89
|
-
* @param {string} spriteMatcher - Glob pattern for SVG sprite configuration.
|
|
90
|
-
* @returns {Object} An object containing the Webpack entries.
|
|
181
|
+
* Build the complete Webpack entries map.
|
|
182
|
+
* @returns {Record<string,string>} Webpack entries.
|
|
91
183
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
BaseScssMatcher,
|
|
96
|
-
ComponentScssMatcher,
|
|
97
|
-
ComponentLibraryScssMatcher,
|
|
98
|
-
spriteMatcher,
|
|
99
|
-
) {
|
|
100
|
-
const entries = {};
|
|
101
|
-
|
|
102
|
-
/**
|
|
103
|
-
* Add an entry to the entries object after sanitizing the key.
|
|
104
|
-
*
|
|
105
|
-
* @param {string} key - The key for the entry.
|
|
106
|
-
* @param {string} file - The file path to associate with the entry.
|
|
107
|
-
*/
|
|
108
|
-
const addEntry = (key, file) => {
|
|
109
|
-
const sanitizedKey = sanitizePath(key);
|
|
110
|
-
if (
|
|
111
|
-
sanitizedKey &&
|
|
112
|
-
!Object.prototype.hasOwnProperty.call(entries, sanitizedKey)
|
|
113
|
-
) {
|
|
114
|
-
// eslint-disable-next-line security/detect-object-injection
|
|
115
|
-
entries[sanitizedKey] = file;
|
|
116
|
-
}
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
// Non-component or global JS entries.
|
|
120
|
-
globSync(BaseJsMatcher).forEach((file) => {
|
|
121
|
-
const filePath = file.split(`${srcDir}/`)[1];
|
|
122
|
-
const pathParts = filePath.split('/');
|
|
123
|
-
const filePathDist = `${pathParts.slice(0, -1).join('/')}/js/${pathParts
|
|
124
|
-
.at(-1)
|
|
125
|
-
.replace('.js', '')}`;
|
|
126
|
-
const newFilePath = fs.pathExistsSync(resolve(projectDir, 'src'))
|
|
127
|
-
? `dist/global/${filePathDist}`
|
|
128
|
-
: `dist/js/${filePathDist}`;
|
|
129
|
-
addEntry(newFilePath, file);
|
|
130
|
-
});
|
|
184
|
+
const buildEntries = () => {
|
|
185
|
+
/** @type {Map<string, string | string[]>} */
|
|
186
|
+
const entries = new Map();
|
|
131
187
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const newFilePath = `${prefix}/${filePathDist}`;
|
|
140
|
-
addEntry(newFilePath, file);
|
|
141
|
-
}
|
|
142
|
-
});
|
|
188
|
+
/* ----------------------------- Base / Global JS ----------------------------- */
|
|
189
|
+
for (const file of glob(BaseJsPattern)) {
|
|
190
|
+
const sub = distSubpathForBase(file, 'js');
|
|
191
|
+
// If no "src/", legacy layout puts global JS directly under "dist/js".
|
|
192
|
+
const outRoot = hasSrc ? pj(globalOutBase) : pj('dist', 'js');
|
|
193
|
+
addEntry(entries, pj(outRoot, sub), file);
|
|
194
|
+
}
|
|
143
195
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.replace('.scss', '')}`;
|
|
151
|
-
const newFilePath = fs.pathExistsSync(resolve(projectDir, 'src'))
|
|
152
|
-
? `dist/global/${filePathDist}`
|
|
153
|
-
: `dist/css/${filePathDist}`;
|
|
154
|
-
addEntry(newFilePath, file);
|
|
155
|
-
});
|
|
196
|
+
/* --------------------------- Component JS (no dist) -------------------------- */
|
|
197
|
+
for (const file of glob(ComponentJsPattern)) {
|
|
198
|
+
if (file.includes('/dist/')) continue; // guard against accidental recursion
|
|
199
|
+
const sub = distSubpathForComponent(file, 'js');
|
|
200
|
+
addEntry(entries, componentOutPath(sub), file);
|
|
201
|
+
}
|
|
156
202
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
addEntry(newFilePath, file);
|
|
165
|
-
});
|
|
203
|
+
/* ------------------------------ Base / Global CSS --------------------------- */
|
|
204
|
+
for (const file of glob(BaseScssPattern)) {
|
|
205
|
+
const sub = distSubpathForBase(file, 'css');
|
|
206
|
+
// If no "src/", legacy layout puts global CSS directly under "dist/css".
|
|
207
|
+
const outRoot = hasSrc ? pj(globalOutBase) : pj('dist', 'css');
|
|
208
|
+
addEntry(entries, pj(outRoot, sub), file);
|
|
209
|
+
}
|
|
166
210
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
});
|
|
211
|
+
/* ---------------------------- Component CSS (SCSS) --------------------------- */
|
|
212
|
+
for (const file of glob(ComponentScssPattern)) {
|
|
213
|
+
const sub = distSubpathForComponent(file, 'scss'); // maps to css
|
|
214
|
+
addEntry(entries, componentOutPath(sub), file);
|
|
215
|
+
}
|
|
173
216
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
});
|
|
217
|
+
/* -------------------------- Component Library (Storybook) -------------------- */
|
|
218
|
+
for (const file of glob(ComponentLibraryScssPattern)) {
|
|
219
|
+
const rel = path.relative(srcDir, file).replace(/\.scss$/, '');
|
|
220
|
+
addEntry(entries, pj('dist', 'storybook', rel), file);
|
|
221
|
+
}
|
|
180
222
|
|
|
181
|
-
return entries;
|
|
182
|
-
}
|
|
223
|
+
return Object.fromEntries(entries);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
/* -------------------------------------------------------------------------- */
|
|
227
|
+
/* WEBPACK CONFIG EXPORT */
|
|
228
|
+
/* -------------------------------------------------------------------------- */
|
|
183
229
|
|
|
184
230
|
export default {
|
|
185
231
|
target: 'web',
|
|
186
|
-
stats: {
|
|
187
|
-
|
|
188
|
-
},
|
|
189
|
-
entry: getEntries(
|
|
190
|
-
BaseJsPattern,
|
|
191
|
-
ComponentJsPattern,
|
|
192
|
-
BaseScssPattern,
|
|
193
|
-
ComponentScssPattern,
|
|
194
|
-
ComponentLibraryScssPattern,
|
|
195
|
-
spritePattern,
|
|
196
|
-
),
|
|
232
|
+
stats: { errorDetails: true },
|
|
233
|
+
entry: buildEntries(),
|
|
197
234
|
module: {
|
|
198
235
|
rules: [
|
|
199
236
|
loaders.CSSLoader,
|
|
@@ -204,22 +241,26 @@ export default {
|
|
|
204
241
|
],
|
|
205
242
|
},
|
|
206
243
|
plugins: [
|
|
244
|
+
plugins.RemoveEmptyJS,
|
|
207
245
|
plugins.MiniCssExtractPlugin,
|
|
208
246
|
plugins.ImageminPlugin,
|
|
209
|
-
plugins.
|
|
247
|
+
plugins.SpritePlugin,
|
|
210
248
|
plugins.ProgressPlugin,
|
|
211
249
|
plugins.CopyTwigPlugin,
|
|
250
|
+
plugins.CopyComponentAssetsPlugin,
|
|
251
|
+
...(plugins.CopyGlobalAssetsPlugin ? [plugins.CopyGlobalAssetsPlugin] : []),
|
|
212
252
|
plugins.CleanWebpackPlugin,
|
|
213
253
|
],
|
|
214
254
|
output: {
|
|
215
|
-
path:
|
|
255
|
+
path: projectDir,
|
|
216
256
|
filename: '[name].js',
|
|
217
257
|
},
|
|
218
258
|
resolve: resolves.TwigResolve,
|
|
219
259
|
optimization: optimizers,
|
|
260
|
+
// Quiet deprecation noise from Sass @import warnings
|
|
220
261
|
ignoreWarnings: [
|
|
221
262
|
(warning) =>
|
|
222
|
-
warning
|
|
263
|
+
Boolean(warning?.message) &&
|
|
223
264
|
/Sass @import rules are deprecated/.test(warning.message),
|
|
224
265
|
],
|
|
225
266
|
};
|
|
@@ -27,13 +27,14 @@ const isDrupal = emulsifyConfig.project.platform === 'drupal';
|
|
|
27
27
|
const ignored = ['**/dist/**'];
|
|
28
28
|
|
|
29
29
|
// If it’s Drupal and there is no src/, also ignore components
|
|
30
|
-
if (isDrupal && srcExists) {
|
|
30
|
+
if (isDrupal && !srcExists) {
|
|
31
31
|
ignored.push('**/components/**');
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export default merge(common, {
|
|
35
35
|
mode: 'development',
|
|
36
36
|
devtool: 'source-map',
|
|
37
|
+
watch: true,
|
|
37
38
|
watchOptions: {
|
|
38
39
|
ignored,
|
|
39
40
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emulsify/core",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.3.1",
|
|
4
4
|
"description": "Bundled tooling for Storybook development + Webpack Build",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"component library",
|
|
@@ -72,7 +72,6 @@
|
|
|
72
72
|
"babel-preset-minify": "^0.5.2",
|
|
73
73
|
"bem-twig-extension": "^0.1.1",
|
|
74
74
|
"breakpoint-sass": "^3.0.0",
|
|
75
|
-
"chalk": "^5.6.0",
|
|
76
75
|
"clean-webpack-plugin": "^4.0.0",
|
|
77
76
|
"concurrently": "^9.2.1",
|
|
78
77
|
"copy-webpack-plugin": "^13.0.1",
|
|
@@ -108,16 +107,16 @@
|
|
|
108
107
|
"postcss-scss": "^4.0.9",
|
|
109
108
|
"ramda": "^0.31.3",
|
|
110
109
|
"regenerator-runtime": "^0.14.1",
|
|
111
|
-
"sass": "^1.92.
|
|
110
|
+
"sass": "^1.92.1",
|
|
112
111
|
"sass-loader": "^16.0.5",
|
|
113
112
|
"storybook": "^8.6.14",
|
|
114
|
-
"style-dictionary": "^
|
|
115
|
-
"stylelint": "^16.
|
|
113
|
+
"style-dictionary": "^5.0.4",
|
|
114
|
+
"stylelint": "^16.24.0",
|
|
116
115
|
"stylelint-config-standard-scss": "^15.0.1",
|
|
117
116
|
"stylelint-prettier": "^5.0.3",
|
|
118
117
|
"stylelint-selector-bem-pattern": "^4.0.1",
|
|
119
118
|
"stylelint-webpack-plugin": "^5.0.1",
|
|
120
|
-
"svg-
|
|
119
|
+
"svg-spritemap-webpack-plugin": "^5.0.1",
|
|
121
120
|
"token-transformer": "^0.0.33",
|
|
122
121
|
"twig-drupal-filters": "^3.2.0",
|
|
123
122
|
"twig-testing-library": "^1.2.0",
|
|
@@ -125,6 +124,7 @@
|
|
|
125
124
|
"webpack": "^5.101.3",
|
|
126
125
|
"webpack-cli": "^6.0.1",
|
|
127
126
|
"webpack-merge": "^6.0.1",
|
|
127
|
+
"webpack-remove-empty-scripts": "^1.1.1",
|
|
128
128
|
"yaml": "^2.8.1"
|
|
129
129
|
},
|
|
130
130
|
"devDependencies": {
|
|
@@ -134,7 +134,7 @@
|
|
|
134
134
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
135
135
|
"@semantic-release/git": "^10.0.1",
|
|
136
136
|
"@semantic-release/github": "^11.0.5",
|
|
137
|
-
"@semantic-release/release-notes-generator": "^14.0
|
|
137
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
138
138
|
"all-contributors-cli": "^6.26.1",
|
|
139
139
|
"husky": "^9.1.7",
|
|
140
140
|
"lint-staged": "^16.1.6",
|
package/scripts/a11y.js
CHANGED
|
@@ -1,61 +1,113 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* @
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* @fileoverview a11y.js
|
|
4
|
+
* Runs accessibility linting (pa11y/axe) against a Storybook build
|
|
5
|
+
* and reports issues.
|
|
6
6
|
*/
|
|
7
|
-
const chalk = import("chalk").then(m => m.default);
|
|
8
7
|
|
|
9
8
|
const R = require('ramda');
|
|
10
9
|
const path = require('path');
|
|
11
10
|
const pa11y = require('pa11y');
|
|
11
|
+
|
|
12
12
|
const {
|
|
13
13
|
storybookBuildDir,
|
|
14
14
|
pa11y: pa11yConfig,
|
|
15
15
|
} = require('../config/a11y.config.js');
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
// Project-specific configuration.
|
|
17
18
|
const {
|
|
18
19
|
ignore,
|
|
19
20
|
components,
|
|
20
21
|
} = require('../../../config/emulsify-core/a11y.config.js');
|
|
21
22
|
|
|
23
|
+
/** Absolute path to Storybook build directory. */
|
|
22
24
|
const STORYBOOK_BUILD_DIR = path.resolve(__dirname, '../', storybookBuildDir);
|
|
25
|
+
/** Absolute path to Storybook iframe file used for per-story rendering. */
|
|
23
26
|
const STORYBOOK_IFRAME = path.join(STORYBOOK_BUILD_DIR, 'iframe.html');
|
|
24
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Map pa11y/axe severity to a label (historically a color name).
|
|
30
|
+
* Retained for backward compatibility, but not used for styling anymore.
|
|
31
|
+
* @deprecated Colors are no longer used; this function returns a label only.
|
|
32
|
+
* @param {'error'|'warning'|'notice'} severity
|
|
33
|
+
* @returns {'red'|'yellow'|'blue'|undefined}
|
|
34
|
+
*/
|
|
25
35
|
const severityToColor = R.cond([
|
|
26
36
|
[R.equals('error'), R.always('red')],
|
|
27
37
|
[R.equals('warning'), R.always('yellow')],
|
|
28
38
|
[R.equals('notice'), R.always('blue')],
|
|
29
39
|
]);
|
|
30
40
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} Pa11yIssue
|
|
43
|
+
* @property {string} code - Rule identifier.
|
|
44
|
+
* @property {'error'|'warning'|'notice'} type - Severity level.
|
|
45
|
+
* @property {string} message - Human-readable description.
|
|
46
|
+
* @property {string} context - HTML context snippet.
|
|
47
|
+
* @property {string} selector - CSS selector for the node.
|
|
48
|
+
* @property {{ description?: string }} [runnerExtras] - Extra data from the runner.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Determine whether an issue should be reported (not ignored).
|
|
53
|
+
* @param {Pa11yIssue} issue
|
|
54
|
+
* @returns {boolean} True if the issue is NOT ignored and should be logged.
|
|
55
|
+
*/
|
|
56
|
+
const issueIsValid = (issue) => {
|
|
57
|
+
const code = issue?.code;
|
|
58
|
+
const description = issue?.runnerExtras?.description;
|
|
59
|
+
const codeIgnored = Array.isArray(ignore?.codes) && ignore.codes.includes(code);
|
|
60
|
+
const descIgnored =
|
|
61
|
+
description &&
|
|
62
|
+
Array.isArray(ignore?.descriptions) &&
|
|
63
|
+
ignore.descriptions.includes(description);
|
|
64
|
+
return !(codeIgnored || descIgnored);
|
|
65
|
+
};
|
|
35
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Log a single accessibility issue in a readable, colorless block.
|
|
69
|
+
* @param {Pa11yIssue} issue
|
|
70
|
+
* @returns {void}
|
|
71
|
+
*/
|
|
36
72
|
const logIssue = ({ type: severity, message, context, selector }) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
73
|
+
const lines = [
|
|
74
|
+
'', // leading blank for readability
|
|
75
|
+
`severity: ${severity}`,
|
|
76
|
+
`message: ${message}`,
|
|
77
|
+
`context: ${context}`,
|
|
78
|
+
`selector: ${selector}`,
|
|
79
|
+
'',
|
|
80
|
+
];
|
|
81
|
+
// eslint-disable-next-line no-console
|
|
82
|
+
console.log(lines.join('\n'));
|
|
43
83
|
};
|
|
44
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Log a report for a single component/page and return whether it had issues.
|
|
87
|
+
* @param {{ issues: Pa11yIssue[], pageUrl: string }} report
|
|
88
|
+
* @returns {boolean} True if the component has at least one non-ignored issue.
|
|
89
|
+
*/
|
|
45
90
|
const logReport = ({ issues, pageUrl }) => {
|
|
46
|
-
const validIssues = issues.filter(issueIsValid);
|
|
91
|
+
const validIssues = (issues || []).filter(issueIsValid);
|
|
47
92
|
const hasIssues = validIssues.length > 0;
|
|
48
93
|
|
|
49
94
|
if (hasIssues) {
|
|
50
|
-
|
|
51
|
-
|
|
95
|
+
// eslint-disable-next-line no-console
|
|
96
|
+
console.log(`Issues found in component: ${pageUrl}`);
|
|
97
|
+
validIssues.forEach(logIssue);
|
|
52
98
|
} else {
|
|
53
|
-
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.log(`No issues found in component: ${pageUrl}`);
|
|
54
101
|
}
|
|
55
102
|
|
|
56
103
|
return hasIssues;
|
|
57
104
|
};
|
|
58
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Run pa11y on a single Storybook story by its ID.
|
|
108
|
+
* @param {string} name - Story ID (e.g., "components-button--primary").
|
|
109
|
+
* @returns {Promise<{ issues: Pa11yIssue[], pageUrl: string }>} Pa11y result.
|
|
110
|
+
*/
|
|
59
111
|
const lintComponent = async (name) =>
|
|
60
112
|
pa11y(`${STORYBOOK_IFRAME}?id=${name}`, {
|
|
61
113
|
includeNotices: true,
|
|
@@ -64,21 +116,29 @@ const lintComponent = async (name) =>
|
|
|
64
116
|
...pa11yConfig,
|
|
65
117
|
});
|
|
66
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Lint a list of components, log reports, and exit(1) if any have issues.
|
|
121
|
+
* @param {string[]} names - List of Storybook story IDs.
|
|
122
|
+
* @returns {Promise<void>}
|
|
123
|
+
*/
|
|
67
124
|
const lintReportAndExit = R.pipe(
|
|
68
|
-
|
|
69
|
-
(
|
|
125
|
+
/** @param {string[]} list */
|
|
126
|
+
(list) => list.map(lintComponent),
|
|
127
|
+
(promises) => Promise.all(promises),
|
|
70
128
|
R.andThen(
|
|
71
129
|
R.pipe(
|
|
72
|
-
|
|
130
|
+
/** @param {Array<{issues: Pa11yIssue[], pageUrl: string}>} results */
|
|
131
|
+
(results) => results.map(logReport),
|
|
73
132
|
R.reject(R.equals(false)),
|
|
74
133
|
R.unless(R.isEmpty, () => process.exit(1)),
|
|
75
134
|
),
|
|
76
135
|
),
|
|
77
136
|
);
|
|
78
137
|
|
|
79
|
-
// Only perform linting/reporting when instructed.
|
|
138
|
+
// Only perform linting/reporting when instructed via "-r".
|
|
80
139
|
/* istanbul ignore next */
|
|
81
140
|
if (R.pathEq(['argv', 2], '-r')(process)) {
|
|
141
|
+
// eslint-disable-next-line promise/catch-or-return
|
|
82
142
|
lintReportAndExit(components);
|
|
83
143
|
}
|
|
84
144
|
|