@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.
Files changed (78) hide show
  1. package/.storybook/main-vite.js +91 -36
  2. package/README.md +185 -56
  3. package/config/a11y.config.js +2 -1
  4. package/config/vite/entries.js +4 -4
  5. package/config/vite/plugins/{copy-src-assets.js → assets/copy-src-assets.js} +1 -1
  6. package/config/vite/plugins/{copy-twig-files.js → assets/copy-twig-files.js} +1 -1
  7. package/config/vite/plugins/{css-asset-relativizer.js → assets/css-asset-relativizer.js} +9 -0
  8. package/config/vite/plugins/{mirror-components.js → assets/mirror-components.js} +2 -28
  9. package/config/vite/plugins/{source-file-index.js → assets/source-file-index.js} +1 -1
  10. package/config/vite/plugins/{svg-sprite.js → assets/svg-sprite.js} +2 -2
  11. package/config/vite/plugins/index.js +23 -14
  12. package/config/vite/plugins/reporter/asset-resolver.js +541 -0
  13. package/config/vite/plugins/reporter/build-errors.js +284 -0
  14. package/config/vite/plugins/reporter/diagnostics.js +366 -0
  15. package/config/vite/plugins/reporter/format.js +199 -0
  16. package/config/vite/plugins/reporter/index.js +308 -0
  17. package/config/vite/plugins/reporter/render.js +758 -0
  18. package/config/vite/plugins/reporter/sass-logger.js +237 -0
  19. package/config/vite/plugins/reporter/vite-logger.js +188 -0
  20. package/config/vite/plugins/reporter/watch-mode.js +40 -0
  21. package/config/vite/plugins/{twig-extension-installers.js → twig/extension-installers.js} +1 -1
  22. package/config/vite/{twig-extensions.js → plugins/twig/extensions.js} +2 -2
  23. package/config/vite/plugins/{twig-module.js → twig/twig-module.js} +197 -85
  24. package/config/vite/plugins/{virtual-twig-asset-sources.js → twig/virtual-twig-asset-sources.js} +37 -136
  25. package/config/vite/plugins/{virtual-twig-globs.js → twig/virtual-twig-globs.js} +3 -32
  26. package/config/vite/plugins/{vituum-patch.js → twig/vituum-patch.js} +3 -3
  27. package/config/vite/plugins.js +1 -1
  28. package/config/vite/project-config.js +1 -1
  29. package/config/vite/project-structure.js +1 -1
  30. package/config/vite/utils/lru.js +77 -0
  31. package/config/vite/utils/package-version.js +42 -0
  32. package/config/vite/utils/paths.js +1 -9
  33. package/config/vite/utils/react-singleton.js +1 -1
  34. package/config/vite/vite.config.js +51 -5
  35. package/package.json +93 -58
  36. package/scripts/a11y.js +115 -23
  37. package/scripts/audit/checks/core-imports.js +78 -0
  38. package/scripts/audit/checks/css-asset-references.js +99 -0
  39. package/scripts/audit/checks/drupal-assumptions.js +48 -0
  40. package/scripts/audit/checks/files-outside-roots.js +53 -0
  41. package/scripts/audit/checks/generated-package-scripts.js +113 -0
  42. package/scripts/audit/checks/legacy-twig-stories.js +33 -0
  43. package/scripts/audit/checks/package-overrides.js +91 -0
  44. package/scripts/audit/checks/project-config.js +71 -0
  45. package/scripts/audit/checks/story-discovery.js +35 -0
  46. package/scripts/audit/checks/twig-references.js +69 -0
  47. package/scripts/audit/checks/twig-volume.js +54 -0
  48. package/scripts/audit/checks/webpack-patterns.js +86 -0
  49. package/scripts/audit/index.js +177 -0
  50. package/scripts/audit/lib/css.js +165 -0
  51. package/scripts/audit/lib/files.js +168 -0
  52. package/scripts/audit/lib/findings.js +31 -0
  53. package/scripts/audit/lib/package-json.js +65 -0
  54. package/scripts/audit/lib/twig.js +227 -0
  55. package/scripts/audit/report.js +273 -0
  56. package/scripts/audit-twig-stories.js +115 -78
  57. package/scripts/audit.js +150 -1632
  58. package/scripts/check-node-version.js +136 -10
  59. package/scripts/inspect-components.js +456 -0
  60. package/scripts/lib/cli.js +179 -0
  61. package/scripts/lib/fs.js +31 -0
  62. package/scripts/lib/proc.js +78 -0
  63. package/scripts/lib/text.js +14 -0
  64. package/scripts/loadYaml.js +2 -2
  65. package/src/extensions/shared/attributes.js +3 -3
  66. package/src/extensions/shared/lists.js +2 -6
  67. package/src/extensions/shared/root-relative.js +38 -0
  68. package/src/storybook/index.js +4 -0
  69. package/src/storybook/render-twig.js +1 -1
  70. package/src/storybook/render-web-component.js +459 -0
  71. package/src/storybook/twig/asset-source-runtime.js +193 -0
  72. package/src/storybook/twig/{source-extensions.js → constants.js} +3 -1
  73. package/src/storybook/twig/reference-paths.js +2 -13
  74. package/src/storybook/twig/setup.js +12 -2
  75. package/src/storybook/twig/source-function.js +5 -2
  76. package/config/vite/utils/unique.js +0 -36
  77. package/src/storybook/twig/include.js +0 -28
  78. package/src/storybook/twig/source-events.js +0 -5
@@ -0,0 +1,179 @@
1
+ /**
2
+ * @file Shared CLI argument helpers.
3
+ */
4
+
5
+ /**
6
+ * Format standard command usage text.
7
+ *
8
+ * @param {string} usageLine - Usage line.
9
+ * @param {string[]} optionLines - Option lines.
10
+ * @returns {string} Usage text.
11
+ */
12
+ export function createUsage(usageLine, optionLines) {
13
+ return [usageLine, '', 'Options:', ...optionLines].join('\n');
14
+ }
15
+
16
+ /**
17
+ * Determine whether a script is running as a CLI entrypoint.
18
+ *
19
+ * npm package bins run through symlinks, so argv[1] may be either the source
20
+ * script filename or the published bin name.
21
+ *
22
+ * @param {string[]} names - Accepted script or bin basenames.
23
+ * @param {string[]} [argv=process.argv] - Process arguments.
24
+ * @returns {boolean} TRUE when argv[1] matches one of the names.
25
+ */
26
+ export function isCliEntrypoint(names, argv = process.argv) {
27
+ const entryName = argv[1]?.split(/[\\/]/).pop();
28
+ return names.includes(entryName);
29
+ }
30
+
31
+ /**
32
+ * Clone default option values without sharing arrays between parses.
33
+ *
34
+ * @param {object} defaults - Default values.
35
+ * @returns {object} Parsed option seed.
36
+ */
37
+ function cloneDefaults(defaults) {
38
+ return Object.fromEntries(
39
+ Object.entries(defaults).map(([key, value]) => [
40
+ key,
41
+ Array.isArray(value) ? [...value] : value,
42
+ ]),
43
+ );
44
+ }
45
+
46
+ /**
47
+ * Normalize a boolean flag configuration.
48
+ *
49
+ * @param {string|object} spec - Flag config.
50
+ * @returns {{key: string, value: *}} Normalized flag config.
51
+ */
52
+ function normalizeFlagSpec(spec) {
53
+ if (typeof spec === 'string') {
54
+ return { key: spec, value: true };
55
+ }
56
+
57
+ return {
58
+ value: true,
59
+ ...spec,
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Read the next argv value for an option.
65
+ *
66
+ * @param {string[]} argv - CLI arguments.
67
+ * @param {number} index - Current option index.
68
+ * @param {object} spec - Option config.
69
+ * @returns {string} Raw value.
70
+ */
71
+ function readNextValue(argv, index, spec) {
72
+ const value = argv[index + 1];
73
+
74
+ if (
75
+ value === undefined ||
76
+ (value === '' && spec.rejectEmptyValue !== false) ||
77
+ (spec.rejectOptionLikeValue !== false && value.startsWith('--'))
78
+ ) {
79
+ throw new Error(spec.missingMessage);
80
+ }
81
+
82
+ return value;
83
+ }
84
+
85
+ /**
86
+ * Parse and assign one option value.
87
+ *
88
+ * @param {object} parsed - Parsed options.
89
+ * @param {object} spec - Option config.
90
+ * @param {string} value - Raw option value.
91
+ * @returns {void}
92
+ */
93
+ function assignOptionValue(parsed, spec, value) {
94
+ const nextValue = spec.parse ? spec.parse(value) : value;
95
+
96
+ if (spec.validate && !spec.validate(nextValue)) {
97
+ throw new Error(spec.invalidMessage || spec.missingMessage);
98
+ }
99
+
100
+ if (spec.append) {
101
+ const values = Array.isArray(nextValue) ? nextValue : [nextValue];
102
+ parsed[spec.key].push(...values);
103
+ return;
104
+ }
105
+
106
+ parsed[spec.key] = nextValue;
107
+ }
108
+
109
+ /**
110
+ * Parse common CLI flags.
111
+ *
112
+ * @param {string[]} argv - CLI arguments.
113
+ * @param {object} [config={}] - Parser configuration.
114
+ * @param {object} [config.defaults={}] - Default option values.
115
+ * @param {object} [config.flags={}] - Boolean flag map.
116
+ * @param {object} [config.options={}] - Value option map.
117
+ * @param {boolean} [config.allowPositionalProjectDir=false] - Allow one root.
118
+ * @returns {object} Parsed options.
119
+ */
120
+ export function parseArgs(argv, config = {}) {
121
+ const {
122
+ defaults = {},
123
+ flags = {},
124
+ options = {},
125
+ allowPositionalProjectDir = false,
126
+ } = config;
127
+ const parsed = cloneDefaults(defaults);
128
+ let positionalProjectDirFound = false;
129
+
130
+ for (let index = 0; index < argv.length; index += 1) {
131
+ const arg = argv[index];
132
+
133
+ if (arg === '--help' || arg === '-h') {
134
+ parsed.help = true;
135
+ continue;
136
+ }
137
+
138
+ if (Object.prototype.hasOwnProperty.call(flags, arg)) {
139
+ const spec = normalizeFlagSpec(flags[arg]);
140
+ parsed[spec.key] = spec.value;
141
+ continue;
142
+ }
143
+
144
+ const [inlineName, inlineValue] = arg.startsWith('--')
145
+ ? arg.split(/=(.*)/s, 2)
146
+ : [arg, undefined];
147
+ const optionName = Object.prototype.hasOwnProperty.call(options, arg)
148
+ ? arg
149
+ : inlineValue !== undefined && options[inlineName]
150
+ ? inlineName
151
+ : undefined;
152
+
153
+ if (optionName) {
154
+ const spec = options[optionName];
155
+ const value =
156
+ inlineValue === undefined
157
+ ? readNextValue(argv, index, spec)
158
+ : inlineValue;
159
+ assignOptionValue(parsed, spec, value);
160
+ if (inlineValue === undefined) {
161
+ index += 1;
162
+ }
163
+ continue;
164
+ }
165
+
166
+ if (allowPositionalProjectDir && !arg.startsWith('--')) {
167
+ if (positionalProjectDirFound) {
168
+ throw new Error(`Unknown option: ${arg}`);
169
+ }
170
+ parsed.projectDir = arg;
171
+ positionalProjectDirFound = true;
172
+ continue;
173
+ }
174
+
175
+ throw new Error(`Unknown option: ${arg}`);
176
+ }
177
+
178
+ return parsed;
179
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @file Shared filesystem helpers for CLI scripts.
3
+ */
4
+
5
+ import { readdirSync, statSync } from 'node:fs';
6
+ import { resolve } from 'node:path';
7
+
8
+ /**
9
+ * Recursively measure a directory size.
10
+ *
11
+ * Unreadable directories contribute only the bytes already counted before the
12
+ * filesystem error, matching the audit script's historical safe behavior.
13
+ *
14
+ * @param {string} directory - Directory path.
15
+ * @returns {number} Size in bytes.
16
+ */
17
+ export function directorySize(directory) {
18
+ let total = 0;
19
+
20
+ try {
21
+ for (const entry of readdirSync(directory)) {
22
+ const entryPath = resolve(directory, entry);
23
+ const stats = statSync(entryPath);
24
+ total += stats.isDirectory() ? directorySize(entryPath) : stats.size;
25
+ }
26
+ } catch {
27
+ return total;
28
+ }
29
+
30
+ return total;
31
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * @file Shared process helpers for CLI scripts.
3
+ */
4
+
5
+ import { execFileSync, spawnSync } from 'node:child_process';
6
+
7
+ /**
8
+ * Build a default failed-command message.
9
+ *
10
+ * @param {object} context - Failed command context.
11
+ * @param {string} context.command - Command name.
12
+ * @param {string[]} context.args - Command arguments.
13
+ * @param {string} [context.cwd] - Working directory.
14
+ * @param {number|null} [context.status] - Exit status.
15
+ * @returns {string} Failure message.
16
+ */
17
+ function defaultFailureMessage({ command, args, cwd, status }) {
18
+ const location = cwd ? ` in ${cwd}` : '';
19
+ return `${command} ${args.join(' ')} failed${location} with exit ${status}`;
20
+ }
21
+
22
+ /**
23
+ * Run a child process.
24
+ *
25
+ * By default this uses spawnSync and returns the full result. Pass
26
+ * `{ mode: 'exec' }` for execFileSync behavior, which returns stdout.
27
+ *
28
+ * @param {string} command - Command name or path.
29
+ * @param {string[]} [args=[]] - Command arguments.
30
+ * @param {object} [options={}] - Child process options.
31
+ * @param {'spawn'|'exec'} [options.mode='spawn'] - Process API to use.
32
+ * @param {boolean} [options.echoOutputOnFailure=false] - Echo captured output.
33
+ * @param {string|Function} [options.failureMessage] - Error message override.
34
+ * @returns {*} spawnSync result or execFileSync stdout.
35
+ */
36
+ export function run(command, args = [], options = {}) {
37
+ const {
38
+ mode = 'spawn',
39
+ echoOutputOnFailure = false,
40
+ failureMessage,
41
+ ...processOptions
42
+ } = options;
43
+
44
+ if (mode === 'exec') {
45
+ return execFileSync(command, args, {
46
+ encoding: 'utf8',
47
+ ...processOptions,
48
+ });
49
+ }
50
+
51
+ const result = spawnSync(command, args, {
52
+ encoding: 'utf8',
53
+ ...processOptions,
54
+ });
55
+
56
+ if (result.status !== 0) {
57
+ if (echoOutputOnFailure) {
58
+ process.stdout.write(result.stdout || '');
59
+ process.stderr.write(result.stderr || '');
60
+ }
61
+
62
+ const context = {
63
+ args,
64
+ command,
65
+ cwd: processOptions.cwd,
66
+ status: result.status,
67
+ result,
68
+ };
69
+ const message =
70
+ typeof failureMessage === 'function'
71
+ ? failureMessage(context)
72
+ : failureMessage || defaultFailureMessage(context);
73
+
74
+ throw new Error(message);
75
+ }
76
+
77
+ return result;
78
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @file Shared text helpers for CLI scripts.
3
+ */
4
+
5
+ /**
6
+ * Find the 1-based line number for a character index.
7
+ *
8
+ * @param {string} source - Source text.
9
+ * @param {number} index - Character index.
10
+ * @returns {number} 1-based line number.
11
+ */
12
+ export function lineNumberAt(source, index) {
13
+ return source.slice(0, index).split('\n').length;
14
+ }
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { resolve } from 'path';
6
6
  import { readFileSync } from 'fs';
7
- import { parse } from 'yaml';
7
+ import { load as parseYaml } from 'js-yaml';
8
8
 
9
9
  /**
10
10
  * Small utility function that loads a yaml file and parses it synchronously.
@@ -17,5 +17,5 @@ import { parse } from 'yaml';
17
17
  export default function loadYaml(relativePath) {
18
18
  // Resolve from this script directory so tests can pass stable relative paths.
19
19
  const fullPath = resolve(__dirname, relativePath);
20
- return parse(readFileSync(fullPath, 'utf8'));
20
+ return parseYaml(readFileSync(fullPath, 'utf8'));
21
21
  }
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  import { escapeAttributeValue, isSafeAttributeName } from './html.js';
7
- import { flattenList, uniqueList } from './lists.js';
7
+ import { flattenList, unique } from './lists.js';
8
8
  import { isPlainObject } from './object.js';
9
9
 
10
10
  /**
@@ -59,7 +59,7 @@ export function classTokensFromValue(value) {
59
59
  return value.getClassList();
60
60
  }
61
61
 
62
- return uniqueList(
62
+ return unique(
63
63
  flattenList(value)
64
64
  .flatMap((item) => String(item || '').split(/\s+/))
65
65
  .map((item) => cleanClassToken(item))
@@ -181,7 +181,7 @@ export class AttributeBag {
181
181
  if (!tokens.length) return this;
182
182
 
183
183
  const existing = this.attributes.get('class') || [];
184
- this.attributes.set('class', uniqueList([...existing, ...tokens]));
184
+ this.attributes.set('class', unique([...existing, ...tokens]));
185
185
  return this;
186
186
  }
187
187
 
@@ -3,10 +3,6 @@
3
3
  * @module extensions/shared/lists
4
4
  */
5
5
 
6
- import { unique } from '../../../config/vite/utils/unique.js';
7
-
8
- export { unique };
9
-
10
6
  /**
11
7
  * Convert scalar or nested array values into a flat list.
12
8
  *
@@ -32,7 +28,7 @@ export function flattenList(value) {
32
28
  * @param {*[]} values - Values to deduplicate.
33
29
  * @returns {*[]} Unique values.
34
30
  */
35
- export function uniqueList(values) {
31
+ export function unique(values = []) {
36
32
  // Preserve first-seen order; class order can affect utility CSS output.
37
- return unique(values);
33
+ return Array.from(new Set(values));
38
34
  }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * @file Browser-safe root-relative path helpers shared by Vite and Storybook.
3
+ */
4
+
5
+ const normalizeProjectPath = (filePath) => {
6
+ const normalized = String(filePath || '')
7
+ .replace(/\\/g, '/')
8
+ .replace(/\/{2,}/g, '/');
9
+
10
+ return normalized === '/' ? normalized : normalized.replace(/\/+$/, '');
11
+ };
12
+
13
+ /**
14
+ * Convert an absolute project path to a Vite root-relative key.
15
+ *
16
+ * @param {string} projectDir - Absolute project root.
17
+ * @param {string} absolutePath - Absolute file or directory path.
18
+ * @returns {string} Root-relative path with a leading slash.
19
+ */
20
+ export function toRootRelativePath(projectDir, absolutePath) {
21
+ if (!absolutePath) return '';
22
+
23
+ const normalizedProjectDir = normalizeProjectPath(projectDir);
24
+ const normalizedPath = normalizeProjectPath(absolutePath);
25
+
26
+ if (normalizedProjectDir && normalizedPath === normalizedProjectDir) {
27
+ return '/';
28
+ }
29
+
30
+ if (
31
+ normalizedProjectDir &&
32
+ normalizedPath.startsWith(`${normalizedProjectDir}/`)
33
+ ) {
34
+ return `/${normalizedPath.slice(normalizedProjectDir.length + 1)}`;
35
+ }
36
+
37
+ return `/${normalizedPath.replace(/^\/+/, '')}`;
38
+ }
@@ -12,3 +12,7 @@ export {
12
12
  TwigHtmlStory,
13
13
  TwigStory,
14
14
  } from './render-twig.js';
15
+ export {
16
+ defineCustomElement,
17
+ renderWebComponent,
18
+ } from './render-web-component.js';
@@ -7,7 +7,7 @@ import {
7
7
  attachStorybookBehaviors,
8
8
  normalizeStorybookPlatformAdapter,
9
9
  } from './platform-behaviors.js';
10
- import { TWIG_SOURCE_LOADED_EVENT } from './twig/source-events.js';
10
+ import { TWIG_SOURCE_LOADED_EVENT } from './twig/constants.js';
11
11
 
12
12
  /**
13
13
  * Read the normalized Emulsify environment injected by Storybook's Vite config.