@emulsify/core 4.2.1 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.storybook/main-vite.js +102 -37
- package/.storybook/main.js +20 -0
- package/.storybook/ready-reporter.js +230 -0
- package/README.md +186 -56
- package/config/a11y.config.js +2 -1
- package/config/vite/entries.js +4 -4
- package/config/vite/plugins/assets/copy-src-assets.js +120 -0
- package/config/vite/plugins/assets/copy-twig-files.js +130 -0
- package/config/vite/plugins/{css-asset-relativizer.js → assets/css-asset-relativizer.js} +9 -0
- package/config/vite/plugins/{mirror-components.js → assets/mirror-components.js} +2 -28
- package/config/vite/plugins/{source-file-index.js → assets/source-file-index.js} +1 -1
- package/config/vite/plugins/{svg-sprite.js → assets/svg-sprite.js} +2 -2
- package/config/vite/plugins/index.js +23 -14
- package/config/vite/plugins/reporter/asset-resolver.js +541 -0
- package/config/vite/plugins/reporter/build-errors.js +284 -0
- package/config/vite/plugins/reporter/diagnostics.js +367 -0
- package/config/vite/plugins/reporter/format.js +240 -0
- package/config/vite/plugins/reporter/index.js +417 -0
- package/config/vite/plugins/reporter/render.js +1332 -0
- package/config/vite/plugins/reporter/sass-logger.js +237 -0
- package/config/vite/plugins/reporter/source-roots.js +561 -0
- package/config/vite/plugins/reporter/verbosity.js +119 -0
- package/config/vite/plugins/reporter/vite-logger.js +249 -0
- package/config/vite/plugins/reporter/watch-mode.js +40 -0
- package/config/vite/plugins/{twig-extension-installers.js → twig/extension-installers.js} +1 -1
- package/config/vite/{twig-extensions.js → plugins/twig/extensions.js} +2 -2
- package/config/vite/plugins/{twig-module.js → twig/twig-module.js} +197 -85
- package/config/vite/plugins/{virtual-twig-asset-sources.js → twig/virtual-twig-asset-sources.js} +37 -136
- package/config/vite/plugins/{virtual-twig-globs.js → twig/virtual-twig-globs.js} +3 -32
- package/config/vite/plugins/{vituum-patch.js → twig/vituum-patch.js} +3 -3
- package/config/vite/plugins.js +1 -1
- package/config/vite/project-config.js +1 -1
- package/config/vite/project-structure.js +1 -1
- package/config/vite/utils/lru.js +77 -0
- package/config/vite/utils/package-version.js +42 -0
- package/config/vite/utils/paths.js +1 -9
- package/config/vite/utils/react-singleton.js +1 -1
- package/config/vite/vite.config.js +91 -5
- package/package.json +95 -57
- package/scripts/a11y.js +115 -23
- package/scripts/audit/checks/core-imports.js +78 -0
- package/scripts/audit/checks/css-asset-references.js +99 -0
- package/scripts/audit/checks/drupal-assumptions.js +48 -0
- package/scripts/audit/checks/files-outside-roots.js +53 -0
- package/scripts/audit/checks/generated-package-scripts.js +113 -0
- package/scripts/audit/checks/legacy-twig-stories.js +33 -0
- package/scripts/audit/checks/package-overrides.js +91 -0
- package/scripts/audit/checks/project-config.js +71 -0
- package/scripts/audit/checks/story-discovery.js +35 -0
- package/scripts/audit/checks/twig-references.js +69 -0
- package/scripts/audit/checks/twig-volume.js +54 -0
- package/scripts/audit/checks/webpack-patterns.js +86 -0
- package/scripts/audit/index.js +177 -0
- package/scripts/audit/lib/css.js +165 -0
- package/scripts/audit/lib/files.js +168 -0
- package/scripts/audit/lib/findings.js +31 -0
- package/scripts/audit/lib/package-json.js +65 -0
- package/scripts/audit/lib/twig.js +227 -0
- package/scripts/audit/report.js +273 -0
- package/scripts/audit-twig-stories.js +115 -78
- package/scripts/audit.js +150 -1632
- package/scripts/check-node-version.js +136 -10
- package/scripts/inspect-components.js +456 -0
- package/scripts/lib/cli.js +179 -0
- package/scripts/lib/fs.js +31 -0
- package/scripts/lib/proc.js +78 -0
- package/scripts/lib/text.js +14 -0
- package/scripts/loadYaml.js +2 -2
- package/src/extensions/shared/attributes.js +3 -3
- package/src/extensions/shared/lists.js +2 -6
- package/src/extensions/shared/root-relative.js +38 -0
- package/src/storybook/index.js +4 -0
- package/src/storybook/render-twig.js +1 -1
- package/src/storybook/render-web-component.js +459 -0
- package/src/storybook/twig/asset-source-runtime.js +193 -0
- package/src/storybook/twig/{source-extensions.js → constants.js} +3 -1
- package/src/storybook/twig/reference-paths.js +2 -13
- package/src/storybook/twig/setup.js +12 -2
- package/src/storybook/twig/source-function.js +5 -2
- package/config/vite/plugins/copy-src-assets.js +0 -76
- package/config/vite/plugins/copy-twig-files.js +0 -84
- package/config/vite/utils/unique.js +0 -36
- package/src/storybook/twig/include.js +0 -28
- package/src/storybook/twig/source-events.js +0 -5
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Drupal assumption audit check.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { lineNumberAt } from '../../lib/text.js';
|
|
6
|
+
import { makeFinding } from '../lib/findings.js';
|
|
7
|
+
import { cachedReadFile } from '../lib/files.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Audit Drupal assumptions in non-Drupal projects.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} context - Audit context.
|
|
13
|
+
* @returns {object[]} Findings.
|
|
14
|
+
*/
|
|
15
|
+
export function auditDrupalAssumptions(context) {
|
|
16
|
+
const { codeFiles, env } = context;
|
|
17
|
+
if (env.platform === 'drupal') return [];
|
|
18
|
+
|
|
19
|
+
const findings = [];
|
|
20
|
+
const patterns = [
|
|
21
|
+
/\bDrupal\.attachBehaviors\b/,
|
|
22
|
+
/\bwindow\.Drupal\b/,
|
|
23
|
+
/\bglobalThis\.Drupal\b/,
|
|
24
|
+
/['"][^'"]*_drupal\.js['"]/,
|
|
25
|
+
/['"]twig-drupal-filters['"]/,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const filePath of codeFiles) {
|
|
29
|
+
const source = cachedReadFile(filePath);
|
|
30
|
+
const match = patterns.map((pattern) => pattern.exec(source)).find(Boolean);
|
|
31
|
+
|
|
32
|
+
if (!match) continue;
|
|
33
|
+
|
|
34
|
+
findings.push(
|
|
35
|
+
makeFinding({
|
|
36
|
+
id: 'drupal-assumption-non-drupal',
|
|
37
|
+
severity: 'warn',
|
|
38
|
+
filePath,
|
|
39
|
+
line: lineNumberAt(source, match.index || 0),
|
|
40
|
+
message:
|
|
41
|
+
'Drupal-specific Storybook/runtime code was found, but the active platform is not drupal.',
|
|
42
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/platform-adapters.md',
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return findings;
|
|
48
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Files outside source roots audit check.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { displayPath, makeFinding } from '../lib/findings.js';
|
|
6
|
+
import { isInsideAnyRoot } from '../lib/files.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Audit files that look like component Twig files outside source roots.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} context - Audit context.
|
|
12
|
+
* @returns {object[]} Findings.
|
|
13
|
+
*/
|
|
14
|
+
export function auditFilesOutsideRoots(context) {
|
|
15
|
+
const { env, projectDir, twigFiles } = context;
|
|
16
|
+
const roots = [
|
|
17
|
+
...(env.projectStructure?.twigRoots || []),
|
|
18
|
+
...(env.projectStructure?.sourceRoots || []),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
if (!roots.length) return [];
|
|
22
|
+
|
|
23
|
+
return twigFiles
|
|
24
|
+
.filter((filePath) => !isInsideAnyRoot(filePath, roots))
|
|
25
|
+
.map((filePath) =>
|
|
26
|
+
makeFinding({
|
|
27
|
+
id: 'twig-file-outside-source-roots',
|
|
28
|
+
severity: 'info',
|
|
29
|
+
filePath,
|
|
30
|
+
message:
|
|
31
|
+
'Twig file is outside normalized source roots and will not be available to Storybook include()/source() unless another integration loads it.',
|
|
32
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/project-structure.md',
|
|
33
|
+
}),
|
|
34
|
+
)
|
|
35
|
+
.filter((finding) => !isNonComponentTwigFile(projectDir, finding.filePath));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Determine whether a Twig file is intentionally outside component roots.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} projectDir - Absolute project root.
|
|
42
|
+
* @param {string} filePath - Absolute Twig file path.
|
|
43
|
+
* @returns {boolean} TRUE when the file should not be treated as component source.
|
|
44
|
+
*/
|
|
45
|
+
function isNonComponentTwigFile(projectDir, filePath) {
|
|
46
|
+
const relPath = displayPath(projectDir, filePath);
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
relPath.startsWith('docs/') ||
|
|
50
|
+
relPath.startsWith('templates/') ||
|
|
51
|
+
relPath.includes('/templates/')
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Generated package script audit check.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
safeExists,
|
|
8
|
+
safeReadJson,
|
|
9
|
+
} from '../../../config/vite/utils/fs-safe.js';
|
|
10
|
+
import { makeFinding } from '../lib/findings.js';
|
|
11
|
+
import {
|
|
12
|
+
packageIsEmulsifyCore,
|
|
13
|
+
packageUsesEmulsifyCore,
|
|
14
|
+
} from '../lib/package-json.js';
|
|
15
|
+
|
|
16
|
+
const GENERATED_PACKAGE_SCRIPT_DOCS =
|
|
17
|
+
'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/migration-4x.md#manual-packagejson-updates';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Audit generated-theme package scripts that must be updated manually.
|
|
21
|
+
*
|
|
22
|
+
* Generated themes copy their root package.json from the starter at creation
|
|
23
|
+
* time. Whisk updates do not automatically flow into existing themes, so the
|
|
24
|
+
* audit flags stale Webpack-era scripts and missing Core 4 audit/Vite scripts.
|
|
25
|
+
*
|
|
26
|
+
* @param {object} context - Audit context.
|
|
27
|
+
* @returns {object[]} Findings.
|
|
28
|
+
*/
|
|
29
|
+
export function auditGeneratedPackageScripts(context) {
|
|
30
|
+
const { env, projectDir } = context;
|
|
31
|
+
const packagePath = resolve(projectDir, 'package.json');
|
|
32
|
+
|
|
33
|
+
if (!safeExists(packagePath)) {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { data: packageJson, error } = safeReadJson(packagePath);
|
|
38
|
+
if (error || !packageUsesEmulsifyCore(packageJson)) {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (packageIsEmulsifyCore(packageJson)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const scripts = packageJson.scripts || {};
|
|
47
|
+
const starterRepository = env.projectConfig?.starter?.repository;
|
|
48
|
+
const fromGeneratedStarter =
|
|
49
|
+
typeof starterRepository === 'string' &&
|
|
50
|
+
/emulsify-(drupal|wordpress|craftcms|starter)|emulsify-ds/i.test(
|
|
51
|
+
starterRepository,
|
|
52
|
+
);
|
|
53
|
+
const usesGeneratedCoreScripts = Object.values(scripts).some(
|
|
54
|
+
(script) =>
|
|
55
|
+
typeof script === 'string' &&
|
|
56
|
+
/node_modules\/@emulsify\/core\/(?:config\/(?:webpack|vite)|scripts\/audit)/.test(
|
|
57
|
+
script,
|
|
58
|
+
),
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
if (!fromGeneratedStarter && !usesGeneratedCoreScripts) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const findings = [];
|
|
66
|
+
const details = [];
|
|
67
|
+
const buildScript = scripts.build || '';
|
|
68
|
+
|
|
69
|
+
if (/\bwebpack\b|config\/webpack/.test(buildScript)) {
|
|
70
|
+
details.push('Replace scripts.build with the Vite build command.');
|
|
71
|
+
} else if (
|
|
72
|
+
/node_modules\/@emulsify\/core\/config\/vite\/vite\.config\.js/.test(
|
|
73
|
+
buildScript,
|
|
74
|
+
) &&
|
|
75
|
+
/\bvite\s+(?:--config|-c)\b/.test(buildScript)
|
|
76
|
+
) {
|
|
77
|
+
details.push('Replace scripts.build with the Vite build command.');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (Object.prototype.hasOwnProperty.call(scripts, 'build-dev')) {
|
|
81
|
+
details.push('Remove scripts.build-dev; the Vite build replaces it.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (/\bwebpack\b|npm:webpack|config\/webpack/.test(scripts.develop || '')) {
|
|
85
|
+
details.push('Replace scripts.develop with the Vite/Storybook watcher.');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (Object.prototype.hasOwnProperty.call(scripts, 'webpack')) {
|
|
89
|
+
details.push('Replace scripts.webpack with scripts.vite.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const scriptName of ['audit', 'audit:twig-stories', 'vite']) {
|
|
93
|
+
if (!Object.prototype.hasOwnProperty.call(scripts, scriptName)) {
|
|
94
|
+
details.push(`Add scripts.${scriptName}.`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (details.length) {
|
|
99
|
+
findings.push(
|
|
100
|
+
makeFinding({
|
|
101
|
+
id: 'generated-package-json-migration-needed',
|
|
102
|
+
severity: 'warn',
|
|
103
|
+
filePath: packagePath,
|
|
104
|
+
message:
|
|
105
|
+
'package.json does not match the generated-theme scripts expected by Emulsify Core 4.',
|
|
106
|
+
details,
|
|
107
|
+
docs: GENERATED_PACKAGE_SCRIPT_DOCS,
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return findings;
|
|
113
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Legacy Twig story audit check.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { analyzeStorySource } from '../../audit-twig-stories.js';
|
|
6
|
+
import { makeFinding } from '../lib/findings.js';
|
|
7
|
+
import { cachedReadFile } from '../lib/files.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Add legacy Twig story migration findings.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} context - Audit context.
|
|
13
|
+
* @returns {object[]} Findings.
|
|
14
|
+
*/
|
|
15
|
+
export function auditLegacyTwigStories(context) {
|
|
16
|
+
const { storyFiles } = context;
|
|
17
|
+
const findings = storyFiles
|
|
18
|
+
.map((filePath) => analyzeStorySource(cachedReadFile(filePath), filePath))
|
|
19
|
+
.filter((result) => result.shouldUpgrade);
|
|
20
|
+
|
|
21
|
+
return findings.map((finding) =>
|
|
22
|
+
makeFinding({
|
|
23
|
+
id: 'legacy-twig-story',
|
|
24
|
+
severity: 'warn',
|
|
25
|
+
filePath: finding.filePath,
|
|
26
|
+
line: finding.directTemplateReturns[0]?.line,
|
|
27
|
+
message:
|
|
28
|
+
'Twig story appears to return an HTML string directly. This remains compatible, but renderTwig() is preferred for active migrations.',
|
|
29
|
+
details: finding.reasons,
|
|
30
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/storybook.md#legacy-twig-story-compatibility',
|
|
31
|
+
}),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Package override audit check.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
safeExists,
|
|
8
|
+
safeReadJson,
|
|
9
|
+
} from '../../../config/vite/utils/fs-safe.js';
|
|
10
|
+
import { makeFinding } from '../lib/findings.js';
|
|
11
|
+
import {
|
|
12
|
+
hasRecommendedOverride,
|
|
13
|
+
packageUsesEmulsifyCore,
|
|
14
|
+
} from '../lib/package-json.js';
|
|
15
|
+
|
|
16
|
+
const RECOMMENDED_PACKAGE_OVERRIDES = [
|
|
17
|
+
{
|
|
18
|
+
label: 'glob',
|
|
19
|
+
value: '^13.0.6',
|
|
20
|
+
paths: [['glob']],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
label: 'locutus',
|
|
24
|
+
value: '^3.0.36',
|
|
25
|
+
paths: [['locutus']],
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
label: 'minimatch@3.0.x',
|
|
29
|
+
value: '^3.1.5',
|
|
30
|
+
paths: [['minimatch@3.0.x']],
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Audit package-level dependency override policy for installed projects.
|
|
36
|
+
*
|
|
37
|
+
* npm only applies `overrides` from the root package being installed. When
|
|
38
|
+
* Emulsify Core is installed into a generated theme, Core's own overrides do
|
|
39
|
+
* not protect that theme's transitive dependency graph.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} context - Audit context.
|
|
42
|
+
* @returns {object[]} Findings.
|
|
43
|
+
*/
|
|
44
|
+
export function auditPackageOverrides(context) {
|
|
45
|
+
const { projectDir } = context;
|
|
46
|
+
const packagePath = resolve(projectDir, 'package.json');
|
|
47
|
+
|
|
48
|
+
if (!safeExists(packagePath)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const { data: packageJson, error } = safeReadJson(packagePath);
|
|
53
|
+
if (error) {
|
|
54
|
+
return [
|
|
55
|
+
makeFinding({
|
|
56
|
+
id: 'package-json-unreadable',
|
|
57
|
+
severity: 'warn',
|
|
58
|
+
filePath: packagePath,
|
|
59
|
+
message: `Unable to parse package.json: ${error.message || error}`,
|
|
60
|
+
}),
|
|
61
|
+
];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!packageUsesEmulsifyCore(packageJson)) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const overrides = packageJson.overrides || {};
|
|
69
|
+
const missing = RECOMMENDED_PACKAGE_OVERRIDES.filter(
|
|
70
|
+
(recommendation) => !hasRecommendedOverride(overrides, recommendation),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
if (!missing.length) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [
|
|
78
|
+
makeFinding({
|
|
79
|
+
id: 'recommended-package-overrides-missing',
|
|
80
|
+
severity: 'warn',
|
|
81
|
+
filePath: packagePath,
|
|
82
|
+
message:
|
|
83
|
+
'package.json is missing recommended root npm overrides for Emulsify Core transitive install warnings.',
|
|
84
|
+
details: missing.map(
|
|
85
|
+
(recommendation) =>
|
|
86
|
+
`Add overrides.${recommendation.label}: ${recommendation.value}.`,
|
|
87
|
+
),
|
|
88
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/migration-4x.md#install-warning-controls',
|
|
89
|
+
}),
|
|
90
|
+
];
|
|
91
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Project configuration audit check.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { displayPath, makeFinding } from '../lib/findings.js';
|
|
7
|
+
import { safeIsDirectory } from '../lib/files.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Audit basic project configuration and structure root health.
|
|
11
|
+
*
|
|
12
|
+
* @param {object} context - Audit context.
|
|
13
|
+
* @returns {object[]} Findings.
|
|
14
|
+
*/
|
|
15
|
+
export function auditProjectConfig(context) {
|
|
16
|
+
const { configExists, env, error, projectDir } = context;
|
|
17
|
+
const findings = [];
|
|
18
|
+
|
|
19
|
+
if (!configExists) {
|
|
20
|
+
findings.push(
|
|
21
|
+
makeFinding({
|
|
22
|
+
id: 'missing-project-config',
|
|
23
|
+
severity: 'error',
|
|
24
|
+
message:
|
|
25
|
+
'project.emulsify.json is missing, so platform and structure defaults may not match the project.',
|
|
26
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/project-structure.md',
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (error) {
|
|
32
|
+
findings.push(
|
|
33
|
+
makeFinding({
|
|
34
|
+
id: 'project-config-resolution-failed',
|
|
35
|
+
severity: 'error',
|
|
36
|
+
message: `Unable to resolve project.emulsify.json: ${error.message || error}`,
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
for (const implementation of env.structureImplementations || []) {
|
|
42
|
+
if (!safeIsDirectory(implementation.directory)) {
|
|
43
|
+
findings.push(
|
|
44
|
+
makeFinding({
|
|
45
|
+
id: 'missing-structure-implementation',
|
|
46
|
+
severity: 'error',
|
|
47
|
+
filePath: resolve(projectDir, 'project.emulsify.json'),
|
|
48
|
+
message: `Configured structureImplementation "${implementation.name}" does not exist: ${displayPath(
|
|
49
|
+
projectDir,
|
|
50
|
+
implementation.directory,
|
|
51
|
+
)}`,
|
|
52
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/project-structure.md',
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
for (const root of env.ignoredAssetRoots || []) {
|
|
59
|
+
findings.push(
|
|
60
|
+
makeFinding({
|
|
61
|
+
id: 'invalid-asset-root',
|
|
62
|
+
severity: 'warn',
|
|
63
|
+
filePath: resolve(projectDir, 'project.emulsify.json'),
|
|
64
|
+
message: `Configured asset root "${root}" was ignored because it resolves outside the project root.`,
|
|
65
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/project-structure.md#asset-roots',
|
|
66
|
+
}),
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return findings;
|
|
71
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Story discovery audit check.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { collectStoryFiles } from '../../audit-twig-stories.js';
|
|
6
|
+
import { makeFinding } from '../lib/findings.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Audit story files that will not be discovered by Storybook.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} context - Audit context.
|
|
12
|
+
* @returns {object[]} Findings.
|
|
13
|
+
*/
|
|
14
|
+
export function auditStoryDiscovery(context) {
|
|
15
|
+
const { projectDir, storyFiles } = context;
|
|
16
|
+
const discovered = new Set(collectStoryFiles(projectDir));
|
|
17
|
+
const findings = [];
|
|
18
|
+
|
|
19
|
+
for (const storyFile of storyFiles) {
|
|
20
|
+
if (discovered.has(storyFile)) continue;
|
|
21
|
+
|
|
22
|
+
findings.push(
|
|
23
|
+
makeFinding({
|
|
24
|
+
id: 'story-outside-discovered-roots',
|
|
25
|
+
severity: 'error',
|
|
26
|
+
filePath: storyFile,
|
|
27
|
+
message:
|
|
28
|
+
'Story file is outside the normalized Storybook roots and will not be discovered.',
|
|
29
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/project-structure.md',
|
|
30
|
+
}),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return findings;
|
|
35
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Twig reference audit check.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { makeFinding } from '../lib/findings.js';
|
|
7
|
+
import { cachedReadFile } from '../lib/files.js';
|
|
8
|
+
import {
|
|
9
|
+
findTwigIncludeSourceReferences,
|
|
10
|
+
findTwigNamespaceReferences,
|
|
11
|
+
resolvesTwigReference,
|
|
12
|
+
} from '../lib/twig.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Audit Twig namespace and include/source resolution.
|
|
16
|
+
*
|
|
17
|
+
* @param {object} context - Audit context.
|
|
18
|
+
* @returns {object[]} Findings.
|
|
19
|
+
*/
|
|
20
|
+
export function auditTwigReferences(context) {
|
|
21
|
+
const { env, projectDir, twigFiles } = context;
|
|
22
|
+
const namespaceRoots = env.namespaceRoots || {};
|
|
23
|
+
const knownNamespaces = new Set([...Object.keys(namespaceRoots), 'assets']);
|
|
24
|
+
const findings = [];
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
|
|
27
|
+
for (const twigFile of twigFiles) {
|
|
28
|
+
const source = cachedReadFile(twigFile);
|
|
29
|
+
|
|
30
|
+
for (const ref of findTwigNamespaceReferences(source)) {
|
|
31
|
+
if (knownNamespaces.has(ref.namespace)) continue;
|
|
32
|
+
|
|
33
|
+
const key = `${twigFile}:${ref.line}:unknown:${ref.namespace}`;
|
|
34
|
+
if (seen.has(key)) continue;
|
|
35
|
+
seen.add(key);
|
|
36
|
+
|
|
37
|
+
findings.push(
|
|
38
|
+
makeFinding({
|
|
39
|
+
id: 'unknown-twig-namespace',
|
|
40
|
+
severity: 'warn',
|
|
41
|
+
filePath: twigFile,
|
|
42
|
+
line: ref.line,
|
|
43
|
+
message: `Twig namespace "@${ref.namespace}" is not configured in the normalized project structure.`,
|
|
44
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/project-structure.md#twig-namespaces',
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
for (const ref of findTwigIncludeSourceReferences(source)) {
|
|
50
|
+
if (!resolvesTwigReference(ref.value, twigFile, env)) {
|
|
51
|
+
findings.push(
|
|
52
|
+
makeFinding({
|
|
53
|
+
id: 'unresolved-twig-reference',
|
|
54
|
+
severity: 'warn',
|
|
55
|
+
filePath: twigFile,
|
|
56
|
+
line: ref.line,
|
|
57
|
+
message: `${ref.type}() reference "${ref.value}" could not be resolved from the normalized Twig roots.`,
|
|
58
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/storybook.md#include',
|
|
59
|
+
}),
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return findings.map((finding) => ({
|
|
66
|
+
...finding,
|
|
67
|
+
filePath: finding.filePath || resolve(projectDir, 'project.emulsify.json'),
|
|
68
|
+
}));
|
|
69
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Twig volume audit check.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { globSync } from 'glob';
|
|
7
|
+
import { directorySize } from '../../lib/fs.js';
|
|
8
|
+
import { makeFinding } from '../lib/findings.js';
|
|
9
|
+
import { DEFAULT_IGNORES, safeIsDirectory } from '../lib/files.js';
|
|
10
|
+
|
|
11
|
+
const TWIG_GLOB = '**/*.twig';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Audit Twig volume under Storybook roots.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} context - Audit context.
|
|
17
|
+
* @returns {object[]} Findings.
|
|
18
|
+
*/
|
|
19
|
+
export function auditTwigVolume(context) {
|
|
20
|
+
const { env, twigThreshold } = context;
|
|
21
|
+
const roots = Array.from(new Set(env.projectStructure?.twigRoots || []));
|
|
22
|
+
const twigFiles = new Set();
|
|
23
|
+
|
|
24
|
+
for (const root of roots) {
|
|
25
|
+
if (!safeIsDirectory(root)) continue;
|
|
26
|
+
for (const filePath of globSync(TWIG_GLOB, {
|
|
27
|
+
cwd: root,
|
|
28
|
+
absolute: true,
|
|
29
|
+
nodir: true,
|
|
30
|
+
ignore: DEFAULT_IGNORES,
|
|
31
|
+
})) {
|
|
32
|
+
twigFiles.add(resolve(filePath));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (twigFiles.size <= twigThreshold) return [];
|
|
37
|
+
|
|
38
|
+
const totalBytes = roots.reduce(
|
|
39
|
+
(total, root) => total + directorySize(root),
|
|
40
|
+
0,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return [
|
|
44
|
+
makeFinding({
|
|
45
|
+
id: 'large-twig-storybook-roots',
|
|
46
|
+
severity: 'info',
|
|
47
|
+
message: `${twigFiles.size} Twig files are under Storybook Twig roots. Eager Twig imports are reliable but can increase Storybook startup/build cost for large libraries.`,
|
|
48
|
+
details: [
|
|
49
|
+
`Approximate Twig root size: ${Math.round(totalBytes / 1024)} KB.`,
|
|
50
|
+
],
|
|
51
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/performance.md#storybook-twig-imports',
|
|
52
|
+
}),
|
|
53
|
+
];
|
|
54
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Webpack-era pattern audit check.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { safeExists } from '../../../config/vite/utils/fs-safe.js';
|
|
7
|
+
import { lineNumberAt } from '../../lib/text.js';
|
|
8
|
+
import { makeFinding } from '../lib/findings.js';
|
|
9
|
+
import { cachedReadFile, safeIsDirectory } from '../lib/files.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Audit Webpack-era files and code patterns.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} context - Audit context.
|
|
15
|
+
* @returns {object[]} Findings.
|
|
16
|
+
*/
|
|
17
|
+
export function auditWebpackPatterns(context) {
|
|
18
|
+
const { codeFiles, projectDir } = context;
|
|
19
|
+
const findings = [];
|
|
20
|
+
const webpackConfig = resolve(projectDir, '.storybook/webpack.config.js');
|
|
21
|
+
const webpackDir = resolve(projectDir, 'config/webpack');
|
|
22
|
+
|
|
23
|
+
if (safeExists(webpackConfig)) {
|
|
24
|
+
findings.push(
|
|
25
|
+
makeFinding({
|
|
26
|
+
id: 'webpack-config-file',
|
|
27
|
+
severity: 'warn',
|
|
28
|
+
filePath: webpackConfig,
|
|
29
|
+
message:
|
|
30
|
+
'Webpack-specific Storybook config is present and should be migrated to Vite/Storybook overrides.',
|
|
31
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/migration-4x.md#vite-customization',
|
|
32
|
+
}),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (safeIsDirectory(webpackDir)) {
|
|
37
|
+
findings.push(
|
|
38
|
+
makeFinding({
|
|
39
|
+
id: 'webpack-config-directory',
|
|
40
|
+
severity: 'warn',
|
|
41
|
+
filePath: webpackDir,
|
|
42
|
+
message:
|
|
43
|
+
'config/webpack exists. Webpack-specific customization should move to Vite plugins or extendConfig().',
|
|
44
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/extension-points.md#vite-plugins-and-config-patches',
|
|
45
|
+
}),
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const patterns = [
|
|
50
|
+
{
|
|
51
|
+
regex: /\brequire\.context\s*\(/,
|
|
52
|
+
message: 'require.context() is Webpack-specific and should be migrated.',
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
regex:
|
|
56
|
+
/\b(raw-loader|twig-loader|style-loader|file-loader|sass-loader)\b/,
|
|
57
|
+
message: 'Webpack loader references should be migrated to Vite plugins.',
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
regex: /from\s+['"][^'"]+![^'"]+['"]|import\s+['"][^'"]+![^'"]+['"]/,
|
|
61
|
+
message: 'Inline Webpack loader import syntax should be removed.',
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
for (const filePath of codeFiles) {
|
|
66
|
+
const source = cachedReadFile(filePath);
|
|
67
|
+
|
|
68
|
+
for (const pattern of patterns) {
|
|
69
|
+
const match = pattern.regex.exec(source);
|
|
70
|
+
if (!match) continue;
|
|
71
|
+
|
|
72
|
+
findings.push(
|
|
73
|
+
makeFinding({
|
|
74
|
+
id: 'webpack-era-pattern',
|
|
75
|
+
severity: 'warn',
|
|
76
|
+
filePath,
|
|
77
|
+
line: lineNumberAt(source, match.index || 0),
|
|
78
|
+
message: pattern.message,
|
|
79
|
+
docs: 'https://github.com/emulsify-ds/emulsify-core/blob/4.x/docs/migration-4x.md#vite-customization',
|
|
80
|
+
}),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return findings;
|
|
86
|
+
}
|