@emulsify/core 4.3.2 → 4.5.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-static-assets.js +5 -8
- package/.storybook/main-vite.js +11 -3
- package/README.md +14 -6
- package/config/a11y-wcag22.js +11 -0
- package/config/vite/entries.js +7 -2
- package/config/vite/environment.js +4 -0
- package/config/vite/plugins/assets/asset-url-rebase.js +241 -0
- package/config/vite/plugins/assets/copy-src-assets.js +82 -12
- package/config/vite/plugins/assets/copy-twig-files.js +85 -16
- package/config/vite/plugins/assets/css-asset-rebase.js +306 -0
- package/config/vite/plugins/assets/css-asset-relativizer.js +301 -21
- package/config/vite/plugins/assets/development-source-maps.js +273 -0
- package/config/vite/plugins/assets/mirror-components.js +98 -82
- package/config/vite/plugins/assets/output-freshness.js +235 -0
- package/config/vite/plugins/assets/source-file-index.js +13 -13
- package/config/vite/plugins/assets/stable-watch-output.js +165 -0
- package/config/vite/plugins/assets/storybook-output.js +27 -0
- package/config/vite/plugins/index.js +95 -9
- package/config/vite/plugins/reporter/asset-resolver.js +34 -6
- package/config/vite/plugins/reporter/build-errors.js +7 -3
- package/config/vite/plugins/reporter/diagnostics.js +140 -10
- package/config/vite/plugins/reporter/index.js +380 -75
- package/config/vite/plugins/reporter/render.js +297 -44
- package/config/vite/plugins/reporter/sass-logger.js +30 -0
- package/config/vite/plugins/reporter/source-roots.js +101 -21
- package/config/vite/plugins/reporter/strict-mode.js +99 -0
- package/config/vite/plugins/reporter/vite-logger.js +220 -8
- package/config/vite/plugins/reporter/watch-mode.js +6 -2
- package/config/vite/plugins/twig/twig-module.js +35 -258
- package/config/vite/plugins/twig/virtual-twig-asset-sources.js +48 -49
- package/config/vite/project-config.js +121 -21
- package/config/vite/project-structure.js +6 -0
- package/config/vite/utils/asset-roots.js +205 -0
- package/config/vite/utils/css-urls.js +350 -0
- package/config/vite/utils/fs-safe.js +38 -1
- package/config/vite/utils/source-directory-skips.js +13 -0
- package/config/vite/utils/source-maps.js +88 -0
- package/config/vite/utils/twig-component-resolver.js +316 -0
- package/config/vite/vite.config.js +106 -42
- package/package.json +54 -40
- package/scripts/a11y.js +88 -9
- package/scripts/audit/checks/css-asset-references.js +256 -24
- package/scripts/audit/checks/twig-references.js +16 -5
- package/scripts/audit/fix.js +836 -0
- package/scripts/audit/index.js +10 -2
- package/scripts/audit/lib/css.js +41 -35
- package/scripts/audit/lib/story-ast.js +392 -0
- package/scripts/audit/lib/story-render-paths.js +600 -0
- package/scripts/audit/lib/story-selection.js +190 -0
- package/scripts/audit/lib/twig.js +372 -80
- package/scripts/audit/report.js +83 -5
- package/scripts/audit-twig-stories.js +73 -3
- package/scripts/audit.js +87 -2
- package/src/storybook/twig/source-function.js +14 -10
|
@@ -18,6 +18,12 @@ import {
|
|
|
18
18
|
formatAuditJsonErrorReport,
|
|
19
19
|
formatAuditJsonReport,
|
|
20
20
|
} from './audit/report.js';
|
|
21
|
+
import {
|
|
22
|
+
findRenderTwigBindings,
|
|
23
|
+
findTwigTemplateBindings,
|
|
24
|
+
parseStoryModule,
|
|
25
|
+
} from './audit/lib/story-ast.js';
|
|
26
|
+
import { classifyStoryRenderPaths } from './audit/lib/story-render-paths.js';
|
|
21
27
|
import { lineNumberAt } from './lib/text.js';
|
|
22
28
|
|
|
23
29
|
const STORY_GLOB = '**/*.stories.{js,jsx,ts,tsx}';
|
|
@@ -115,13 +121,13 @@ export function findDirectTemplateReturns(source, templateNames = []) {
|
|
|
115
121
|
}
|
|
116
122
|
|
|
117
123
|
/**
|
|
118
|
-
* Analyze one Storybook story
|
|
124
|
+
* Analyze one Storybook story with the legacy regex implementation.
|
|
119
125
|
*
|
|
120
126
|
* @param {string} source - Story source.
|
|
121
|
-
* @param {string}
|
|
127
|
+
* @param {string} filePath - Story file path.
|
|
122
128
|
* @returns {object} Story analysis.
|
|
123
129
|
*/
|
|
124
|
-
|
|
130
|
+
function analyzeStorySourceWithRegex(source, filePath) {
|
|
125
131
|
const twigImports = findTwigImports(source);
|
|
126
132
|
const templateNames = twigImports.map((item) => item.name);
|
|
127
133
|
const hasRenderTwig = importsRenderTwig(source);
|
|
@@ -160,6 +166,70 @@ export function analyzeStorySource(source, filePath = '') {
|
|
|
160
166
|
};
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Analyze one Storybook story source string.
|
|
171
|
+
*
|
|
172
|
+
* @param {string} source - Story source.
|
|
173
|
+
* @param {string} [filePath=''] - Story file path.
|
|
174
|
+
* @returns {object} Story analysis.
|
|
175
|
+
*/
|
|
176
|
+
export function analyzeStorySource(source, filePath = '') {
|
|
177
|
+
const parsed = parseStoryModule(source, filePath);
|
|
178
|
+
if (!parsed) return analyzeStorySourceWithRegex(source, filePath);
|
|
179
|
+
|
|
180
|
+
const twigImports = findTwigTemplateBindings(parsed.ast);
|
|
181
|
+
const renderTwigNames = findRenderTwigBindings(parsed.ast);
|
|
182
|
+
const hasRenderTwig = renderTwigNames.size > 0;
|
|
183
|
+
const reasons = [];
|
|
184
|
+
|
|
185
|
+
if (!twigImports.length) {
|
|
186
|
+
return {
|
|
187
|
+
filePath,
|
|
188
|
+
twigImports,
|
|
189
|
+
hasRenderTwig,
|
|
190
|
+
directTemplateReturns: [],
|
|
191
|
+
reasons,
|
|
192
|
+
shouldUpgrade: false,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const classification = classifyStoryRenderPaths(parsed.ast, {
|
|
197
|
+
templateNames: twigImports.map((item) => item.name),
|
|
198
|
+
renderTwigNames,
|
|
199
|
+
});
|
|
200
|
+
const fallbackLine = Number.isInteger(twigImports[0]?.line)
|
|
201
|
+
? twigImports[0].line
|
|
202
|
+
: 1;
|
|
203
|
+
const directTemplateReturns = classification.legacy.map((item) => ({
|
|
204
|
+
name: item.name || twigImports[0].name,
|
|
205
|
+
line: Number.isInteger(item.line) ? item.line : fallbackLine,
|
|
206
|
+
}));
|
|
207
|
+
|
|
208
|
+
if (!hasRenderTwig && classification.legacy.length) {
|
|
209
|
+
reasons.push('imports Twig templates without renderTwig()');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (classification.legacy.length) {
|
|
213
|
+
reasons.push('appears to return Twig HTML strings directly');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (reasons.length && !directTemplateReturns.length) {
|
|
217
|
+
directTemplateReturns.push({
|
|
218
|
+
name: twigImports[0].name,
|
|
219
|
+
line: fallbackLine,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
filePath,
|
|
225
|
+
twigImports,
|
|
226
|
+
hasRenderTwig,
|
|
227
|
+
directTemplateReturns,
|
|
228
|
+
reasons,
|
|
229
|
+
shouldUpgrade: reasons.length > 0,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
163
233
|
/**
|
|
164
234
|
* Resolve Storybook source roots for the project.
|
|
165
235
|
*
|
package/scripts/audit.js
CHANGED
|
@@ -11,11 +11,14 @@ import {
|
|
|
11
11
|
parseArgs as parseCliArgs,
|
|
12
12
|
} from './lib/cli.js';
|
|
13
13
|
import { DEFAULT_TWIG_THRESHOLD, runAudits } from './audit/index.js';
|
|
14
|
+
import { applyAuditFixes, remainingFindings } from './audit/fix.js';
|
|
14
15
|
import {
|
|
15
16
|
formatAuditJsonErrorReport,
|
|
16
17
|
formatAuditJsonReport,
|
|
17
18
|
formatAuditReport,
|
|
19
|
+
summarizeFindings,
|
|
18
20
|
} from './audit/report.js';
|
|
21
|
+
import { displayPath } from './audit/lib/findings.js';
|
|
19
22
|
|
|
20
23
|
export { auditProject, runAudits } from './audit/index.js';
|
|
21
24
|
export {
|
|
@@ -27,6 +30,7 @@ export {
|
|
|
27
30
|
formatAuditReport,
|
|
28
31
|
} from './audit/report.js';
|
|
29
32
|
export { collectProjectFiles } from './audit/lib/files.js';
|
|
33
|
+
export { applyAuditFixes, remainingFindings } from './audit/fix.js';
|
|
30
34
|
export { findCssUrlReferences } from './audit/lib/css.js';
|
|
31
35
|
export {
|
|
32
36
|
findTwigIncludeSourceReferences,
|
|
@@ -45,10 +49,12 @@ const cliFailureExitCode = 2;
|
|
|
45
49
|
*/
|
|
46
50
|
function usage() {
|
|
47
51
|
return createUsage(
|
|
48
|
-
'Usage: emulsify-audit [--root <dir>] [--json] [--fail-on <severity>] [--fail-on-found] [--twig-threshold <count>]',
|
|
52
|
+
'Usage: emulsify-audit [--root <dir>] [--json] [--fix] [--dry-run] [--fail-on <severity>] [--fail-on-found] [--twig-threshold <count>]',
|
|
49
53
|
[
|
|
50
54
|
' --root <dir> Project root to scan. Defaults to the current directory.',
|
|
51
55
|
' --json Print machine-readable JSON.',
|
|
56
|
+
' --fix Rewrite unambiguous CSS asset URLs to the canonical /assets/... form.',
|
|
57
|
+
' --dry-run With --fix, report the rewrites without touching files.',
|
|
52
58
|
' --fail-on <severity> Exit with code 1 for error, warn, info, or any findings at that threshold.',
|
|
53
59
|
' --fail-on-found Compatibility alias for --fail-on any.',
|
|
54
60
|
` --twig-threshold <count> Warn when Storybook roots contain more than this many Twig files. Default: ${DEFAULT_TWIG_THRESHOLD}.`,
|
|
@@ -70,6 +76,8 @@ function parseArgs(argv) {
|
|
|
70
76
|
failOn: null,
|
|
71
77
|
json: false,
|
|
72
78
|
help: false,
|
|
79
|
+
fix: false,
|
|
80
|
+
dryRun: false,
|
|
73
81
|
twigThreshold: DEFAULT_TWIG_THRESHOLD,
|
|
74
82
|
},
|
|
75
83
|
flags: {
|
|
@@ -78,6 +86,8 @@ function parseArgs(argv) {
|
|
|
78
86
|
value: 'any',
|
|
79
87
|
},
|
|
80
88
|
'--json': 'json',
|
|
89
|
+
'--fix': 'fix',
|
|
90
|
+
'--dry-run': 'dryRun',
|
|
81
91
|
},
|
|
82
92
|
options: {
|
|
83
93
|
'--fail-on': {
|
|
@@ -167,6 +177,9 @@ export function runCli(argv = process.argv.slice(2)) {
|
|
|
167
177
|
if (options.help && options.json) {
|
|
168
178
|
throw new Error('--json cannot be combined with --help.');
|
|
169
179
|
}
|
|
180
|
+
if (options.dryRun && !options.fix) {
|
|
181
|
+
throw new Error('--dry-run requires --fix.');
|
|
182
|
+
}
|
|
170
183
|
} catch (error) {
|
|
171
184
|
return reportArgumentFailure(error, jsonRequested);
|
|
172
185
|
}
|
|
@@ -178,6 +191,28 @@ export function runCli(argv = process.argv.slice(2)) {
|
|
|
178
191
|
|
|
179
192
|
try {
|
|
180
193
|
const result = runAudits(options);
|
|
194
|
+
let findings = result.findings;
|
|
195
|
+
|
|
196
|
+
if (options.fix) {
|
|
197
|
+
try {
|
|
198
|
+
result.fixes = applyAuditFixes(findings, {
|
|
199
|
+
dryRun: options.dryRun,
|
|
200
|
+
projectDir: result.projectDir,
|
|
201
|
+
sourceRoots: result.sourceRoots,
|
|
202
|
+
});
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return reportFixFailure(error, options);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// A dry run changes nothing on disk, so nothing is subtracted. A real
|
|
208
|
+
// run removed the findings it fixed from the source, so the threshold
|
|
209
|
+
// must be judged on what is left.
|
|
210
|
+
if (!options.dryRun) {
|
|
211
|
+
result.findings = remainingFindings(findings, result.fixes.applied);
|
|
212
|
+
result.summary = summarizeFindings(result.findings);
|
|
213
|
+
findings = result.findings;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
181
216
|
|
|
182
217
|
if (options.json) {
|
|
183
218
|
console.log(formatAuditJsonReport(result));
|
|
@@ -185,7 +220,7 @@ export function runCli(argv = process.argv.slice(2)) {
|
|
|
185
220
|
console.log(formatAuditReport(result));
|
|
186
221
|
}
|
|
187
222
|
|
|
188
|
-
return shouldFailAudit(
|
|
223
|
+
return shouldFailAudit(findings, options.failOn) ? 1 : 0;
|
|
189
224
|
} catch (error) {
|
|
190
225
|
if (options.json) {
|
|
191
226
|
console.log(
|
|
@@ -202,6 +237,56 @@ export function runCli(argv = process.argv.slice(2)) {
|
|
|
202
237
|
}
|
|
203
238
|
}
|
|
204
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Print a failure that happened while writing fixes.
|
|
242
|
+
*
|
|
243
|
+
* @param {*} error - Write failure.
|
|
244
|
+
* @param {object} options - Parsed CLI options.
|
|
245
|
+
* @returns {number} Exit code.
|
|
246
|
+
*/
|
|
247
|
+
function reportFixFailure(error, options) {
|
|
248
|
+
const projectDir = resolve(options.projectDir);
|
|
249
|
+
|
|
250
|
+
if (options.json) {
|
|
251
|
+
console.log(
|
|
252
|
+
formatAuditJsonErrorReport(error, {
|
|
253
|
+
code: 'fix-failed',
|
|
254
|
+
projectDir,
|
|
255
|
+
fixes: error?.fixes,
|
|
256
|
+
}),
|
|
257
|
+
);
|
|
258
|
+
} else {
|
|
259
|
+
const lines = [`Audit fix failed: ${error.message || error}`];
|
|
260
|
+
const applied = error?.fixes?.applied || [];
|
|
261
|
+
const rewrittenFiles = Array.from(
|
|
262
|
+
new Set(applied.map(({ finding }) => finding.filePath)),
|
|
263
|
+
).sort();
|
|
264
|
+
|
|
265
|
+
if (rewrittenFiles.length) {
|
|
266
|
+
const verb = error?.fixes?.dryRun ? 'Would apply' : 'Applied';
|
|
267
|
+
lines.push(
|
|
268
|
+
`${verb} ${applied.length} fix(es) across ${rewrittenFiles.length} file(s) before the failure:`,
|
|
269
|
+
...rewrittenFiles.map(
|
|
270
|
+
(filePath) => ` ${displayPath(projectDir, filePath)}`,
|
|
271
|
+
),
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const skipped = error?.fixes?.skipped || [];
|
|
276
|
+
if (skipped.length) {
|
|
277
|
+
lines.push(`Skipped ${skipped.length} fixable finding(s):`);
|
|
278
|
+
for (const { finding, reason } of skipped) {
|
|
279
|
+
const where = `${displayPath(projectDir, finding.filePath)}:${finding.line}`;
|
|
280
|
+
lines.push(` ${where} ${reason}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
console.error(lines.join('\n'));
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return cliFailureExitCode;
|
|
288
|
+
}
|
|
289
|
+
|
|
205
290
|
if (isCliEntrypoint(['audit.js', 'emulsify-audit'])) {
|
|
206
291
|
process.exitCode = runCli();
|
|
207
292
|
}
|
|
@@ -22,14 +22,15 @@ function getRuntimeEnv() {
|
|
|
22
22
|
return globalThis.__EMULSIFY_ENV__ || DEFAULT_ENV;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
// Storybook copies staticDirs beside the preview document, so `iframe.html`
|
|
26
|
+
// and the public `assets/` directory are always siblings in a static build. A
|
|
27
|
+
// document-relative base is resolved against the preview document's own URL:
|
|
28
|
+
// `/iframe.html` at a domain root resolves to `/assets/...`, and
|
|
29
|
+
// `/project/iframe.html` under a deployment subpath resolves to
|
|
30
|
+
// `/project/assets/...`. That covers root deployments, project Pages URLs on
|
|
31
|
+
// any host, custom domains, and arbitrary nested paths without hostname
|
|
32
|
+
// detection or a configured base path.
|
|
33
|
+
const PUBLIC_ASSET_BASE = './assets/';
|
|
33
34
|
|
|
34
35
|
const pendingSourceLoads = new Set();
|
|
35
36
|
const warnedAssetSources = new Set();
|
|
@@ -54,7 +55,10 @@ function normalizeAssetPath(assetPath) {
|
|
|
54
55
|
/**
|
|
55
56
|
* Read a text asset from Storybook's static server.
|
|
56
57
|
*
|
|
57
|
-
*
|
|
58
|
+
* The request URL stays relative to the preview document, so the fallback
|
|
59
|
+
* resolves the same way at a domain root and under a deployment subpath.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} relPath - Public asset path below the public asset base.
|
|
58
62
|
* @returns {string|undefined} Fetched text when available.
|
|
59
63
|
*/
|
|
60
64
|
function fetchTextAsset(relPath) {
|
|
@@ -77,7 +81,7 @@ function fetchTextAsset(relPath) {
|
|
|
77
81
|
/**
|
|
78
82
|
* Warn once when a text asset cannot use the lazy virtual source map.
|
|
79
83
|
*
|
|
80
|
-
* @param {string} relPath - Public asset path below
|
|
84
|
+
* @param {string} relPath - Public asset path below the public asset base.
|
|
81
85
|
* @param {string} reason - Short explanation of the missing source.
|
|
82
86
|
*/
|
|
83
87
|
function warnTextAssetSource(relPath, reason) {
|