@emulsify/core 4.0.4 → 4.1.0
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/main.js +50 -19
- package/.storybook/preview.js +5 -12
- package/README.md +11 -3
- package/config/a11y.config.js +2 -1
- package/config/vite/environment.js +2 -0
- package/config/vite/project-config.js +65 -0
- package/config/vite/project-structure.js +35 -1
- package/package.json +2 -1
- package/scripts/a11y.js +150 -13
- package/scripts/audit.js +14 -2
- package/src/storybook/preview-decorator.js +45 -0
package/.storybook/main.js
CHANGED
|
@@ -120,12 +120,58 @@ function readOptionalHtmlFragment(relativePath) {
|
|
|
120
120
|
* @returns {Array<string|{from: string, to: string}>} Existing static directory entries.
|
|
121
121
|
*/
|
|
122
122
|
function existingStaticDirs(staticDirs) {
|
|
123
|
-
|
|
123
|
+
const seen = new Set();
|
|
124
|
+
const existing = [];
|
|
125
|
+
|
|
126
|
+
for (const staticDir of staticDirs) {
|
|
124
127
|
const directory =
|
|
125
128
|
typeof staticDir === 'string' ? staticDir : staticDir.from;
|
|
126
129
|
|
|
127
|
-
|
|
128
|
-
|
|
130
|
+
if (!directory || !fs.existsSync(directory)) continue;
|
|
131
|
+
|
|
132
|
+
const key =
|
|
133
|
+
typeof staticDir === 'string'
|
|
134
|
+
? staticDir
|
|
135
|
+
: `${staticDir.from || ''}\0${staticDir.to || ''}`;
|
|
136
|
+
if (seen.has(key)) continue;
|
|
137
|
+
|
|
138
|
+
seen.add(key);
|
|
139
|
+
existing.push(staticDir);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return existing;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Build static directory mounts for normalized project asset roots.
|
|
147
|
+
*
|
|
148
|
+
* @param {StorybookEnvironment} env - Resolved project paths used by Storybook.
|
|
149
|
+
* @returns {Array<string|{from: string, to: string}>} Static directory entries.
|
|
150
|
+
*/
|
|
151
|
+
function buildAssetStaticDirs(env) {
|
|
152
|
+
const configuredAssetRoots = Array.isArray(env.projectStructure?.assetRoots)
|
|
153
|
+
? env.projectStructure.assetRoots
|
|
154
|
+
: [];
|
|
155
|
+
const assetRoots = [
|
|
156
|
+
...configuredAssetRoots,
|
|
157
|
+
path.resolve(projectRoot, 'assets'),
|
|
158
|
+
path.resolve(projectRoot, 'src/assets'),
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
return existingStaticDirs([
|
|
162
|
+
...assetRoots.map((root) => ({
|
|
163
|
+
from: root,
|
|
164
|
+
to: '/assets',
|
|
165
|
+
})),
|
|
166
|
+
{
|
|
167
|
+
from: path.resolve(projectRoot, 'dist/assets'),
|
|
168
|
+
to: '/assets',
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
from: path.resolve(projectRoot, 'dist'),
|
|
172
|
+
to: '/dist',
|
|
173
|
+
},
|
|
174
|
+
]);
|
|
129
175
|
}
|
|
130
176
|
|
|
131
177
|
/**
|
|
@@ -265,22 +311,7 @@ const baseConfig = {
|
|
|
265
311
|
*
|
|
266
312
|
* @type {Array<string|{from: string, to: string}>}
|
|
267
313
|
*/
|
|
268
|
-
staticDirs:
|
|
269
|
-
...existingStaticDirs([
|
|
270
|
-
{
|
|
271
|
-
from: path.resolve(projectRoot, 'assets'),
|
|
272
|
-
to: '/assets',
|
|
273
|
-
},
|
|
274
|
-
{
|
|
275
|
-
from: path.resolve(projectRoot, 'dist/assets'),
|
|
276
|
-
to: '/assets',
|
|
277
|
-
},
|
|
278
|
-
{
|
|
279
|
-
from: path.resolve(projectRoot, 'dist'),
|
|
280
|
-
to: '/dist',
|
|
281
|
-
},
|
|
282
|
-
]),
|
|
283
|
-
],
|
|
314
|
+
staticDirs: buildAssetStaticDirs(resolvedStorybookEnv),
|
|
284
315
|
|
|
285
316
|
/**
|
|
286
317
|
* Enable the default addon set used by Emulsify.
|
package/.storybook/preview.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { getRules } from 'axe-core';
|
|
6
|
-
import React from 'react';
|
|
7
6
|
import { defaultDecorateStory, useEffect } from 'storybook/preview-api';
|
|
8
7
|
import Twig from 'twig';
|
|
9
8
|
import { twigExtensionInstallers } from 'virtual:emulsify-twig-extension-installers';
|
|
@@ -12,9 +11,9 @@ import {
|
|
|
12
11
|
normalizePreviewOverrideModule,
|
|
13
12
|
} from '../src/storybook/preview-parameters.js';
|
|
14
13
|
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from '../src/storybook/
|
|
14
|
+
applyStoryDecorators,
|
|
15
|
+
renderPreviewStory,
|
|
16
|
+
} from '../src/storybook/preview-decorator.js';
|
|
18
17
|
import {
|
|
19
18
|
attachStorybookBehaviors,
|
|
20
19
|
fetchCSSFiles,
|
|
@@ -93,13 +92,7 @@ const AxeRules = enableRulesByTag([
|
|
|
93
92
|
* @returns {Function} Decorated story function.
|
|
94
93
|
*/
|
|
95
94
|
export const applyDecorators = (storyFn, decorators) =>
|
|
96
|
-
defaultDecorateStory
|
|
97
|
-
(context) =>
|
|
98
|
-
withLegacyStoryToString(React.createElement(storyFn, context), () =>
|
|
99
|
-
storyFn(context),
|
|
100
|
-
),
|
|
101
|
-
decorators,
|
|
102
|
-
);
|
|
95
|
+
applyStoryDecorators(defaultDecorateStory, storyFn, decorators);
|
|
103
96
|
|
|
104
97
|
/**
|
|
105
98
|
* Storybook decorators to apply platform-specific behavior after each story render.
|
|
@@ -125,7 +118,7 @@ export const decorators = [
|
|
|
125
118
|
});
|
|
126
119
|
}, [args]);
|
|
127
120
|
|
|
128
|
-
return
|
|
121
|
+
return renderPreviewStory(Story, context, {
|
|
129
122
|
platformAdapter,
|
|
130
123
|
});
|
|
131
124
|
},
|
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ See [Version Evolution](docs/version-evolution.md) for more release history.
|
|
|
34
34
|
|
|
35
35
|
Twig and React are equally valid ways to build component libraries with Emulsify Core. The right authoring model depends on the consuming project:
|
|
36
36
|
|
|
37
|
-
- Use Twig for CMS themes and server-rendered template systems. Drupal has a dedicated adapter today
|
|
37
|
+
- Use Twig for CMS themes and server-rendered template systems. Drupal has a dedicated adapter today. WordPress and Timber projects should currently use `platform: "none"` unless a project adds its own platform-specific behavior.
|
|
38
38
|
- Use React for standalone UI libraries, application components, or projects that already use React.
|
|
39
39
|
- Use mixed Twig and React when a design system needs to document both CMS-rendered and JavaScript-rendered components in the same Storybook instance.
|
|
40
40
|
|
|
@@ -56,10 +56,18 @@ Every project should provide a `project.emulsify.json` file at the project root:
|
|
|
56
56
|
"platform": "none",
|
|
57
57
|
"name": "example",
|
|
58
58
|
"machineName": "example"
|
|
59
|
+
},
|
|
60
|
+
"assets": {
|
|
61
|
+
"roots": ["./design/assets"]
|
|
59
62
|
}
|
|
60
63
|
}
|
|
61
64
|
```
|
|
62
65
|
|
|
66
|
+
Asset files are discovered from the default asset roots and any additional
|
|
67
|
+
roots configured in `project.emulsify.json`. Use asset roots when a project
|
|
68
|
+
stores fonts, images, icons, or other static files outside the default
|
|
69
|
+
locations.
|
|
70
|
+
|
|
63
71
|
Common project scripts call the shared Emulsify Core Vite and Storybook config:
|
|
64
72
|
|
|
65
73
|
- `storybook`: starts Storybook development.
|
|
@@ -86,7 +94,7 @@ The documentation is split by task:
|
|
|
86
94
|
|
|
87
95
|
## Known Limitations
|
|
88
96
|
|
|
89
|
-
- Implemented platform adapters are currently `none` and `drupal`. WordPress
|
|
97
|
+
- Implemented platform adapters are currently `none` and `drupal`. WordPress and Timber projects should currently use `platform: "none"`. This keeps Emulsify Core in platform-neutral mode while still supporting Twig-oriented component development. A dedicated WordPress adapter may be added later when WordPress-specific behavior is introduced. See [Platform Adapters](docs/platform-adapters.md).
|
|
90
98
|
- Storybook's Twig resolver eagerly imports Twig modules and raw Twig source. This is reliable for `include()` and `source()`, but large Twig libraries should keep Storybook source roots intentional. See [Performance](docs/performance.md).
|
|
91
99
|
- Production sourcemaps are enabled by default unless a project overrides Vite config through `config/emulsify-core/vite/plugins.*`. See [Performance](docs/performance.md).
|
|
92
100
|
- Project extensions use the public `config/emulsify-core` directory: `config/emulsify-core/vite/plugins.*` for Vite, `config/emulsify-core/storybook/...` for Storybook, and `config/emulsify-core/a11y.config.js` for a11y. See [Extension Points](docs/extension-points.md).
|
|
@@ -103,7 +111,7 @@ Release-readiness coverage validates:
|
|
|
103
111
|
- Projects using multiple `variant.structureImplementations`.
|
|
104
112
|
- Mixed Twig + React Storybook projects.
|
|
105
113
|
|
|
106
|
-
WordPress
|
|
114
|
+
WordPress and Timber projects should currently use `platform: "none"`. This keeps Emulsify Core in platform-neutral mode while still supporting Twig-oriented component development. A dedicated WordPress adapter may be added later when WordPress-specific behavior is introduced. The implemented adapters in this package are currently `none` and `drupal`.
|
|
107
115
|
|
|
108
116
|
## Public Imports
|
|
109
117
|
|
package/config/a11y.config.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
export default {
|
|
9
9
|
storybookBuildDir: '../../../../.out',
|
|
10
|
+
discoverStories: true,
|
|
10
11
|
pa11y: {
|
|
11
12
|
includeNotices: false,
|
|
12
13
|
includeWarnings: false,
|
|
@@ -17,7 +18,7 @@ export default {
|
|
|
17
18
|
codes: ['landmark-one-main', 'page-has-heading-one'],
|
|
18
19
|
descriptions: ['Ensures all page content is contained by landmarks'],
|
|
19
20
|
},
|
|
20
|
-
//
|
|
21
|
+
// Manual Storybook IDs merged with IDs discovered from built Storybook output.
|
|
21
22
|
components: [
|
|
22
23
|
'base-colors--palettes',
|
|
23
24
|
'base-motion--usage',
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* - `SDC`: boolean from project.emulsify.json `project.singleDirectoryComponents`.
|
|
10
10
|
* - `structureOverrides`: true when safe `variant.structureImplementations` exist.
|
|
11
11
|
* - `structureRoots`: array of directories from `variant.structureImplementations`.
|
|
12
|
+
* - `assetRoots`: array of directories from safe `assets.roots` config.
|
|
12
13
|
* - `platformAdapter`: active adapter for platform-specific behavior.
|
|
13
14
|
*/
|
|
14
15
|
|
|
@@ -26,6 +27,7 @@ import { resolveProjectConfig } from './project-config.js';
|
|
|
26
27
|
* structureOverrides: boolean,
|
|
27
28
|
* structureRoots: string[],
|
|
28
29
|
* structureImplementations: Array<{name: string, directory: string}>,
|
|
30
|
+
* assetRoots: string[],
|
|
29
31
|
* componentRoots: string[],
|
|
30
32
|
* globalRoots: string[],
|
|
31
33
|
* namespaceRoots: Record<string, string>,
|
|
@@ -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.0
|
|
3
|
+
"version": "4.1.0",
|
|
4
4
|
"description": "Bundled tooling for Storybook development + Vite Build",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"component library",
|
|
@@ -98,6 +98,7 @@
|
|
|
98
98
|
"src/storybook/index.js",
|
|
99
99
|
"src/storybook/main-config.js",
|
|
100
100
|
"src/storybook/platform-behaviors.js",
|
|
101
|
+
"src/storybook/preview-decorator.js",
|
|
101
102
|
"src/storybook/preview-parameters.js",
|
|
102
103
|
"src/storybook/render-twig.js",
|
|
103
104
|
"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
|
+
}
|