@emulsify/core 4.0.4 → 4.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.
- package/.storybook/css-components.js +3 -0
- package/.storybook/css-dist.js +1 -0
- package/.storybook/main-static-assets.js +217 -0
- package/.storybook/main-vite.js +224 -0
- package/.storybook/main.js +10 -370
- package/.storybook/manager-head.css +120 -0
- package/.storybook/preview.js +8 -18
- package/.storybook/utils.js +1 -1
- package/README.md +11 -3
- package/config/a11y.config.js +2 -1
- package/config/vite/environment.js +2 -0
- package/config/vite/plugins/virtual-twig-asset-sources.js +29 -3
- package/config/vite/project-config.js +65 -0
- package/config/vite/project-structure.js +35 -1
- package/package.json +5 -1
- package/scripts/a11y.js +150 -13
- package/scripts/audit.js +14 -2
- package/src/storybook/preview-decorator.js +45 -0
|
@@ -16,6 +16,7 @@ export const VIRTUAL_TWIG_ASSET_SOURCES_ID =
|
|
|
16
16
|
'virtual:emulsify-twig-asset-sources';
|
|
17
17
|
const RESOLVED_VIRTUAL_TWIG_ASSET_SOURCES_ID = `\0${VIRTUAL_TWIG_ASSET_SOURCES_ID}`;
|
|
18
18
|
const GENERATED_ASSET_ALIASES = new Set(['icons.svg']);
|
|
19
|
+
const GENERATED_ASSET_ROOTS = ['/dist/assets'];
|
|
19
20
|
const PUBLIC_ASSET_ROOTS = new Map([
|
|
20
21
|
['/assets', '/assets'],
|
|
21
22
|
['/dist/assets', '/assets'],
|
|
@@ -116,14 +117,21 @@ export function assetSourceRoots(env) {
|
|
|
116
117
|
*/
|
|
117
118
|
export function generatedAssetSourceRoots(env) {
|
|
118
119
|
return unique(
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
GENERATED_ASSET_ROOTS.map((root) =>
|
|
121
|
+
toAbsoluteAssetRoot(env?.projectDir, root),
|
|
122
|
+
)
|
|
121
123
|
.filter((root) => root && safeExists(root))
|
|
122
124
|
.map((root) => toRootRelativePath(env?.projectDir, root))
|
|
123
125
|
.filter(Boolean),
|
|
124
126
|
);
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
function generatedAssetRootPrefixes() {
|
|
130
|
+
return GENERATED_ASSET_ROOTS.map((root) =>
|
|
131
|
+
`${root}/`.replace(/\/{2,}/g, '/'),
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
127
135
|
/**
|
|
128
136
|
* Build Vite glob patterns from text asset roots.
|
|
129
137
|
*
|
|
@@ -216,6 +224,20 @@ export function publicAssetSourceEntries(env) {
|
|
|
216
224
|
}
|
|
217
225
|
}
|
|
218
226
|
|
|
227
|
+
for (const root of generatedAssetRootPrefixes()) {
|
|
228
|
+
const publicBase = publicAssetBaseForRoot(root);
|
|
229
|
+
if (!publicBase) continue;
|
|
230
|
+
|
|
231
|
+
for (const alias of GENERATED_ASSET_ALIASES) {
|
|
232
|
+
const key = `${root.replace(/\/+$/, '')}/${alias}`.replace(
|
|
233
|
+
/\/{2,}/g,
|
|
234
|
+
'/',
|
|
235
|
+
);
|
|
236
|
+
const url = `${publicBase}${alias}`.replace(/\/{2,}/g, '/');
|
|
237
|
+
entries.set(key, { key, url });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
219
241
|
return Array.from(entries.values());
|
|
220
242
|
}
|
|
221
243
|
|
|
@@ -232,6 +254,10 @@ export function generateVirtualTwigAssetSourcesModule(env) {
|
|
|
232
254
|
const generatedRootPrefixes = generatedAssetSourceRoots(env).map((root) =>
|
|
233
255
|
`${root === '/' ? '' : root}/`.replace(/\/{2,}/g, '/'),
|
|
234
256
|
);
|
|
257
|
+
const allGeneratedRootPrefixes = unique([
|
|
258
|
+
...generatedRootPrefixes,
|
|
259
|
+
...generatedAssetRootPrefixes(),
|
|
260
|
+
]);
|
|
235
261
|
const patterns = assetSourceGlobPatterns(env);
|
|
236
262
|
const globEntries = patterns.length
|
|
237
263
|
? patterns
|
|
@@ -255,7 +281,7 @@ export function generateVirtualTwigAssetSourcesModule(env) {
|
|
|
255
281
|
*/
|
|
256
282
|
|
|
257
283
|
export const assetRootPrefixes = ${JSON.stringify(rootPrefixes)};
|
|
258
|
-
export const generatedAssetRootPrefixes = ${JSON.stringify(
|
|
284
|
+
export const generatedAssetRootPrefixes = ${JSON.stringify(allGeneratedRootPrefixes)};
|
|
259
285
|
export const generatedAssetAliases = ${JSON.stringify(
|
|
260
286
|
Array.from(GENERATED_ASSET_ALIASES),
|
|
261
287
|
)};
|
|
@@ -11,6 +11,7 @@ import { normalize, resolve, sep } from 'path';
|
|
|
11
11
|
import { getPlatformAdapter, normalizePlatformName } from './platforms.js';
|
|
12
12
|
import { resolveProjectStructure } from './project-structure.js';
|
|
13
13
|
import { safeExists, safeReadJson } from './utils/fs-safe.js';
|
|
14
|
+
import { unique } from './utils/unique.js';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Cache normalized project config by project root and relevant env signature.
|
|
@@ -94,6 +95,65 @@ function normalizeStructureImplementations(projectDir, implementations = []) {
|
|
|
94
95
|
.filter(Boolean);
|
|
95
96
|
}
|
|
96
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Read public asset root configuration from supported project config paths.
|
|
100
|
+
*
|
|
101
|
+
* `assets.roots` is the documented public path.
|
|
102
|
+
* `projectStructure.assetRoots` and `project.assetRoots` remain supported for
|
|
103
|
+
* compatibility with earlier follow-up work and local project experiments.
|
|
104
|
+
*
|
|
105
|
+
* @param {object} rawConfig - Parsed project.emulsify.json contents.
|
|
106
|
+
* @returns {Array} Raw configured asset roots.
|
|
107
|
+
*/
|
|
108
|
+
function rawAssetRoots(rawConfig = {}) {
|
|
109
|
+
return [
|
|
110
|
+
...(Array.isArray(rawConfig?.assets?.roots) ? rawConfig.assets.roots : []),
|
|
111
|
+
...(Array.isArray(rawConfig?.projectStructure?.assetRoots)
|
|
112
|
+
? rawConfig.projectStructure.assetRoots
|
|
113
|
+
: []),
|
|
114
|
+
...(Array.isArray(rawConfig?.project?.assetRoots)
|
|
115
|
+
? rawConfig.project.assetRoots
|
|
116
|
+
: []),
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Normalize public asset root declarations.
|
|
122
|
+
*
|
|
123
|
+
* Paths must resolve inside the project root. Invalid entries are retained as
|
|
124
|
+
* diagnostics so the audit command can report them without making Storybook or
|
|
125
|
+
* Vite builds fail for existing projects.
|
|
126
|
+
*
|
|
127
|
+
* @param {string} projectDir - Absolute project root.
|
|
128
|
+
* @param {Array} roots - Raw configured asset root entries.
|
|
129
|
+
* @returns {{roots: string[], ignored: string[]}} Normalized asset root state.
|
|
130
|
+
*/
|
|
131
|
+
function normalizeAssetRoots(projectDir, roots = []) {
|
|
132
|
+
if (!Array.isArray(roots)) return { roots: [], ignored: [] };
|
|
133
|
+
|
|
134
|
+
const normalizedRoots = [];
|
|
135
|
+
const ignored = [];
|
|
136
|
+
|
|
137
|
+
for (const item of roots) {
|
|
138
|
+
if (typeof item !== 'string' || !item.trim()) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const directory = coerceToProjectPath(projectDir, item);
|
|
143
|
+
if (!directory) {
|
|
144
|
+
ignored.push(item);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
normalizedRoots.push(normalize(directory));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
roots: unique(normalizedRoots),
|
|
153
|
+
ignored: unique(ignored),
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
97
157
|
/**
|
|
98
158
|
* Normalize project config for current tooling consumers.
|
|
99
159
|
*
|
|
@@ -140,6 +200,7 @@ export function resolveProjectConfig(
|
|
|
140
200
|
root,
|
|
141
201
|
rawStructureImplementations,
|
|
142
202
|
);
|
|
203
|
+
const assetRoots = normalizeAssetRoots(root, rawAssetRoots(rawConfig));
|
|
143
204
|
const structureRoots = structureImplementations.map(
|
|
144
205
|
(implementation) => implementation.directory,
|
|
145
206
|
);
|
|
@@ -149,6 +210,8 @@ export function resolveProjectConfig(
|
|
|
149
210
|
srcExists,
|
|
150
211
|
SDC: singleDirectoryComponents,
|
|
151
212
|
structureImplementations,
|
|
213
|
+
assetRoots: assetRoots.roots,
|
|
214
|
+
ignoredAssetRoots: assetRoots.ignored,
|
|
152
215
|
platformAdapter,
|
|
153
216
|
});
|
|
154
217
|
|
|
@@ -166,6 +229,8 @@ export function resolveProjectConfig(
|
|
|
166
229
|
structureOverrides: projectStructure.structureOverrides,
|
|
167
230
|
structureImplementations,
|
|
168
231
|
structureRoots,
|
|
232
|
+
assetRoots: projectStructure.assetRoots,
|
|
233
|
+
ignoredAssetRoots: projectStructure.ignoredAssetRoots,
|
|
169
234
|
componentRoots: projectStructure.componentRoots,
|
|
170
235
|
globalRoots: projectStructure.globalRoots,
|
|
171
236
|
namespaceRoots: projectStructure.namespaceRoots,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* process.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { basename, relative, resolve, sep } from 'path';
|
|
10
|
+
import { basename, normalize, relative, resolve, sep } from 'path';
|
|
11
11
|
import { safeExists } from './utils/fs-safe.js';
|
|
12
12
|
import { replaceLastSlash, toPosixPath } from './utils/paths.js';
|
|
13
13
|
import { unique } from './utils/unique.js';
|
|
@@ -181,6 +181,33 @@ function fallbackNamespaceRoots({
|
|
|
181
181
|
return namespaceRoots;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
/**
|
|
185
|
+
* Normalize project-scoped asset roots.
|
|
186
|
+
*
|
|
187
|
+
* @param {string} projectDir - Absolute project root.
|
|
188
|
+
* @param {string[]} assetRoots - Raw asset root paths.
|
|
189
|
+
* @returns {string[]} Safe absolute asset root paths.
|
|
190
|
+
*/
|
|
191
|
+
function normalizeAssetRoots(projectDir, assetRoots = []) {
|
|
192
|
+
if (!Array.isArray(assetRoots)) return [];
|
|
193
|
+
|
|
194
|
+
const projectRoot = resolve(projectDir);
|
|
195
|
+
|
|
196
|
+
return unique(
|
|
197
|
+
assetRoots
|
|
198
|
+
.map((root) =>
|
|
199
|
+
typeof root === 'string' && root.trim()
|
|
200
|
+
? normalize(resolve(projectRoot, root))
|
|
201
|
+
: '',
|
|
202
|
+
)
|
|
203
|
+
.filter(
|
|
204
|
+
(root) =>
|
|
205
|
+
root &&
|
|
206
|
+
(root === projectRoot || root.startsWith(`${projectRoot}${sep}`)),
|
|
207
|
+
),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
184
211
|
/**
|
|
185
212
|
* Resolve the serializable project structure model.
|
|
186
213
|
*
|
|
@@ -190,6 +217,8 @@ function fallbackNamespaceRoots({
|
|
|
190
217
|
* srcExists?: boolean,
|
|
191
218
|
* SDC?: boolean,
|
|
192
219
|
* structureImplementations?: {name: string, directory: string}[],
|
|
220
|
+
* assetRoots?: string[],
|
|
221
|
+
* ignoredAssetRoots?: string[],
|
|
193
222
|
* platformAdapter?: object
|
|
194
223
|
* }} [env] - Normalized project environment.
|
|
195
224
|
* @returns {object} Project structure model.
|
|
@@ -214,6 +243,8 @@ export function resolveProjectStructure(env) {
|
|
|
214
243
|
srcDir = defaultSrcDir,
|
|
215
244
|
srcExists = safeExists(defaultSrcDir),
|
|
216
245
|
SDC = false,
|
|
246
|
+
assetRoots: rawAssetRoots = [],
|
|
247
|
+
ignoredAssetRoots = [],
|
|
217
248
|
platformAdapter = {},
|
|
218
249
|
} = resolvedEnv;
|
|
219
250
|
const structureImplementations =
|
|
@@ -244,6 +275,7 @@ export function resolveProjectStructure(env) {
|
|
|
244
275
|
});
|
|
245
276
|
const componentRoots = componentRootRecords.map((root) => root.directory);
|
|
246
277
|
const globalRoots = globalRootRecords.map((root) => root.directory);
|
|
278
|
+
const assetRoots = normalizeAssetRoots(projectDir, rawAssetRoots);
|
|
247
279
|
const namespaceRootValues = Object.values(namespaceRoots);
|
|
248
280
|
const sourceRoots = unique(
|
|
249
281
|
[...componentRoots, ...globalRoots].filter(Boolean),
|
|
@@ -278,7 +310,9 @@ export function resolveProjectStructure(env) {
|
|
|
278
310
|
globalRootRecords,
|
|
279
311
|
componentRoots,
|
|
280
312
|
globalRoots,
|
|
313
|
+
assetRoots,
|
|
281
314
|
sourceRoots,
|
|
315
|
+
ignoredAssetRoots: unique(ignoredAssetRoots),
|
|
282
316
|
sourceRootRecords,
|
|
283
317
|
storyRoots,
|
|
284
318
|
twigRoots,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@emulsify/core",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.1.1",
|
|
4
4
|
"description": "Bundled tooling for Storybook development + Vite Build",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"component library",
|
|
@@ -38,7 +38,10 @@
|
|
|
38
38
|
".storybook/css-components.js",
|
|
39
39
|
".storybook/css-dist.js",
|
|
40
40
|
".storybook/emulsifyTheme.js",
|
|
41
|
+
".storybook/main-static-assets.js",
|
|
42
|
+
".storybook/main-vite.js",
|
|
41
43
|
".storybook/main.js",
|
|
44
|
+
".storybook/manager-head.css",
|
|
42
45
|
".storybook/manager.js",
|
|
43
46
|
".storybook/preview.js",
|
|
44
47
|
".storybook/utils.js",
|
|
@@ -98,6 +101,7 @@
|
|
|
98
101
|
"src/storybook/index.js",
|
|
99
102
|
"src/storybook/main-config.js",
|
|
100
103
|
"src/storybook/platform-behaviors.js",
|
|
104
|
+
"src/storybook/preview-decorator.js",
|
|
101
105
|
"src/storybook/preview-parameters.js",
|
|
102
106
|
"src/storybook/render-twig.js",
|
|
103
107
|
"src/storybook/twig/include-function.js",
|
package/scripts/a11y.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* and reports issues.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import { existsSync } from 'fs';
|
|
8
|
+
import { existsSync, readFileSync } from 'fs';
|
|
9
9
|
import path from 'path';
|
|
10
10
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
11
11
|
import * as R from 'ramda';
|
|
@@ -16,15 +16,15 @@ import a11yConfig from '../config/a11y.config.js';
|
|
|
16
16
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
17
|
const __dirname = path.dirname(__filename);
|
|
18
18
|
|
|
19
|
-
const { storybookBuildDir, pa11y: pa11yConfig } = a11yConfig;
|
|
20
|
-
|
|
21
19
|
// Project-specific configuration.
|
|
22
|
-
let {
|
|
20
|
+
let {
|
|
21
|
+
ignore = {},
|
|
22
|
+
components = [],
|
|
23
|
+
discoverStories = true,
|
|
24
|
+
storybookBuildDir,
|
|
25
|
+
pa11y: pa11yConfig = {},
|
|
26
|
+
} = a11yConfig;
|
|
23
27
|
|
|
24
|
-
/** Absolute path to Storybook build directory. */
|
|
25
|
-
const STORYBOOK_BUILD_DIR = path.resolve(__dirname, '../', storybookBuildDir);
|
|
26
|
-
/** Absolute path to Storybook iframe file used for per-story rendering. */
|
|
27
|
-
const STORYBOOK_IFRAME = path.join(STORYBOOK_BUILD_DIR, 'iframe.html');
|
|
28
28
|
/** Project-specific accessibility config path used by generated themes. */
|
|
29
29
|
const PROJECT_A11Y_CONFIG = path.resolve(
|
|
30
30
|
__dirname,
|
|
@@ -48,12 +48,26 @@ const loadProjectA11yConfig = async () => {
|
|
|
48
48
|
/**
|
|
49
49
|
* Apply project-specific a11y config values over shared defaults.
|
|
50
50
|
*
|
|
51
|
-
* @param {{ignore?: object, components?: string[]}} config - Project config.
|
|
51
|
+
* @param {{ignore?: object, components?: string[], discoverStories?: boolean, storybookBuildDir?: string, pa11y?: object}} config - Project config.
|
|
52
52
|
* @returns {void}
|
|
53
53
|
*/
|
|
54
54
|
const applyProjectA11yConfig = (config = {}) => {
|
|
55
55
|
ignore = config.ignore || ignore;
|
|
56
|
-
components = config.components
|
|
56
|
+
components = Array.isArray(config.components)
|
|
57
|
+
? config.components
|
|
58
|
+
: components;
|
|
59
|
+
discoverStories =
|
|
60
|
+
typeof config.discoverStories === 'boolean'
|
|
61
|
+
? config.discoverStories
|
|
62
|
+
: discoverStories;
|
|
63
|
+
storybookBuildDir =
|
|
64
|
+
typeof config.storybookBuildDir === 'string' && config.storybookBuildDir
|
|
65
|
+
? config.storybookBuildDir
|
|
66
|
+
: storybookBuildDir;
|
|
67
|
+
pa11yConfig =
|
|
68
|
+
config.pa11y && typeof config.pa11y === 'object'
|
|
69
|
+
? { ...pa11yConfig, ...config.pa11y }
|
|
70
|
+
: pa11yConfig;
|
|
57
71
|
};
|
|
58
72
|
|
|
59
73
|
/**
|
|
@@ -67,12 +81,128 @@ const printHelp = () => {
|
|
|
67
81
|
'Usage: node scripts/a11y.js [options]',
|
|
68
82
|
'',
|
|
69
83
|
'Options:',
|
|
70
|
-
' -r Run pa11y against configured Storybook
|
|
84
|
+
' -r Run pa11y against discovered and configured Storybook story IDs.',
|
|
71
85
|
' -h, --help Print this help text.',
|
|
72
86
|
].join('\n'),
|
|
73
87
|
);
|
|
74
88
|
};
|
|
75
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Resolve the configured Storybook build directory.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} [buildDir=storybookBuildDir] - Configured build directory.
|
|
94
|
+
* @returns {string} Absolute Storybook build directory.
|
|
95
|
+
*/
|
|
96
|
+
const resolveStorybookBuildDir = (buildDir = storybookBuildDir) =>
|
|
97
|
+
path.resolve(__dirname, '../', buildDir);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve Storybook's iframe file used for per-story rendering.
|
|
101
|
+
*
|
|
102
|
+
* @param {string} [buildDir=storybookBuildDir] - Configured build directory.
|
|
103
|
+
* @returns {string} Absolute iframe.html path.
|
|
104
|
+
*/
|
|
105
|
+
const resolveStorybookIframe = (buildDir = storybookBuildDir) =>
|
|
106
|
+
path.join(resolveStorybookBuildDir(buildDir), 'iframe.html');
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Return unique non-empty Storybook IDs in first-seen order.
|
|
110
|
+
*
|
|
111
|
+
* @param {Array} values - Candidate story IDs.
|
|
112
|
+
* @returns {string[]} Unique story IDs.
|
|
113
|
+
*/
|
|
114
|
+
const normalizeStoryIds = (values = []) =>
|
|
115
|
+
Array.from(
|
|
116
|
+
new Set(
|
|
117
|
+
values
|
|
118
|
+
.filter((value) => typeof value === 'string')
|
|
119
|
+
.map((value) => value.trim())
|
|
120
|
+
.filter(Boolean),
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract runnable story IDs from a Storybook index object.
|
|
126
|
+
*
|
|
127
|
+
* @param {object} index - Parsed Storybook index.json or stories.json.
|
|
128
|
+
* @returns {string[]} Story IDs.
|
|
129
|
+
*/
|
|
130
|
+
const storyIdsFromStorybookIndex = (index = {}) => {
|
|
131
|
+
const entries =
|
|
132
|
+
index?.entries && typeof index.entries === 'object'
|
|
133
|
+
? index.entries
|
|
134
|
+
: index?.stories && typeof index.stories === 'object'
|
|
135
|
+
? index.stories
|
|
136
|
+
: {};
|
|
137
|
+
|
|
138
|
+
return normalizeStoryIds(
|
|
139
|
+
Object.entries(entries)
|
|
140
|
+
.filter(([, entry]) => !entry?.type || entry.type === 'story')
|
|
141
|
+
.map(([id, entry]) =>
|
|
142
|
+
typeof entry?.id === 'string' && entry.id ? entry.id : id,
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Discover runnable Storybook story IDs from built Storybook output.
|
|
149
|
+
*
|
|
150
|
+
* @param {string} [buildDir=storybookBuildDir] - Configured Storybook build directory.
|
|
151
|
+
* @param {{warn?: Function}} [options] - Reporting options.
|
|
152
|
+
* @returns {string[]} Story IDs discovered from Storybook's generated index.
|
|
153
|
+
*/
|
|
154
|
+
const discoverStoryIds = (
|
|
155
|
+
buildDir = storybookBuildDir,
|
|
156
|
+
{ warn = console.warn } = {},
|
|
157
|
+
) => {
|
|
158
|
+
const indexPath = path.join(resolveStorybookBuildDir(buildDir), 'index.json');
|
|
159
|
+
|
|
160
|
+
if (!existsSync(indexPath)) {
|
|
161
|
+
warn(
|
|
162
|
+
`Storybook index not found at ${indexPath}; falling back to configured Pa11y story IDs.`,
|
|
163
|
+
);
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
return storyIdsFromStorybookIndex(
|
|
169
|
+
JSON.parse(readFileSync(indexPath, 'utf8')),
|
|
170
|
+
);
|
|
171
|
+
} catch (error) {
|
|
172
|
+
warn(
|
|
173
|
+
`Unable to read Storybook index at ${indexPath}: ${
|
|
174
|
+
error.message || error
|
|
175
|
+
}; falling back to configured Pa11y story IDs.`,
|
|
176
|
+
);
|
|
177
|
+
return [];
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Resolve the final Pa11y story ID list.
|
|
183
|
+
*
|
|
184
|
+
* @param {object} [options={}] - Resolution options.
|
|
185
|
+
* @param {string[]} [options.manualIds=components] - Manually configured IDs.
|
|
186
|
+
* @param {boolean} [options.discover=discoverStories] - Whether discovery is enabled.
|
|
187
|
+
* @param {string} [options.buildDir=storybookBuildDir] - Storybook build directory.
|
|
188
|
+
* @param {Function} [options.warn=console.warn] - Warning sink.
|
|
189
|
+
* @returns {string[]} Story IDs to lint.
|
|
190
|
+
*/
|
|
191
|
+
const resolvePa11yStoryIds = ({
|
|
192
|
+
manualIds = components,
|
|
193
|
+
discover = discoverStories,
|
|
194
|
+
buildDir = storybookBuildDir,
|
|
195
|
+
warn = console.warn,
|
|
196
|
+
} = {}) => {
|
|
197
|
+
const manual = normalizeStoryIds(manualIds);
|
|
198
|
+
if (discover === false) return manual;
|
|
199
|
+
|
|
200
|
+
return normalizeStoryIds([
|
|
201
|
+
...manual,
|
|
202
|
+
...discoverStoryIds(buildDir, { warn }),
|
|
203
|
+
]);
|
|
204
|
+
};
|
|
205
|
+
|
|
76
206
|
/**
|
|
77
207
|
* Map pa11y/axe severity to a label (historically a color name).
|
|
78
208
|
* Retained for backward compatibility, but not used for styling anymore.
|
|
@@ -155,7 +285,7 @@ const logReport = ({ issues, pageUrl }) => {
|
|
|
155
285
|
* @returns {Promise<{ issues: Pa11yIssue[], pageUrl: string }>} Pa11y result.
|
|
156
286
|
*/
|
|
157
287
|
const lintComponent = async (name) =>
|
|
158
|
-
pa11y(`${
|
|
288
|
+
pa11y(`${resolveStorybookIframe()}?id=${name}`, {
|
|
159
289
|
includeNotices: true,
|
|
160
290
|
includeWarnings: true,
|
|
161
291
|
runners: ['axe'],
|
|
@@ -188,15 +318,22 @@ if (R.includes(process.argv[2], ['-h', '--help'])) {
|
|
|
188
318
|
} else if (R.pathEq(['argv', 2], '-r')(process)) {
|
|
189
319
|
loadProjectA11yConfig().then((projectConfig) => {
|
|
190
320
|
applyProjectA11yConfig(projectConfig);
|
|
191
|
-
return lintReportAndExit(
|
|
321
|
+
return lintReportAndExit(resolvePa11yStoryIds());
|
|
192
322
|
});
|
|
193
323
|
}
|
|
194
324
|
|
|
195
325
|
export {
|
|
196
326
|
severityToColor,
|
|
327
|
+
applyProjectA11yConfig,
|
|
328
|
+
discoverStoryIds,
|
|
197
329
|
issueIsValid,
|
|
198
330
|
logIssue,
|
|
199
331
|
logReport,
|
|
200
332
|
lintComponent,
|
|
201
333
|
lintReportAndExit,
|
|
334
|
+
normalizeStoryIds,
|
|
335
|
+
resolvePa11yStoryIds,
|
|
336
|
+
resolveStorybookBuildDir,
|
|
337
|
+
resolveStorybookIframe,
|
|
338
|
+
storyIdsFromStorybookIndex,
|
|
202
339
|
};
|
package/scripts/audit.js
CHANGED
|
@@ -396,6 +396,18 @@ function auditProjectConfig(context) {
|
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
for (const root of env.ignoredAssetRoots || []) {
|
|
400
|
+
findings.push(
|
|
401
|
+
makeFinding({
|
|
402
|
+
id: 'invalid-asset-root',
|
|
403
|
+
severity: 'warn',
|
|
404
|
+
filePath: resolve(projectDir, 'project.emulsify.json'),
|
|
405
|
+
message: `Configured asset root "${root}" was ignored because it resolves outside the project root.`,
|
|
406
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/project-structure.md#asset-roots',
|
|
407
|
+
}),
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
399
411
|
return findings;
|
|
400
412
|
}
|
|
401
413
|
|
|
@@ -1046,7 +1058,7 @@ function styleRuntimeDirectories(filePath, env, projectDir) {
|
|
|
1046
1058
|
function auditCssAssetReferences(context) {
|
|
1047
1059
|
const { env, projectDir, styleFiles } = context;
|
|
1048
1060
|
const findings = [];
|
|
1049
|
-
const
|
|
1061
|
+
const projectAssetRoots = auditAssetRoots(env).filter(safeIsDirectory);
|
|
1050
1062
|
const styleSourceRoots = env.projectStructure?.sourceRoots || [];
|
|
1051
1063
|
|
|
1052
1064
|
for (const filePath of styleFiles) {
|
|
@@ -1092,7 +1104,7 @@ function auditCssAssetReferences(context) {
|
|
|
1092
1104
|
}
|
|
1093
1105
|
|
|
1094
1106
|
if (
|
|
1095
|
-
isSameOrInside(resolvedAsset,
|
|
1107
|
+
projectAssetRoots.some((root) => isSameOrInside(resolvedAsset, root)) &&
|
|
1096
1108
|
(!sourceAsset || runtimeAsset || assetPath.startsWith('..'))
|
|
1097
1109
|
) {
|
|
1098
1110
|
findings.push(
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Shared helpers for Storybook preview decorator wiring.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import {
|
|
7
|
+
renderHtmlStoryResult,
|
|
8
|
+
withLegacyStoryToString,
|
|
9
|
+
} from './render-twig.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Render a Storybook story function as a React element with full context props.
|
|
13
|
+
*
|
|
14
|
+
* @param {Function} storyFn - Storybook story function.
|
|
15
|
+
* @param {object} context - Full Storybook story context.
|
|
16
|
+
* @returns {React.ReactElement} React story element.
|
|
17
|
+
*/
|
|
18
|
+
export function createStoryElement(storyFn, context) {
|
|
19
|
+
return withLegacyStoryToString(React.createElement(storyFn, context), () =>
|
|
20
|
+
storyFn(context),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Apply Storybook decorators while preserving full story context.
|
|
26
|
+
*
|
|
27
|
+
* @param {Function} decorateStory - Storybook decorator applier.
|
|
28
|
+
* @param {Function} storyFn - Storybook story function.
|
|
29
|
+
* @param {Function[]} decorators - Storybook decorators.
|
|
30
|
+
* @returns {Function} Decorated story function.
|
|
31
|
+
*/
|
|
32
|
+
export const applyStoryDecorators = (decorateStory, storyFn, decorators) =>
|
|
33
|
+
decorateStory((context) => createStoryElement(storyFn, context), decorators);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Render a preview Story callback through Emulsify's HTML compatibility layer.
|
|
37
|
+
*
|
|
38
|
+
* @param {Function} Story - Decorated Storybook story callback.
|
|
39
|
+
* @param {object} context - Full Storybook story context.
|
|
40
|
+
* @param {object} [options={}] - Render options.
|
|
41
|
+
* @returns {*} Rendered Storybook story result.
|
|
42
|
+
*/
|
|
43
|
+
export function renderPreviewStory(Story, context, options = {}) {
|
|
44
|
+
return renderHtmlStoryResult(Story(context), options);
|
|
45
|
+
}
|