@emulsify/core 3.5.0 → 4.0.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/.cli/init.js +40 -31
- package/.storybook/_drupal.js +129 -8
- package/.storybook/css-components.js +13 -0
- package/.storybook/css-dist.js +5 -0
- package/.storybook/emulsifyTheme.js +9 -6
- package/.storybook/main.js +397 -106
- package/.storybook/manager.js +9 -16
- package/.storybook/preview.js +88 -110
- package/.storybook/utils.js +69 -74
- package/README.md +110 -59
- package/config/.stylelintrc.json +2 -6
- package/config/a11y.config.js +9 -5
- package/config/babel.config.js +6 -11
- package/config/eslint.config.js +31 -3
- package/config/postcss.config.js +5 -0
- package/config/vite/entries.js +227 -0
- package/config/vite/environment.js +39 -0
- package/config/vite/platforms.js +70 -0
- package/config/vite/plugins/copy-src-assets.js +76 -0
- package/config/vite/plugins/copy-twig-files.js +84 -0
- package/config/vite/plugins/css-asset-relativizer.js +40 -0
- package/config/vite/plugins/index.js +105 -0
- package/config/vite/plugins/mirror-components.js +358 -0
- package/config/vite/plugins/require-context.js +311 -0
- package/config/vite/plugins/source-file-index.js +184 -0
- package/config/vite/plugins/svg-sprite.js +117 -0
- package/config/vite/plugins/twig-extension-installers.js +36 -0
- package/config/vite/plugins/twig-module.js +1251 -0
- package/config/vite/plugins/virtual-twig-asset-sources.js +404 -0
- package/config/vite/plugins/virtual-twig-globs.js +136 -0
- package/config/vite/plugins/vituum-patch.js +167 -0
- package/config/vite/plugins/yaml-module.js +133 -0
- package/config/vite/plugins.js +12 -0
- package/config/vite/project-config.js +192 -0
- package/config/vite/project-extensions.js +177 -0
- package/config/vite/project-structure.js +447 -0
- package/config/vite/twig-extensions.js +109 -0
- package/config/vite/utils/fs-safe.js +66 -0
- package/config/vite/utils/paths.js +40 -0
- package/config/vite/utils/react-singleton.js +85 -0
- package/config/vite/utils/unique.js +36 -0
- package/config/vite/vite.config.js +161 -0
- package/package.json +164 -75
- package/scripts/a11y.js +70 -16
- package/scripts/audit-twig-stories.js +378 -0
- package/scripts/audit.js +1602 -0
- package/scripts/check-node-version.js +18 -0
- package/scripts/loadYaml.js +5 -1
- package/src/extensions/index.js +8 -0
- package/src/extensions/react/index.js +12 -0
- package/src/extensions/react/register.js +45 -0
- package/src/extensions/shared/attributes.js +308 -0
- package/src/extensions/shared/html.js +41 -0
- package/src/extensions/shared/lists.js +38 -0
- package/src/extensions/shared/object.js +22 -0
- package/src/extensions/twig/function-map.js +20 -0
- package/src/extensions/twig/functions/add-attributes.js +39 -0
- package/src/extensions/twig/functions/bem.js +166 -0
- package/src/extensions/twig/index.js +13 -0
- package/src/extensions/twig/register.js +95 -0
- package/src/extensions/twig/tag-map.js +16 -0
- package/src/extensions/twig/tags/switch.js +266 -0
- package/src/storybook/index.js +14 -0
- package/src/storybook/main-config.js +132 -0
- package/src/storybook/platform-behaviors.js +60 -0
- package/src/storybook/preview-parameters.js +81 -0
- package/src/storybook/render-twig.js +295 -0
- package/src/storybook/twig/drupal-filters.js +7 -0
- package/src/storybook/twig/include-function.js +109 -0
- package/src/storybook/twig/include.js +28 -0
- package/src/storybook/twig/reference-paths.js +294 -0
- package/src/storybook/twig/resolver.js +318 -0
- package/src/storybook/twig/setup.js +39 -0
- package/src/storybook/twig/source-events.js +5 -0
- package/src/storybook/twig/source-extensions.js +24 -0
- package/src/storybook/twig/source-function.js +239 -0
- package/src/storybook/twig/source.js +39 -0
- package/.all-contributorsrc +0 -45
- package/.editorconfig +0 -5
- package/.github/ISSUE_TEMPLATE/BUG_REPORT_TEMPLATE.md +0 -18
- package/.github/ISSUE_TEMPLATE/FEATURE_REQUEST_TEMPLATE.md +0 -11
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
- package/.github/dependabot.yml +0 -6
- package/.github/workflows/addtoprojects.yml +0 -21
- package/.github/workflows/contributors.yml +0 -37
- package/.github/workflows/lint.yml +0 -22
- package/.github/workflows/semantic-release.yml +0 -24
- package/.husky/commit-msg +0 -2
- package/.husky/pre-commit +0 -2
- package/.nvmrc +0 -1
- package/.prettierignore +0 -4
- package/.storybook/polyfills/twig-include.js +0 -40
- package/.storybook/polyfills/twig-resolver.js +0 -70
- package/.storybook/polyfills/twig-source.js +0 -65
- package/.storybook/webpack.config.js +0 -269
- package/CODE_OF_CONDUCT.md +0 -56
- package/commitlint.config.js +0 -5
- package/config/jest.config.js +0 -19
- package/config/webpack/app.js +0 -1
- package/config/webpack/loaders.js +0 -167
- package/config/webpack/optimizers.js +0 -26
- package/config/webpack/plugins.js +0 -283
- package/config/webpack/resolves.js +0 -157
- package/config/webpack/sdc-loader.js +0 -16
- package/config/webpack/webpack.common.js +0 -272
- package/config/webpack/webpack.dev.js +0 -41
- package/config/webpack/webpack.prod.js +0 -6
- package/release.config.cjs +0 -30
- package/scripts/a11y.test.js +0 -172
- package/scripts/loadYaml.test.js +0 -30
package/scripts/a11y.js
CHANGED
|
@@ -5,25 +5,74 @@
|
|
|
5
5
|
* and reports issues.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
import { existsSync } from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
11
|
+
import * as R from 'ramda';
|
|
12
|
+
import pa11y from 'pa11y';
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import a11yConfig from '../config/a11y.config.js';
|
|
15
|
+
|
|
16
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
17
|
+
const __dirname = path.dirname(__filename);
|
|
18
|
+
|
|
19
|
+
const { storybookBuildDir, pa11y: pa11yConfig } = a11yConfig;
|
|
16
20
|
|
|
17
21
|
// Project-specific configuration.
|
|
18
|
-
|
|
19
|
-
ignore,
|
|
20
|
-
components,
|
|
21
|
-
} = require('../../../config/emulsify-core/a11y.config.js');
|
|
22
|
+
let { ignore = {}, components = [] } = a11yConfig;
|
|
22
23
|
|
|
23
24
|
/** Absolute path to Storybook build directory. */
|
|
24
25
|
const STORYBOOK_BUILD_DIR = path.resolve(__dirname, '../', storybookBuildDir);
|
|
25
26
|
/** Absolute path to Storybook iframe file used for per-story rendering. */
|
|
26
27
|
const STORYBOOK_IFRAME = path.join(STORYBOOK_BUILD_DIR, 'iframe.html');
|
|
28
|
+
/** Project-specific accessibility config path used by generated themes. */
|
|
29
|
+
const PROJECT_A11Y_CONFIG = path.resolve(
|
|
30
|
+
__dirname,
|
|
31
|
+
'../../../config/emulsify-core/a11y.config.js',
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Load project-specific accessibility config when a consuming project provides one.
|
|
36
|
+
*
|
|
37
|
+
* @returns {Promise<object>} Project accessibility config, when present.
|
|
38
|
+
*/
|
|
39
|
+
const loadProjectA11yConfig = async () => {
|
|
40
|
+
if (!existsSync(PROJECT_A11Y_CONFIG)) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const configModule = await import(pathToFileURL(PROJECT_A11Y_CONFIG).href);
|
|
45
|
+
return configModule.default || configModule;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Apply project-specific a11y config values over shared defaults.
|
|
50
|
+
*
|
|
51
|
+
* @param {{ignore?: object, components?: string[]}} config - Project config.
|
|
52
|
+
* @returns {void}
|
|
53
|
+
*/
|
|
54
|
+
const applyProjectA11yConfig = (config = {}) => {
|
|
55
|
+
ignore = config.ignore || ignore;
|
|
56
|
+
components = config.components || components;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Print CLI help.
|
|
61
|
+
*
|
|
62
|
+
* @returns {void}
|
|
63
|
+
*/
|
|
64
|
+
const printHelp = () => {
|
|
65
|
+
// eslint-disable-next-line no-console
|
|
66
|
+
console.log(
|
|
67
|
+
[
|
|
68
|
+
'Usage: node scripts/a11y.js [options]',
|
|
69
|
+
'',
|
|
70
|
+
'Options:',
|
|
71
|
+
' -r Run pa11y against configured Storybook component IDs.',
|
|
72
|
+
' -h, --help Print this help text.',
|
|
73
|
+
].join('\n'),
|
|
74
|
+
);
|
|
75
|
+
};
|
|
27
76
|
|
|
28
77
|
/**
|
|
29
78
|
* Map pa11y/axe severity to a label (historically a color name).
|
|
@@ -56,7 +105,8 @@ const severityToColor = R.cond([
|
|
|
56
105
|
const issueIsValid = (issue) => {
|
|
57
106
|
const code = issue?.code;
|
|
58
107
|
const description = issue?.runnerExtras?.description;
|
|
59
|
-
const codeIgnored =
|
|
108
|
+
const codeIgnored =
|
|
109
|
+
Array.isArray(ignore?.codes) && ignore.codes.includes(code);
|
|
60
110
|
const descIgnored =
|
|
61
111
|
description &&
|
|
62
112
|
Array.isArray(ignore?.descriptions) &&
|
|
@@ -137,12 +187,16 @@ const lintReportAndExit = R.pipe(
|
|
|
137
187
|
|
|
138
188
|
// Only perform linting/reporting when instructed via "-r".
|
|
139
189
|
/* istanbul ignore next */
|
|
140
|
-
if (R.
|
|
141
|
-
|
|
142
|
-
|
|
190
|
+
if (R.includes(process.argv[2], ['-h', '--help'])) {
|
|
191
|
+
printHelp();
|
|
192
|
+
} else if (R.pathEq(['argv', 2], '-r')(process)) {
|
|
193
|
+
loadProjectA11yConfig().then((projectConfig) => {
|
|
194
|
+
applyProjectA11yConfig(projectConfig);
|
|
195
|
+
return lintReportAndExit(components);
|
|
196
|
+
});
|
|
143
197
|
}
|
|
144
198
|
|
|
145
|
-
|
|
199
|
+
export {
|
|
146
200
|
severityToColor,
|
|
147
201
|
issueIsValid,
|
|
148
202
|
logIssue,
|
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @file Report legacy Twig Storybook stories that should migrate to renderTwig().
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
8
|
+
import { relative, resolve } from 'node:path';
|
|
9
|
+
import { globSync } from 'glob';
|
|
10
|
+
import { resolveProjectConfig } from '../config/vite/project-config.js';
|
|
11
|
+
import { toPosixPath } from '../config/vite/utils/paths.js';
|
|
12
|
+
|
|
13
|
+
const STORY_GLOB = '**/*.stories.{js,jsx,ts,tsx}';
|
|
14
|
+
const IDENTIFIER_PATTERN = '[A-Za-z_$][\\w$]*';
|
|
15
|
+
const DEFAULT_IGNORES = [
|
|
16
|
+
'**/node_modules/**',
|
|
17
|
+
'**/dist/**',
|
|
18
|
+
'**/.out/**',
|
|
19
|
+
'**/.coverage/**',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Escape a string for use inside a regular expression.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} value - Raw string.
|
|
26
|
+
* @returns {string} Escaped string.
|
|
27
|
+
*/
|
|
28
|
+
function escapeRegExp(value) {
|
|
29
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Find the 1-based line number for a character index.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} source - File source.
|
|
36
|
+
* @param {number} index - Character index.
|
|
37
|
+
* @returns {number} 1-based line number.
|
|
38
|
+
*/
|
|
39
|
+
function lineNumberAt(source, index) {
|
|
40
|
+
return source.slice(0, index).split('\n').length;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Find imported Twig template identifiers.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} source - Story source.
|
|
47
|
+
* @returns {{name: string, specifier: string, line: number}[]} Twig imports.
|
|
48
|
+
*/
|
|
49
|
+
export function findTwigImports(source) {
|
|
50
|
+
const imports = [];
|
|
51
|
+
const patterns = [
|
|
52
|
+
new RegExp(
|
|
53
|
+
`import\\s+(${IDENTIFIER_PATTERN})\\s+from\\s+['"]([^'"]+\\.twig(?:\\?[^'"]*)?)['"]`,
|
|
54
|
+
'g',
|
|
55
|
+
),
|
|
56
|
+
new RegExp(
|
|
57
|
+
`(?:const|let|var)\\s+(${IDENTIFIER_PATTERN})\\s*=\\s*require\\(\\s*['"]([^'"]+\\.twig(?:\\?[^'"]*)?)['"]\\s*\\)`,
|
|
58
|
+
'g',
|
|
59
|
+
),
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
for (const pattern of patterns) {
|
|
63
|
+
for (const match of source.matchAll(pattern)) {
|
|
64
|
+
imports.push({
|
|
65
|
+
name: match[1],
|
|
66
|
+
specifier: match[2],
|
|
67
|
+
line: lineNumberAt(source, match.index || 0),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return imports;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Determine whether a story imports renderTwig from Emulsify Core.
|
|
77
|
+
*
|
|
78
|
+
* @param {string} source - Story source.
|
|
79
|
+
* @returns {boolean} TRUE when renderTwig is imported from the public helper.
|
|
80
|
+
*/
|
|
81
|
+
export function importsRenderTwig(source) {
|
|
82
|
+
return (
|
|
83
|
+
/\brenderTwig\b/.test(source) &&
|
|
84
|
+
/from\s+['"]@emulsify\/core\/storybook['"]/.test(source)
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Find likely direct returns of imported Twig template functions.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} source - Story source.
|
|
92
|
+
* @param {string[]} templateNames - Imported Twig template identifiers.
|
|
93
|
+
* @returns {{name: string, line: number}[]} Direct template calls.
|
|
94
|
+
*/
|
|
95
|
+
export function findDirectTemplateReturns(source, templateNames = []) {
|
|
96
|
+
const calls = [];
|
|
97
|
+
|
|
98
|
+
for (const templateName of templateNames) {
|
|
99
|
+
const pattern = new RegExp(
|
|
100
|
+
`(?:return\\s+|=>\\s*(?:\\(\\s*)?)${escapeRegExp(templateName)}\\s*\\(`,
|
|
101
|
+
'g',
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
for (const match of source.matchAll(pattern)) {
|
|
105
|
+
calls.push({
|
|
106
|
+
name: templateName,
|
|
107
|
+
line: lineNumberAt(source, match.index || 0),
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return calls;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Analyze one Storybook story source string.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} source - Story source.
|
|
119
|
+
* @param {string} [filePath=''] - Story file path.
|
|
120
|
+
* @returns {object} Story analysis.
|
|
121
|
+
*/
|
|
122
|
+
export function analyzeStorySource(source, filePath = '') {
|
|
123
|
+
const twigImports = findTwigImports(source);
|
|
124
|
+
const templateNames = twigImports.map((item) => item.name);
|
|
125
|
+
const hasRenderTwig = importsRenderTwig(source);
|
|
126
|
+
const directTemplateReturns = findDirectTemplateReturns(
|
|
127
|
+
source,
|
|
128
|
+
templateNames,
|
|
129
|
+
);
|
|
130
|
+
const reasons = [];
|
|
131
|
+
|
|
132
|
+
if (!twigImports.length) {
|
|
133
|
+
return {
|
|
134
|
+
filePath,
|
|
135
|
+
twigImports,
|
|
136
|
+
hasRenderTwig,
|
|
137
|
+
directTemplateReturns,
|
|
138
|
+
reasons,
|
|
139
|
+
shouldUpgrade: false,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (!hasRenderTwig) {
|
|
144
|
+
reasons.push('imports Twig templates without renderTwig()');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (directTemplateReturns.length) {
|
|
148
|
+
reasons.push('appears to return Twig HTML strings directly');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
filePath,
|
|
153
|
+
twigImports,
|
|
154
|
+
hasRenderTwig,
|
|
155
|
+
directTemplateReturns,
|
|
156
|
+
reasons,
|
|
157
|
+
shouldUpgrade: reasons.length > 0,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Resolve Storybook source roots for the project.
|
|
163
|
+
*
|
|
164
|
+
* @param {string} projectDir - Absolute project root.
|
|
165
|
+
* @returns {string[]} Absolute story roots.
|
|
166
|
+
*/
|
|
167
|
+
export function resolveStoryRoots(projectDir) {
|
|
168
|
+
try {
|
|
169
|
+
const env = resolveProjectConfig(projectDir, process.env);
|
|
170
|
+
const storyRoots = env.projectStructure?.storyRoots;
|
|
171
|
+
if (Array.isArray(storyRoots) && storyRoots.length) {
|
|
172
|
+
return storyRoots;
|
|
173
|
+
}
|
|
174
|
+
} catch {
|
|
175
|
+
// Fall back to conventional roots when project config is absent or invalid.
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return [resolve(projectDir, 'src'), resolve(projectDir, 'components')];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Collect Storybook story files from normalized project roots.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} projectDir - Absolute project root.
|
|
185
|
+
* @returns {string[]} Absolute story file paths.
|
|
186
|
+
*/
|
|
187
|
+
export function collectStoryFiles(projectDir) {
|
|
188
|
+
const files = new Set();
|
|
189
|
+
|
|
190
|
+
for (const root of resolveStoryRoots(projectDir)) {
|
|
191
|
+
if (!existsSync(root)) continue;
|
|
192
|
+
|
|
193
|
+
for (const match of globSync(STORY_GLOB, {
|
|
194
|
+
cwd: root,
|
|
195
|
+
nodir: true,
|
|
196
|
+
absolute: true,
|
|
197
|
+
ignore: DEFAULT_IGNORES,
|
|
198
|
+
})) {
|
|
199
|
+
files.add(resolve(match));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return Array.from(files).sort();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Analyze all discovered story files in a project.
|
|
208
|
+
*
|
|
209
|
+
* @param {{projectDir?: string}} [options={}] - Audit options.
|
|
210
|
+
* @returns {{projectDir: string, files: string[], findings: object[]}} Results.
|
|
211
|
+
*/
|
|
212
|
+
export function auditTwigStories(options = {}) {
|
|
213
|
+
const projectDir = resolve(options.projectDir || process.cwd());
|
|
214
|
+
const files = collectStoryFiles(projectDir);
|
|
215
|
+
const findings = files
|
|
216
|
+
.map((filePath) => {
|
|
217
|
+
const source = readFileSync(filePath, 'utf8');
|
|
218
|
+
return analyzeStorySource(source, filePath);
|
|
219
|
+
})
|
|
220
|
+
.filter((result) => result.shouldUpgrade);
|
|
221
|
+
|
|
222
|
+
return {
|
|
223
|
+
projectDir,
|
|
224
|
+
files,
|
|
225
|
+
findings,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Format audit findings for terminal output.
|
|
231
|
+
*
|
|
232
|
+
* @param {{projectDir: string, files: string[], findings: object[]}} result
|
|
233
|
+
* Audit result.
|
|
234
|
+
* @returns {string} Human-readable report.
|
|
235
|
+
*/
|
|
236
|
+
export function formatAuditReport(result) {
|
|
237
|
+
const lines = [
|
|
238
|
+
'Twig story migration audit',
|
|
239
|
+
`Scanned ${result.files.length} story file(s).`,
|
|
240
|
+
];
|
|
241
|
+
|
|
242
|
+
if (!result.findings.length) {
|
|
243
|
+
lines.push('No legacy Twig story candidates found.');
|
|
244
|
+
return lines.join('\n');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
lines.push(
|
|
248
|
+
`Found ${result.findings.length} story file(s) that should be reviewed:`,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
for (const finding of result.findings) {
|
|
252
|
+
const relPath = toPosixPath(relative(result.projectDir, finding.filePath));
|
|
253
|
+
const importNames = finding.twigImports.map((item) => item.name).join(', ');
|
|
254
|
+
|
|
255
|
+
lines.push('', `- ${relPath}`);
|
|
256
|
+
lines.push(` Twig imports: ${importNames}`);
|
|
257
|
+
for (const reason of finding.reasons) {
|
|
258
|
+
lines.push(` Reason: ${reason}.`);
|
|
259
|
+
}
|
|
260
|
+
for (const call of finding.directTemplateReturns) {
|
|
261
|
+
lines.push(
|
|
262
|
+
` Line ${call.line}: ${call.name}() appears to be returned directly.`,
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
lines.push(
|
|
268
|
+
'',
|
|
269
|
+
'Suggested migration: import { renderTwig } from "@emulsify/core/storybook" and move any argument mapping into renderTwig(template, { context }).',
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
return lines.join('\n');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Parse command-line arguments.
|
|
277
|
+
*
|
|
278
|
+
* @param {string[]} argv - CLI arguments.
|
|
279
|
+
* @returns {{projectDir: string, failOnFound: boolean, json: boolean, help: boolean}}
|
|
280
|
+
* Parsed options.
|
|
281
|
+
*/
|
|
282
|
+
function parseArgs(argv) {
|
|
283
|
+
const options = {
|
|
284
|
+
projectDir: process.cwd(),
|
|
285
|
+
failOnFound: false,
|
|
286
|
+
json: false,
|
|
287
|
+
help: false,
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
291
|
+
const arg = argv[index];
|
|
292
|
+
|
|
293
|
+
if (arg === '--help' || arg === '-h') {
|
|
294
|
+
options.help = true;
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (arg === '--fail-on-found') {
|
|
298
|
+
options.failOnFound = true;
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (arg === '--json') {
|
|
302
|
+
options.json = true;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
if (arg === '--root') {
|
|
306
|
+
const value = argv[index + 1];
|
|
307
|
+
if (!value || value.startsWith('--')) {
|
|
308
|
+
throw new Error('--root requires a project directory.');
|
|
309
|
+
}
|
|
310
|
+
options.projectDir = value;
|
|
311
|
+
index += 1;
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (arg.startsWith('--root=')) {
|
|
315
|
+
options.projectDir = arg.slice('--root='.length);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return options;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* CLI usage text.
|
|
327
|
+
*
|
|
328
|
+
* @returns {string} Usage text.
|
|
329
|
+
*/
|
|
330
|
+
function usage() {
|
|
331
|
+
return [
|
|
332
|
+
'Usage: emulsify-audit-twig-stories [--root <dir>] [--json] [--fail-on-found]',
|
|
333
|
+
'',
|
|
334
|
+
'Options:',
|
|
335
|
+
' --root <dir> Project root to scan. Defaults to the current directory.',
|
|
336
|
+
' --json Print machine-readable JSON.',
|
|
337
|
+
' --fail-on-found Exit with code 1 when migration candidates are found.',
|
|
338
|
+
' --help Print this help text.',
|
|
339
|
+
].join('\n');
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Run the CLI.
|
|
344
|
+
*
|
|
345
|
+
* @param {string[]} argv - CLI arguments.
|
|
346
|
+
* @returns {number} Process exit code.
|
|
347
|
+
*/
|
|
348
|
+
export function runCli(argv = process.argv.slice(2)) {
|
|
349
|
+
const options = parseArgs(argv);
|
|
350
|
+
|
|
351
|
+
if (options.help) {
|
|
352
|
+
console.log(usage());
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const result = auditTwigStories({
|
|
357
|
+
projectDir: options.projectDir,
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
if (options.json) {
|
|
361
|
+
console.log(JSON.stringify(result, null, 2));
|
|
362
|
+
} else {
|
|
363
|
+
console.log(formatAuditReport(result));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return options.failOnFound && result.findings.length ? 1 : 0;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (process.argv[1]?.split(/[\\/]/).pop() === 'audit-twig-stories.js') {
|
|
370
|
+
try {
|
|
371
|
+
process.exitCode = runCli();
|
|
372
|
+
} catch (error) {
|
|
373
|
+
console.error(error.message || error);
|
|
374
|
+
console.error('');
|
|
375
|
+
console.error(usage());
|
|
376
|
+
process.exitCode = 1;
|
|
377
|
+
}
|
|
378
|
+
}
|