@emulsify/core 4.3.2 → 4.4.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 +4 -5
- 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 +7 -1
- 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/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-maps.js +88 -0
- package/config/vite/vite.config.js +106 -42
- package/package.json +40 -29
- package/scripts/audit/checks/css-asset-references.js +256 -24
- 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/twig.js +11 -29
- package/scripts/audit/report.js +83 -5
- package/scripts/audit.js +87 -2
- package/src/storybook/twig/source-function.js +14 -10
package/scripts/audit/index.js
CHANGED
|
@@ -142,7 +142,7 @@ export function runAuditChecks(context) {
|
|
|
142
142
|
* Run the combined Emulsify audit.
|
|
143
143
|
*
|
|
144
144
|
* @param {{projectDir?: string, twigThreshold?: number}} [options={}] - Options.
|
|
145
|
-
* @returns {{projectDir: string, summary: object, files: object, findings: object[]}} Audit result.
|
|
145
|
+
* @returns {{projectDir: string, sourceRoots: string[], summary: object, files: object, findings: object[]}} Audit result.
|
|
146
146
|
*/
|
|
147
147
|
export function runAudits(options = {}) {
|
|
148
148
|
resetFileReadCache();
|
|
@@ -161,7 +161,7 @@ export function runAudits(options = {}) {
|
|
|
161
161
|
},
|
|
162
162
|
);
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
const result = {
|
|
165
165
|
projectDir: context.projectDir,
|
|
166
166
|
summary,
|
|
167
167
|
files: {
|
|
@@ -172,6 +172,14 @@ export function runAudits(options = {}) {
|
|
|
172
172
|
},
|
|
173
173
|
findings,
|
|
174
174
|
};
|
|
175
|
+
|
|
176
|
+
// The fix scope is CLI-internal plumbing. Keep raw audit serialization
|
|
177
|
+
// stable while still sharing the normalized roots with the fix phase.
|
|
178
|
+
Object.defineProperty(result, 'sourceRoots', {
|
|
179
|
+
value: context.sourceRoots,
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
return result;
|
|
175
183
|
}
|
|
176
184
|
|
|
177
185
|
export { runAudits as auditProject };
|
package/scripts/audit/lib/css.js
CHANGED
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { basename, dirname, resolve } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
assetTailFor,
|
|
8
|
+
isAssetAliasPath,
|
|
9
|
+
} from '../../../config/vite/plugins/assets/asset-url-rebase.js';
|
|
10
|
+
import { tokenizeStylesheetUrls } from '../../../config/vite/utils/css-urls.js';
|
|
6
11
|
import {
|
|
7
12
|
compiledAssetOutputPath,
|
|
8
13
|
storybookStyleOutputPath,
|
|
@@ -43,40 +48,32 @@ function resolveSassUrlValue(value, variables) {
|
|
|
43
48
|
);
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
/**
|
|
47
|
-
* Mask style comments while preserving line and character positions.
|
|
48
|
-
*
|
|
49
|
-
* @param {string} source - Stylesheet source.
|
|
50
|
-
* @returns {string} Source with comments replaced by whitespace.
|
|
51
|
-
*/
|
|
52
|
-
function maskStyleComments(source) {
|
|
53
|
-
const blank = (match) => match.replace(/[^\n]/g, ' ');
|
|
54
|
-
|
|
55
|
-
return source
|
|
56
|
-
.replace(/\/\*[\s\S]*?\*\//g, blank)
|
|
57
|
-
.replace(/^[\t ]*\/\/.*$/gm, blank);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
51
|
/**
|
|
61
52
|
* Extract URL references from CSS or Sass source.
|
|
62
53
|
*
|
|
54
|
+
* `start` and `end` bracket the specifier *without* its quotes, so an autofix
|
|
55
|
+
* can splice a replacement in without disturbing quote style. The shared
|
|
56
|
+
* tokenizer preserves original positions, so `source.slice(start, end) === raw`.
|
|
57
|
+
*
|
|
63
58
|
* @param {string} source - Stylesheet source.
|
|
64
|
-
* @returns {{value: string, raw: string, line: number}[]} URL references.
|
|
59
|
+
* @returns {{value: string, raw: string, quote: string, line: number, start: number, end: number}[]} URL references.
|
|
65
60
|
*/
|
|
66
61
|
export function findCssUrlReferences(source) {
|
|
67
|
-
const
|
|
68
|
-
const variables = findSassStringVariables(
|
|
62
|
+
const { urls, sourceWithoutComments } = tokenizeStylesheetUrls(source);
|
|
63
|
+
const variables = findSassStringVariables(sourceWithoutComments);
|
|
69
64
|
const references = [];
|
|
70
|
-
const pattern = /url\(\s*(?:(['"])(.*?)\1|([^'")][^)]*?))\s*\)/g;
|
|
71
65
|
|
|
72
|
-
for (const
|
|
73
|
-
const raw =
|
|
66
|
+
for (const token of urls) {
|
|
67
|
+
const raw = token.value;
|
|
74
68
|
const value = resolveSassUrlValue(raw, variables).trim();
|
|
75
69
|
|
|
76
70
|
references.push({
|
|
77
71
|
value,
|
|
78
72
|
raw,
|
|
79
|
-
|
|
73
|
+
quote: token.quote,
|
|
74
|
+
line: lineNumberAt(source, token.start),
|
|
75
|
+
start: token.valueStart,
|
|
76
|
+
end: token.valueEnd,
|
|
80
77
|
});
|
|
81
78
|
}
|
|
82
79
|
|
|
@@ -84,23 +81,32 @@ export function findCssUrlReferences(source) {
|
|
|
84
81
|
}
|
|
85
82
|
|
|
86
83
|
/**
|
|
87
|
-
*
|
|
84
|
+
* Classify how a filesystem-ish CSS URL should be resolved.
|
|
85
|
+
*
|
|
86
|
+
* - `asset-root` — `/assets/...`, `@assets/...`, or legacy `assets/...`.
|
|
87
|
+
* Resolved against the project asset roots, which is what Storybook serves
|
|
88
|
+
* and what the build rebases to.
|
|
89
|
+
* - `runtime` — some other absolute URL (`/sites/default/files/...`). The
|
|
90
|
+
* platform serves it; the audit has nothing to check.
|
|
91
|
+
* - `relative` — resolved from the stylesheet's own directory.
|
|
88
92
|
*
|
|
89
93
|
* @param {string} value - URL value.
|
|
90
|
-
* @returns {
|
|
94
|
+
* @returns {'asset-root'|'runtime'|'relative'} Resolution strategy.
|
|
91
95
|
*/
|
|
92
|
-
export function
|
|
93
|
-
return
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
export function classifyCssAssetUrl(value) {
|
|
97
|
+
if (assetTailFor(cssUrlPath(value))) return 'asset-root';
|
|
98
|
+
|
|
99
|
+
return value.startsWith('/') ? 'runtime' : 'relative';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Determine whether a CSS URL uses the exact namespaced asset alias.
|
|
104
|
+
*
|
|
105
|
+
* @param {string} value - URL path without query or hash.
|
|
106
|
+
* @returns {boolean} TRUE for `@assets/...` paths.
|
|
107
|
+
*/
|
|
108
|
+
export function isCssAssetAlias(value) {
|
|
109
|
+
return isAssetAliasPath(String(value));
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
/**
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
* @file Twig reference parsing and resolution helpers for the project audit.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { dirname,
|
|
5
|
+
import { dirname, resolve } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
resolveAssetRoots,
|
|
8
|
+
toAbsoluteAssetRoot,
|
|
9
|
+
} from '../../../config/vite/utils/asset-roots.js';
|
|
6
10
|
import { safeExists } from '../../../config/vite/utils/fs-safe.js';
|
|
7
11
|
import { candidateKeysForReference } from '../../../src/storybook/twig/reference-paths.js';
|
|
8
12
|
import { lineNumberAt } from '../../lib/text.js';
|
|
@@ -143,45 +147,23 @@ function candidateKeysToFiles(keys, env) {
|
|
|
143
147
|
* @returns {string} Absolute filesystem path, or an empty string.
|
|
144
148
|
*/
|
|
145
149
|
export function resolveAuditAssetRoot(projectDir, assetRoot) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const normalizedProjectDir = resolve(projectDir || process.cwd());
|
|
149
|
-
const normalizedRoot = assetRoot.trim();
|
|
150
|
-
|
|
151
|
-
if (isAbsolute(normalizedRoot)) {
|
|
152
|
-
const absoluteRoot = resolve(normalizedRoot);
|
|
153
|
-
|
|
154
|
-
return safeExists(absoluteRoot)
|
|
155
|
-
? absoluteRoot
|
|
156
|
-
: resolve(normalizedProjectDir, `.${normalizedRoot}`);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
return resolve(normalizedProjectDir, normalizedRoot);
|
|
150
|
+
return toAbsoluteAssetRoot(projectDir, assetRoot);
|
|
160
151
|
}
|
|
161
152
|
|
|
162
153
|
/**
|
|
163
154
|
* Return filesystem roots that Storybook can use for @assets source() calls.
|
|
164
155
|
*
|
|
156
|
+
* Existence filtering stays off here because callers do their own directory
|
|
157
|
+
* check, and a configured-but-missing root is worth reporting rather than
|
|
158
|
+
* silently dropping.
|
|
159
|
+
*
|
|
165
160
|
* @param {object} env - Normalized environment.
|
|
166
161
|
* @param {object} [options={}] - Asset root options.
|
|
167
162
|
* @param {boolean} [options.includeGenerated=false] - Include generated roots.
|
|
168
163
|
* @returns {string[]} Absolute asset roots.
|
|
169
164
|
*/
|
|
170
165
|
export function auditAssetRoots(env = {}, { includeGenerated = false } = {}) {
|
|
171
|
-
|
|
172
|
-
const configuredRoots = Array.isArray(env?.projectStructure?.assetRoots)
|
|
173
|
-
? env.projectStructure.assetRoots
|
|
174
|
-
: [];
|
|
175
|
-
const fallbackRoots = ['assets', 'src/assets'];
|
|
176
|
-
const generatedRoots = includeGenerated ? ['dist/assets'] : [];
|
|
177
|
-
|
|
178
|
-
return Array.from(
|
|
179
|
-
new Set(
|
|
180
|
-
[...fallbackRoots, ...configuredRoots, ...generatedRoots]
|
|
181
|
-
.map((root) => resolveAuditAssetRoot(projectDir, root))
|
|
182
|
-
.filter(Boolean),
|
|
183
|
-
),
|
|
184
|
-
);
|
|
166
|
+
return resolveAssetRoots(env, { includeGenerated, existingOnly: false });
|
|
185
167
|
}
|
|
186
168
|
|
|
187
169
|
/**
|
package/scripts/audit/report.js
CHANGED
|
@@ -58,16 +58,51 @@ export function formatAuditReport(result) {
|
|
|
58
58
|
|
|
59
59
|
if (!result.findings.length) {
|
|
60
60
|
lines.push('No audit findings found.');
|
|
61
|
-
return lines.join('\n');
|
|
62
61
|
}
|
|
63
62
|
|
|
64
63
|
for (const finding of result.findings) {
|
|
65
64
|
lines.push('', ...formatFinding(finding, result.projectDir));
|
|
66
65
|
}
|
|
67
66
|
|
|
67
|
+
lines.push(...formatFixSection(result.fixes, result.projectDir));
|
|
68
|
+
|
|
68
69
|
return lines.join('\n');
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Format the autofix section appended by `--fix`.
|
|
74
|
+
*
|
|
75
|
+
* @param {{dryRun: boolean, applied: object[], skipped: object[]}} [fixes] - Fix result.
|
|
76
|
+
* @param {string} projectDir - Absolute scanned root.
|
|
77
|
+
* @returns {string[]} Report lines.
|
|
78
|
+
*/
|
|
79
|
+
function formatFixSection(fixes, projectDir) {
|
|
80
|
+
if (!fixes) return [];
|
|
81
|
+
|
|
82
|
+
const lines = ['', 'Fixes'];
|
|
83
|
+
const verb = fixes.dryRun ? 'Would apply' : 'Applied';
|
|
84
|
+
|
|
85
|
+
if (!fixes.applied.length) {
|
|
86
|
+
lines.push(`${verb} 0 fix(es).`);
|
|
87
|
+
} else {
|
|
88
|
+
lines.push(`${verb} ${fixes.applied.length} fix(es):`);
|
|
89
|
+
for (const { finding, from, to } of fixes.applied) {
|
|
90
|
+
const where = `${displayPath(projectDir, finding.filePath)}:${finding.line}`;
|
|
91
|
+
lines.push(` ${where} ${from} -> ${to}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (fixes.skipped.length) {
|
|
96
|
+
lines.push(`Skipped ${fixes.skipped.length} fixable finding(s):`);
|
|
97
|
+
for (const { finding, reason } of fixes.skipped) {
|
|
98
|
+
const where = `${displayPath(projectDir, finding.filePath)}:${finding.line}`;
|
|
99
|
+
lines.push(` ${where} ${reason}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return lines;
|
|
104
|
+
}
|
|
105
|
+
|
|
71
106
|
/**
|
|
72
107
|
* Count findings by severity for machine-readable reports.
|
|
73
108
|
*
|
|
@@ -219,7 +254,7 @@ export function createAuditJsonReport(result, options = {}) {
|
|
|
219
254
|
normalizeAuditFinding(finding, result.projectDir, options.defaultSeverity),
|
|
220
255
|
);
|
|
221
256
|
|
|
222
|
-
|
|
257
|
+
const document = {
|
|
223
258
|
schemaVersion: AUDIT_REPORT_SCHEMA_VERSION,
|
|
224
259
|
tool: createToolIdentity(),
|
|
225
260
|
root: '.',
|
|
@@ -227,19 +262,56 @@ export function createAuditJsonReport(result, options = {}) {
|
|
|
227
262
|
files: normalizeFileCounts(result.files),
|
|
228
263
|
findings,
|
|
229
264
|
};
|
|
265
|
+
|
|
266
|
+
// Present only when --fix ran, so the document shape is unchanged for every
|
|
267
|
+
// existing consumer.
|
|
268
|
+
if (result.fixes) {
|
|
269
|
+
document.fixes = normalizeFixes(result.fixes, result.projectDir);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
return document;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Normalize the autofix result for the machine-readable report.
|
|
277
|
+
*
|
|
278
|
+
* @param {{dryRun: boolean, applied: object[], skipped: object[]}} fixes - Fix result.
|
|
279
|
+
* @param {string} projectDir - Absolute scanned root.
|
|
280
|
+
* @returns {object} JSON fix block.
|
|
281
|
+
*/
|
|
282
|
+
function normalizeFixes(fixes, projectDir) {
|
|
283
|
+
const locate = (finding) => ({
|
|
284
|
+
path: displayPath(projectDir, finding.filePath) || '.',
|
|
285
|
+
...(Number.isInteger(finding.line) && finding.line > 0
|
|
286
|
+
? { line: finding.line }
|
|
287
|
+
: {}),
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
dryRun: Boolean(fixes.dryRun),
|
|
292
|
+
applied: fixes.applied.map(({ finding, from, to }) => ({
|
|
293
|
+
...locate(finding),
|
|
294
|
+
from,
|
|
295
|
+
to,
|
|
296
|
+
})),
|
|
297
|
+
skipped: fixes.skipped.map(({ finding, reason }) => ({
|
|
298
|
+
...locate(finding),
|
|
299
|
+
reason: normalizeReportText(reason, projectDir),
|
|
300
|
+
})),
|
|
301
|
+
};
|
|
230
302
|
}
|
|
231
303
|
|
|
232
304
|
/**
|
|
233
305
|
* Create a structured machine-readable CLI or audit failure.
|
|
234
306
|
*
|
|
235
307
|
* @param {*} error - Failure value.
|
|
236
|
-
* @param {{code?: string, projectDir?: string}} [options={}] - Error options.
|
|
308
|
+
* @param {{code?: string, projectDir?: string, fixes?: object}} [options={}] - Error options.
|
|
237
309
|
* @returns {object} JSON error document.
|
|
238
310
|
*/
|
|
239
311
|
export function createAuditJsonErrorReport(error, options = {}) {
|
|
240
312
|
const message = error?.message || error;
|
|
241
313
|
|
|
242
|
-
|
|
314
|
+
const document = {
|
|
243
315
|
schemaVersion: AUDIT_REPORT_SCHEMA_VERSION,
|
|
244
316
|
tool: createToolIdentity(),
|
|
245
317
|
error: {
|
|
@@ -247,6 +319,12 @@ export function createAuditJsonErrorReport(error, options = {}) {
|
|
|
247
319
|
message: normalizeReportText(message, options.projectDir || ''),
|
|
248
320
|
},
|
|
249
321
|
};
|
|
322
|
+
|
|
323
|
+
if (options.fixes) {
|
|
324
|
+
document.fixes = normalizeFixes(options.fixes, options.projectDir || '');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return document;
|
|
250
328
|
}
|
|
251
329
|
|
|
252
330
|
/**
|
|
@@ -265,7 +343,7 @@ export function formatAuditJsonReport(result, options = {}) {
|
|
|
265
343
|
* Format a CLI or audit failure as machine-readable JSON.
|
|
266
344
|
*
|
|
267
345
|
* @param {*} error - Failure value.
|
|
268
|
-
* @param {{code?: string, projectDir?: string}} [options={}] - Error options.
|
|
346
|
+
* @param {{code?: string, projectDir?: string, fixes?: object}} [options={}] - Error options.
|
|
269
347
|
* @returns {string} JSON error document.
|
|
270
348
|
*/
|
|
271
349
|
export function formatAuditJsonErrorReport(error, options = {}) {
|
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) {
|