@emulsify/core 4.2.1 → 4.3.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-vite.js +91 -36
- package/README.md +185 -56
- package/config/a11y.config.js +2 -1
- package/config/vite/entries.js +4 -4
- package/config/vite/plugins/{copy-src-assets.js → assets/copy-src-assets.js} +1 -1
- package/config/vite/plugins/{copy-twig-files.js → assets/copy-twig-files.js} +1 -1
- 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 +366 -0
- package/config/vite/plugins/reporter/format.js +199 -0
- package/config/vite/plugins/reporter/index.js +308 -0
- package/config/vite/plugins/reporter/render.js +758 -0
- package/config/vite/plugins/reporter/sass-logger.js +237 -0
- package/config/vite/plugins/reporter/vite-logger.js +188 -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 +51 -5
- package/package.json +93 -58
- 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/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,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Audit orchestration and check registry.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { resolveProjectConfig } from '../../config/vite/project-config.js';
|
|
7
|
+
import { safeExists } from '../../config/vite/utils/fs-safe.js';
|
|
8
|
+
import { auditCoreImports } from './checks/core-imports.js';
|
|
9
|
+
import { auditCssAssetReferences } from './checks/css-asset-references.js';
|
|
10
|
+
import { auditDrupalAssumptions } from './checks/drupal-assumptions.js';
|
|
11
|
+
import { auditFilesOutsideRoots } from './checks/files-outside-roots.js';
|
|
12
|
+
import { auditGeneratedPackageScripts } from './checks/generated-package-scripts.js';
|
|
13
|
+
import { auditLegacyTwigStories } from './checks/legacy-twig-stories.js';
|
|
14
|
+
import { auditPackageOverrides } from './checks/package-overrides.js';
|
|
15
|
+
import { auditProjectConfig } from './checks/project-config.js';
|
|
16
|
+
import { auditStoryDiscovery } from './checks/story-discovery.js';
|
|
17
|
+
import { auditTwigReferences } from './checks/twig-references.js';
|
|
18
|
+
import { auditTwigVolume } from './checks/twig-volume.js';
|
|
19
|
+
import { auditWebpackPatterns } from './checks/webpack-patterns.js';
|
|
20
|
+
import {
|
|
21
|
+
collectRootedProjectFiles,
|
|
22
|
+
normalizeAuditRoots,
|
|
23
|
+
resetFileReadCache,
|
|
24
|
+
} from './lib/files.js';
|
|
25
|
+
|
|
26
|
+
const STORY_GLOB = '**/*.stories.{js,jsx,ts,tsx}';
|
|
27
|
+
const CODE_GLOB = '**/*.{js,jsx,ts,tsx,mjs,cjs}';
|
|
28
|
+
const TWIG_GLOB = '**/*.twig';
|
|
29
|
+
const STYLE_GLOB = '**/*.{css,scss,sass}';
|
|
30
|
+
|
|
31
|
+
export const DEFAULT_TWIG_THRESHOLD = 250;
|
|
32
|
+
|
|
33
|
+
export const auditChecks = [
|
|
34
|
+
auditProjectConfig,
|
|
35
|
+
auditPackageOverrides,
|
|
36
|
+
auditGeneratedPackageScripts,
|
|
37
|
+
auditStoryDiscovery,
|
|
38
|
+
auditLegacyTwigStories,
|
|
39
|
+
auditTwigReferences,
|
|
40
|
+
auditCssAssetReferences,
|
|
41
|
+
auditWebpackPatterns,
|
|
42
|
+
auditCoreImports,
|
|
43
|
+
auditDrupalAssumptions,
|
|
44
|
+
auditFilesOutsideRoots,
|
|
45
|
+
auditTwigVolume,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normalize the project config, retaining any resolution failure.
|
|
50
|
+
*
|
|
51
|
+
* @param {string} projectDir - Absolute project root.
|
|
52
|
+
* @returns {{env: object, configExists: boolean, error?: Error}}
|
|
53
|
+
*/
|
|
54
|
+
function resolveAuditEnvironment(projectDir) {
|
|
55
|
+
const configExists = safeExists(resolve(projectDir, 'project.emulsify.json'));
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
return {
|
|
59
|
+
env: resolveProjectConfig(projectDir, process.env),
|
|
60
|
+
configExists,
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
return {
|
|
64
|
+
env: {
|
|
65
|
+
projectDir,
|
|
66
|
+
platform: 'none',
|
|
67
|
+
namespaceRoots: {},
|
|
68
|
+
projectStructure: {},
|
|
69
|
+
},
|
|
70
|
+
configExists,
|
|
71
|
+
error,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build the shared context passed to every audit check.
|
|
78
|
+
*
|
|
79
|
+
* @param {{projectDir?: string, twigThreshold?: number}} [options={}] - Options.
|
|
80
|
+
* @returns {object} Audit context.
|
|
81
|
+
*/
|
|
82
|
+
export function createAuditContext(options = {}) {
|
|
83
|
+
const projectDir = resolve(options.projectDir || process.cwd());
|
|
84
|
+
const envResult = resolveAuditEnvironment(projectDir);
|
|
85
|
+
const structure = envResult.env.projectStructure || {};
|
|
86
|
+
const sourceRoots = normalizeAuditRoots(
|
|
87
|
+
projectDir,
|
|
88
|
+
structure.sourceRoots || [],
|
|
89
|
+
);
|
|
90
|
+
const storyRoots = normalizeAuditRoots(
|
|
91
|
+
projectDir,
|
|
92
|
+
structure.storyRoots || sourceRoots,
|
|
93
|
+
);
|
|
94
|
+
const twigRoots = normalizeAuditRoots(
|
|
95
|
+
projectDir,
|
|
96
|
+
structure.twigRoots || sourceRoots,
|
|
97
|
+
);
|
|
98
|
+
const storyFiles = collectRootedProjectFiles(
|
|
99
|
+
projectDir,
|
|
100
|
+
STORY_GLOB,
|
|
101
|
+
storyRoots,
|
|
102
|
+
);
|
|
103
|
+
const codeFiles = collectRootedProjectFiles(
|
|
104
|
+
projectDir,
|
|
105
|
+
CODE_GLOB,
|
|
106
|
+
sourceRoots,
|
|
107
|
+
);
|
|
108
|
+
const twigFiles = collectRootedProjectFiles(projectDir, TWIG_GLOB, twigRoots);
|
|
109
|
+
const styleFiles = collectRootedProjectFiles(
|
|
110
|
+
projectDir,
|
|
111
|
+
STYLE_GLOB,
|
|
112
|
+
sourceRoots,
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
...envResult,
|
|
117
|
+
projectDir,
|
|
118
|
+
sourceRoots,
|
|
119
|
+
storyRoots,
|
|
120
|
+
twigRoots,
|
|
121
|
+
storyFiles,
|
|
122
|
+
codeFiles,
|
|
123
|
+
twigFiles,
|
|
124
|
+
styleFiles,
|
|
125
|
+
twigThreshold: Number.isFinite(options.twigThreshold)
|
|
126
|
+
? options.twigThreshold
|
|
127
|
+
: DEFAULT_TWIG_THRESHOLD,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Run all registered checks against a prepared audit context.
|
|
133
|
+
*
|
|
134
|
+
* @param {object} context - Audit context.
|
|
135
|
+
* @returns {object[]} Findings.
|
|
136
|
+
*/
|
|
137
|
+
export function runAuditChecks(context) {
|
|
138
|
+
return auditChecks.flatMap((check) => check(context));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Run the combined Emulsify audit.
|
|
143
|
+
*
|
|
144
|
+
* @param {{projectDir?: string, twigThreshold?: number}} [options={}] - Options.
|
|
145
|
+
* @returns {{projectDir: string, summary: object, files: object, findings: object[]}} Audit result.
|
|
146
|
+
*/
|
|
147
|
+
export function runAudits(options = {}) {
|
|
148
|
+
resetFileReadCache();
|
|
149
|
+
|
|
150
|
+
const context = createAuditContext(options);
|
|
151
|
+
const findings = runAuditChecks(context);
|
|
152
|
+
const summary = findings.reduce(
|
|
153
|
+
(totals, finding) => ({
|
|
154
|
+
...totals,
|
|
155
|
+
[finding.severity]: (totals[finding.severity] || 0) + 1,
|
|
156
|
+
}),
|
|
157
|
+
{
|
|
158
|
+
error: 0,
|
|
159
|
+
warn: 0,
|
|
160
|
+
info: 0,
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
projectDir: context.projectDir,
|
|
166
|
+
summary,
|
|
167
|
+
files: {
|
|
168
|
+
stories: context.storyFiles.length,
|
|
169
|
+
twig: context.twigFiles.length,
|
|
170
|
+
code: context.codeFiles.length,
|
|
171
|
+
styles: context.styleFiles.length,
|
|
172
|
+
},
|
|
173
|
+
findings,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export { runAudits as auditProject };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file CSS and Sass URL parsing helpers for the project audit.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { basename, dirname, resolve } from 'node:path';
|
|
6
|
+
import {
|
|
7
|
+
compiledAssetOutputPath,
|
|
8
|
+
storybookStyleOutputPath,
|
|
9
|
+
} from '../../../config/vite/project-structure.js';
|
|
10
|
+
import { lineNumberAt } from '../../lib/text.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract simple same-file Sass string variables.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} source - Stylesheet source.
|
|
16
|
+
* @returns {Map<string, string>} Variable value map.
|
|
17
|
+
*/
|
|
18
|
+
function findSassStringVariables(source) {
|
|
19
|
+
const variables = new Map();
|
|
20
|
+
const pattern = /^\s*\$([\w-]+)\s*:\s*(['"])(.*?)\2\s*;?/gm;
|
|
21
|
+
|
|
22
|
+
for (const match of source.matchAll(pattern)) {
|
|
23
|
+
variables.set(match[1], match[3]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return variables;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve same-file Sass variable interpolation in a URL value.
|
|
31
|
+
*
|
|
32
|
+
* This intentionally handles only simple string variables. It is enough to make
|
|
33
|
+
* common asset roots such as `#{$font-url}/Avenir.woff2` auditable without
|
|
34
|
+
* pretending to be a Sass compiler.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} value - Raw URL value.
|
|
37
|
+
* @param {Map<string, string>} variables - Sass variable map.
|
|
38
|
+
* @returns {string} URL value with known interpolations expanded.
|
|
39
|
+
*/
|
|
40
|
+
function resolveSassUrlValue(value, variables) {
|
|
41
|
+
return value.replace(/#\{\$([\w-]+)\}/g, (match, name) =>
|
|
42
|
+
variables.has(name) ? variables.get(name) : match,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
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
|
+
/**
|
|
61
|
+
* Extract URL references from CSS or Sass source.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} source - Stylesheet source.
|
|
64
|
+
* @returns {{value: string, raw: string, line: number}[]} URL references.
|
|
65
|
+
*/
|
|
66
|
+
export function findCssUrlReferences(source) {
|
|
67
|
+
const scanSource = maskStyleComments(source);
|
|
68
|
+
const variables = findSassStringVariables(scanSource);
|
|
69
|
+
const references = [];
|
|
70
|
+
const pattern = /url\(\s*(?:(['"])(.*?)\1|([^'")][^)]*?))\s*\)/g;
|
|
71
|
+
|
|
72
|
+
for (const match of scanSource.matchAll(pattern)) {
|
|
73
|
+
const raw = (match[2] ?? match[3] ?? '').trim();
|
|
74
|
+
const value = resolveSassUrlValue(raw, variables).trim();
|
|
75
|
+
|
|
76
|
+
references.push({
|
|
77
|
+
value,
|
|
78
|
+
raw,
|
|
79
|
+
line: lineNumberAt(source, match.index || 0),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return references;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Determine whether a CSS URL should be skipped by filesystem checks.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} value - URL value.
|
|
90
|
+
* @returns {boolean} TRUE when the URL is not a local relative asset path.
|
|
91
|
+
*/
|
|
92
|
+
export function isNonFilesystemCssUrl(value) {
|
|
93
|
+
return (
|
|
94
|
+
!value ||
|
|
95
|
+
value.startsWith('#') ||
|
|
96
|
+
value.startsWith('/') ||
|
|
97
|
+
value.startsWith('//') ||
|
|
98
|
+
value.startsWith('$') ||
|
|
99
|
+
value.startsWith('#{') ||
|
|
100
|
+
/^[a-z][a-z0-9+.-]*:/i.test(value) ||
|
|
101
|
+
/^var\(/i.test(value) ||
|
|
102
|
+
/^env\(/i.test(value)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Remove query string and hash suffixes from a URL path.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} value - URL value.
|
|
110
|
+
* @returns {string} Path portion.
|
|
111
|
+
*/
|
|
112
|
+
export function cssUrlPath(value) {
|
|
113
|
+
return value.split(/[?#]/)[0];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve an emitted CSS output key to the actual CSS file path.
|
|
118
|
+
*
|
|
119
|
+
* Vite entry keys use `__style` internally to avoid JS/CSS collisions. The
|
|
120
|
+
* shared Vite config removes that suffix from emitted CSS file names.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} key - Output key without extension.
|
|
123
|
+
* @returns {string} Emitted CSS file path relative to output root.
|
|
124
|
+
*/
|
|
125
|
+
function emittedCssRelativePath(key) {
|
|
126
|
+
return `${key.replace(/__style$/i, '')}.css`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Return possible runtime directories for a style file's emitted CSS.
|
|
131
|
+
*
|
|
132
|
+
* @param {string} filePath - Source stylesheet.
|
|
133
|
+
* @param {object} env - Normalized environment.
|
|
134
|
+
* @param {string} projectDir - Project root.
|
|
135
|
+
* @returns {string[]} Absolute runtime directories.
|
|
136
|
+
*/
|
|
137
|
+
export function styleRuntimeDirectories(filePath, env, projectDir) {
|
|
138
|
+
if (!/\.(scss|sass|css)$/i.test(filePath)) return [];
|
|
139
|
+
if (basename(filePath).startsWith('_')) return [];
|
|
140
|
+
|
|
141
|
+
const structure = env.projectStructure || {};
|
|
142
|
+
if (!structure.output) return [];
|
|
143
|
+
|
|
144
|
+
const ctx = {
|
|
145
|
+
projectDir,
|
|
146
|
+
srcDir: env.srcDir || resolve(projectDir, 'src'),
|
|
147
|
+
SDC: Boolean(env.SDC),
|
|
148
|
+
};
|
|
149
|
+
const fileName = basename(filePath);
|
|
150
|
+
const isStorybookStyle = /^(cl-|sb-)/.test(fileName);
|
|
151
|
+
const key = isStorybookStyle
|
|
152
|
+
? storybookStyleOutputPath(filePath, structure, ctx)
|
|
153
|
+
: compiledAssetOutputPath(filePath, 'css', structure, ctx);
|
|
154
|
+
|
|
155
|
+
if (!key) return [];
|
|
156
|
+
|
|
157
|
+
const relCss = emittedCssRelativePath(key);
|
|
158
|
+
const directories = [dirname(resolve(projectDir, 'dist', relCss))];
|
|
159
|
+
|
|
160
|
+
if (structure.mirrorComponentOutput && relCss.startsWith('components/')) {
|
|
161
|
+
directories.push(dirname(resolve(projectDir, relCss)));
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return Array.from(new Set(directories));
|
|
165
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Filesystem helpers for the project audit.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { lstatSync } from 'node:fs';
|
|
6
|
+
import { relative, resolve, sep } from 'node:path';
|
|
7
|
+
import { globSync } from 'glob';
|
|
8
|
+
import {
|
|
9
|
+
safeExists,
|
|
10
|
+
safeReadFile,
|
|
11
|
+
} from '../../../config/vite/utils/fs-safe.js';
|
|
12
|
+
|
|
13
|
+
export const DEFAULT_IGNORES = [
|
|
14
|
+
'**/.coverage/**',
|
|
15
|
+
'**/.git/**',
|
|
16
|
+
'**/.github/**',
|
|
17
|
+
'**/.out/**',
|
|
18
|
+
'**/dist/**',
|
|
19
|
+
'**/*.min.css',
|
|
20
|
+
'**/*.test.{js,jsx,ts,tsx,mjs,cjs}',
|
|
21
|
+
'**/node_modules/**',
|
|
22
|
+
'**/scripts/audit.js',
|
|
23
|
+
'**/vendor/**',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Cache source file reads for one top-level audit run.
|
|
28
|
+
*
|
|
29
|
+
* @type {Map<string, string|null>}
|
|
30
|
+
*/
|
|
31
|
+
const fileReadCache = new Map();
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Clear the per-run source file read cache.
|
|
35
|
+
*
|
|
36
|
+
* @returns {void}
|
|
37
|
+
*/
|
|
38
|
+
export function resetFileReadCache() {
|
|
39
|
+
fileReadCache.clear();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read a text source file once per top-level audit run.
|
|
44
|
+
*
|
|
45
|
+
* Missing files are cached as null internally but still return an empty string
|
|
46
|
+
* to preserve safeReadFile() behavior for existing checks.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} filePath - Absolute or relative file path.
|
|
49
|
+
* @returns {string} File contents, or an empty string when unavailable.
|
|
50
|
+
*/
|
|
51
|
+
export function cachedReadFile(filePath) {
|
|
52
|
+
const absPath = resolve(filePath);
|
|
53
|
+
if (fileReadCache.has(absPath)) {
|
|
54
|
+
return fileReadCache.get(absPath) ?? '';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const source = safeReadFile(absPath);
|
|
58
|
+
const cachedSource = source === '' && !safeExists(absPath) ? null : source;
|
|
59
|
+
fileReadCache.set(absPath, cachedSource);
|
|
60
|
+
|
|
61
|
+
return cachedSource ?? '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Determine whether a candidate is a directory.
|
|
66
|
+
*
|
|
67
|
+
* @param {string} filePath - Absolute path.
|
|
68
|
+
* @returns {boolean} TRUE when the path is a directory.
|
|
69
|
+
*/
|
|
70
|
+
export function safeIsDirectory(filePath) {
|
|
71
|
+
try {
|
|
72
|
+
return lstatSync(filePath).isDirectory();
|
|
73
|
+
} catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Collect files from a project.
|
|
80
|
+
*
|
|
81
|
+
* @param {string} projectDir - Absolute project root.
|
|
82
|
+
* @param {string|string[]} patterns - Glob pattern or patterns.
|
|
83
|
+
* @returns {string[]} Absolute file paths.
|
|
84
|
+
*/
|
|
85
|
+
export function collectProjectFiles(projectDir, patterns) {
|
|
86
|
+
return globSync(patterns, {
|
|
87
|
+
cwd: projectDir,
|
|
88
|
+
nodir: true,
|
|
89
|
+
absolute: true,
|
|
90
|
+
ignore: DEFAULT_IGNORES,
|
|
91
|
+
})
|
|
92
|
+
.map((filePath) => resolve(filePath))
|
|
93
|
+
.sort();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Return a normalized, project-contained root list.
|
|
98
|
+
*
|
|
99
|
+
* @param {string} projectDir - Absolute project root.
|
|
100
|
+
* @param {string[]} roots - Absolute candidate roots.
|
|
101
|
+
* @returns {string[]} Existing roots inside the project.
|
|
102
|
+
*/
|
|
103
|
+
export function normalizeAuditRoots(projectDir, roots = []) {
|
|
104
|
+
const resolvedProject = resolve(projectDir);
|
|
105
|
+
|
|
106
|
+
return Array.from(
|
|
107
|
+
new Set(
|
|
108
|
+
roots
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
.map((root) => resolve(root))
|
|
111
|
+
.filter(
|
|
112
|
+
(root) =>
|
|
113
|
+
isSameOrInside(root, resolvedProject) && safeIsDirectory(root),
|
|
114
|
+
),
|
|
115
|
+
),
|
|
116
|
+
).sort();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Collect files from normalized audit roots only.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} projectDir - Absolute project root.
|
|
123
|
+
* @param {string|string[]} patterns - Glob pattern or patterns.
|
|
124
|
+
* @param {string[]} roots - Absolute roots to scan.
|
|
125
|
+
* @returns {string[]} Absolute file paths.
|
|
126
|
+
*/
|
|
127
|
+
export function collectRootedProjectFiles(projectDir, patterns, roots = []) {
|
|
128
|
+
const files = new Set();
|
|
129
|
+
|
|
130
|
+
for (const root of normalizeAuditRoots(projectDir, roots)) {
|
|
131
|
+
for (const filePath of globSync(patterns, {
|
|
132
|
+
cwd: root,
|
|
133
|
+
nodir: true,
|
|
134
|
+
absolute: true,
|
|
135
|
+
ignore: DEFAULT_IGNORES,
|
|
136
|
+
})) {
|
|
137
|
+
files.add(resolve(filePath));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return Array.from(files).sort();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Determine whether a file is inside one of the roots.
|
|
146
|
+
*
|
|
147
|
+
* @param {string} filePath - Absolute file path.
|
|
148
|
+
* @param {string[]} roots - Absolute roots.
|
|
149
|
+
* @returns {boolean} TRUE when inside a root.
|
|
150
|
+
*/
|
|
151
|
+
export function isInsideAnyRoot(filePath, roots = []) {
|
|
152
|
+
return roots.some((root) => {
|
|
153
|
+
const rel = relative(root, filePath);
|
|
154
|
+
return Boolean(rel) && !rel.startsWith('..') && !rel.includes(`..${sep}`);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Determine whether a path is the same as, or inside, a root directory.
|
|
160
|
+
*
|
|
161
|
+
* @param {string} filePath - Absolute file path.
|
|
162
|
+
* @param {string} root - Absolute root path.
|
|
163
|
+
* @returns {boolean} TRUE when the path is inside or equal to the root.
|
|
164
|
+
*/
|
|
165
|
+
export function isSameOrInside(filePath, root) {
|
|
166
|
+
const rel = relative(root, filePath);
|
|
167
|
+
return !rel || (!rel.startsWith('..') && !rel.includes(`..${sep}`));
|
|
168
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Finding and report-path helpers for the project audit.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { relative } from 'node:path';
|
|
6
|
+
import { toPosixPath } from '../../../config/vite/utils/paths.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Return a project-relative path for report output.
|
|
10
|
+
*
|
|
11
|
+
* @param {string} projectDir - Absolute project root.
|
|
12
|
+
* @param {string} filePath - Absolute file path.
|
|
13
|
+
* @returns {string} Project-relative POSIX path.
|
|
14
|
+
*/
|
|
15
|
+
export function displayPath(projectDir, filePath) {
|
|
16
|
+
return toPosixPath(relative(projectDir, filePath));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build a report finding.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} finding - Finding details.
|
|
23
|
+
* @returns {object} Normalized finding.
|
|
24
|
+
*/
|
|
25
|
+
export function makeFinding(finding) {
|
|
26
|
+
return {
|
|
27
|
+
severity: 'warn',
|
|
28
|
+
docs: undefined,
|
|
29
|
+
...finding,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Package manifest helpers for the project audit.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Return a nested object value.
|
|
7
|
+
*
|
|
8
|
+
* @param {object} obj - Object to inspect.
|
|
9
|
+
* @param {string[]} pathParts - Nested object path.
|
|
10
|
+
* @returns {*} Nested value.
|
|
11
|
+
*/
|
|
12
|
+
export function valueAtPath(obj, pathParts) {
|
|
13
|
+
return pathParts.reduce(
|
|
14
|
+
(current, key) =>
|
|
15
|
+
current && typeof current === 'object' ? current[key] : undefined,
|
|
16
|
+
obj,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Determine whether a package manifest depends on Emulsify Core.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} packageJson - Parsed package.json.
|
|
24
|
+
* @returns {boolean} TRUE when package.json is Core or consumes Core.
|
|
25
|
+
*/
|
|
26
|
+
export function packageUsesEmulsifyCore(packageJson = {}) {
|
|
27
|
+
if (packageJson.name === '@emulsify/core') {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return [
|
|
32
|
+
'dependencies',
|
|
33
|
+
'devDependencies',
|
|
34
|
+
'peerDependencies',
|
|
35
|
+
'optionalDependencies',
|
|
36
|
+
].some((section) =>
|
|
37
|
+
Object.prototype.hasOwnProperty.call(
|
|
38
|
+
packageJson[section] || {},
|
|
39
|
+
'@emulsify/core',
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Determine whether a package manifest is Emulsify Core itself.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} packageJson - Parsed package.json.
|
|
48
|
+
* @returns {boolean} TRUE when package.json is Core.
|
|
49
|
+
*/
|
|
50
|
+
export function packageIsEmulsifyCore(packageJson = {}) {
|
|
51
|
+
return packageJson.name === '@emulsify/core';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Determine whether a recommended override is already present.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} overrides - package.json overrides object.
|
|
58
|
+
* @param {{paths: string[][]}} recommendation - Override recommendation.
|
|
59
|
+
* @returns {boolean} TRUE when any equivalent override path exists.
|
|
60
|
+
*/
|
|
61
|
+
export function hasRecommendedOverride(overrides = {}, recommendation) {
|
|
62
|
+
return recommendation.paths.some(
|
|
63
|
+
(pathParts) => valueAtPath(overrides, pathParts) !== undefined,
|
|
64
|
+
);
|
|
65
|
+
}
|