@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.
Files changed (84) hide show
  1. package/.storybook/main-vite.js +102 -37
  2. package/.storybook/main.js +20 -0
  3. package/.storybook/ready-reporter.js +230 -0
  4. package/README.md +186 -56
  5. package/config/a11y.config.js +2 -1
  6. package/config/vite/entries.js +4 -4
  7. package/config/vite/plugins/assets/copy-src-assets.js +120 -0
  8. package/config/vite/plugins/assets/copy-twig-files.js +130 -0
  9. package/config/vite/plugins/{css-asset-relativizer.js → assets/css-asset-relativizer.js} +9 -0
  10. package/config/vite/plugins/{mirror-components.js → assets/mirror-components.js} +2 -28
  11. package/config/vite/plugins/{source-file-index.js → assets/source-file-index.js} +1 -1
  12. package/config/vite/plugins/{svg-sprite.js → assets/svg-sprite.js} +2 -2
  13. package/config/vite/plugins/index.js +23 -14
  14. package/config/vite/plugins/reporter/asset-resolver.js +541 -0
  15. package/config/vite/plugins/reporter/build-errors.js +284 -0
  16. package/config/vite/plugins/reporter/diagnostics.js +367 -0
  17. package/config/vite/plugins/reporter/format.js +240 -0
  18. package/config/vite/plugins/reporter/index.js +417 -0
  19. package/config/vite/plugins/reporter/render.js +1332 -0
  20. package/config/vite/plugins/reporter/sass-logger.js +237 -0
  21. package/config/vite/plugins/reporter/source-roots.js +561 -0
  22. package/config/vite/plugins/reporter/verbosity.js +119 -0
  23. package/config/vite/plugins/reporter/vite-logger.js +249 -0
  24. package/config/vite/plugins/reporter/watch-mode.js +40 -0
  25. package/config/vite/plugins/{twig-extension-installers.js → twig/extension-installers.js} +1 -1
  26. package/config/vite/{twig-extensions.js → plugins/twig/extensions.js} +2 -2
  27. package/config/vite/plugins/{twig-module.js → twig/twig-module.js} +197 -85
  28. package/config/vite/plugins/{virtual-twig-asset-sources.js → twig/virtual-twig-asset-sources.js} +37 -136
  29. package/config/vite/plugins/{virtual-twig-globs.js → twig/virtual-twig-globs.js} +3 -32
  30. package/config/vite/plugins/{vituum-patch.js → twig/vituum-patch.js} +3 -3
  31. package/config/vite/plugins.js +1 -1
  32. package/config/vite/project-config.js +1 -1
  33. package/config/vite/project-structure.js +1 -1
  34. package/config/vite/utils/lru.js +77 -0
  35. package/config/vite/utils/package-version.js +42 -0
  36. package/config/vite/utils/paths.js +1 -9
  37. package/config/vite/utils/react-singleton.js +1 -1
  38. package/config/vite/vite.config.js +91 -5
  39. package/package.json +95 -57
  40. package/scripts/a11y.js +115 -23
  41. package/scripts/audit/checks/core-imports.js +78 -0
  42. package/scripts/audit/checks/css-asset-references.js +99 -0
  43. package/scripts/audit/checks/drupal-assumptions.js +48 -0
  44. package/scripts/audit/checks/files-outside-roots.js +53 -0
  45. package/scripts/audit/checks/generated-package-scripts.js +113 -0
  46. package/scripts/audit/checks/legacy-twig-stories.js +33 -0
  47. package/scripts/audit/checks/package-overrides.js +91 -0
  48. package/scripts/audit/checks/project-config.js +71 -0
  49. package/scripts/audit/checks/story-discovery.js +35 -0
  50. package/scripts/audit/checks/twig-references.js +69 -0
  51. package/scripts/audit/checks/twig-volume.js +54 -0
  52. package/scripts/audit/checks/webpack-patterns.js +86 -0
  53. package/scripts/audit/index.js +177 -0
  54. package/scripts/audit/lib/css.js +165 -0
  55. package/scripts/audit/lib/files.js +168 -0
  56. package/scripts/audit/lib/findings.js +31 -0
  57. package/scripts/audit/lib/package-json.js +65 -0
  58. package/scripts/audit/lib/twig.js +227 -0
  59. package/scripts/audit/report.js +273 -0
  60. package/scripts/audit-twig-stories.js +115 -78
  61. package/scripts/audit.js +150 -1632
  62. package/scripts/check-node-version.js +136 -10
  63. package/scripts/inspect-components.js +456 -0
  64. package/scripts/lib/cli.js +179 -0
  65. package/scripts/lib/fs.js +31 -0
  66. package/scripts/lib/proc.js +78 -0
  67. package/scripts/lib/text.js +14 -0
  68. package/scripts/loadYaml.js +2 -2
  69. package/src/extensions/shared/attributes.js +3 -3
  70. package/src/extensions/shared/lists.js +2 -6
  71. package/src/extensions/shared/root-relative.js +38 -0
  72. package/src/storybook/index.js +4 -0
  73. package/src/storybook/render-twig.js +1 -1
  74. package/src/storybook/render-web-component.js +459 -0
  75. package/src/storybook/twig/asset-source-runtime.js +193 -0
  76. package/src/storybook/twig/{source-extensions.js → constants.js} +3 -1
  77. package/src/storybook/twig/reference-paths.js +2 -13
  78. package/src/storybook/twig/setup.js +12 -2
  79. package/src/storybook/twig/source-function.js +5 -2
  80. package/config/vite/plugins/copy-src-assets.js +0 -76
  81. package/config/vite/plugins/copy-twig-files.js +0 -84
  82. package/config/vite/utils/unique.js +0 -36
  83. package/src/storybook/twig/include.js +0 -28
  84. package/src/storybook/twig/source-events.js +0 -5
@@ -0,0 +1,249 @@
1
+ /**
2
+ * @file Vite logger wrapper for the Emulsify develop reporter.
3
+ *
4
+ * Sass warnings reach the reporter through a Sass logger, but Vite emits its
5
+ * own diagnostics through `config.logger`. The most common of those in an
6
+ * Emulsify project is the unresolved CSS asset notice:
7
+ *
8
+ * ../images/bg-lines.png referenced in ../images/bg-lines.png didn't resolve
9
+ * at build time, it will remain unchanged to be resolved at runtime
10
+ *
11
+ * Vite emits one per `url()` it cannot resolve, mid-build, so they land
12
+ * interleaved with transform progress and Storybook's startup. The wording is
13
+ * also unhelpful: the "referenced in" file is frequently identical to the URL
14
+ * itself, and nothing in the sentence says whether this is a problem.
15
+ *
16
+ * This wrapper intercepts those and routes them into the shared diagnostics
17
+ * collector so they surface once, grouped, in the build summary. Every other
18
+ * message passes straight through to Vite's own logger untouched.
19
+ */
20
+
21
+ import { isQuiet, isVerbose } from './verbosity.js';
22
+
23
+ // Re-exported because the Vite config and the reporter both branch on it, and
24
+ // this module was where it lived before verbosity grew a third level.
25
+ export { isVerbose };
26
+
27
+ /**
28
+ * Matches Vite's unresolved CSS asset notice.
29
+ *
30
+ * @type {RegExp}
31
+ * @see https://github.com/vitejs/vite - `vite:css` url replacer
32
+ */
33
+ const UNRESOLVED_ASSET_PATTERN =
34
+ /^(.+?) referenced in (.+?) didn't resolve at build time/;
35
+
36
+ /**
37
+ * Remove ANSI escape sequences so pattern matching sees plain text.
38
+ *
39
+ * @param {string} value - Possibly styled text.
40
+ * @returns {string} Plain text.
41
+ */
42
+ const stripAnsi = (value) =>
43
+ // eslint-disable-next-line no-control-regex
44
+ String(value).replace(/\[[0-9;]*m/g, '');
45
+
46
+ /**
47
+ * Matches Rolldown's raw failure dump.
48
+ *
49
+ * The dump repeats every error up to three times — message, cause, and stack —
50
+ * and appends a Dart Sass JS trace that points into `sass.dart.js` rather than
51
+ * anywhere in the project.
52
+ *
53
+ * @type {RegExp}
54
+ */
55
+ const RAW_BUILD_DUMP =
56
+ /^\s*(?:Build failed with \d+ error|\[plugin [\w:-]+\]|Error: \[sass\]|\[sass\] )/;
57
+
58
+ /**
59
+ * Matches a Dart Sass JS stack frame.
60
+ *
61
+ * These point into the compiler bundle rather than the project and are noise
62
+ * wherever they appear, so their presence alone identifies a raw dump.
63
+ *
64
+ * @type {RegExp}
65
+ */
66
+ const DART_SASS_FRAME = /\bsass\.dart\.js:\d+/;
67
+
68
+ /**
69
+ * Detect a message that is nothing but JavaScript stack frames.
70
+ *
71
+ * Rolldown emits the trailing stack of a failure as its own log call. Those
72
+ * frames point into `lightningcss`, `vite`, or `sass` internals rather than
73
+ * anywhere in the project, so a message made only of them carries nothing once
74
+ * the failure itself has been reported.
75
+ *
76
+ * @param {string} message - Plain-text log message.
77
+ * @returns {boolean} TRUE when every line is a stack frame.
78
+ */
79
+ function isBareStackTrace(message) {
80
+ const lines = message.split('\n').filter((line) => line.trim());
81
+
82
+ return lines.length > 0 && lines.every((line) => /^\s*at\s+\S/.test(line));
83
+ }
84
+
85
+ /**
86
+ * Matches Vite's HMR notice.
87
+ *
88
+ * Emitted by the dev server as `hmr update <files>` through `logger.info`.
89
+ *
90
+ * @type {RegExp}
91
+ */
92
+ const HMR_UPDATE_PATTERN = /(^|\s)hmr update\s/;
93
+
94
+ /**
95
+ * Wrap the Storybook dev server's logger to drop HMR notices.
96
+ *
97
+ * These come from Storybook's Vite dev server, not from the watch build, and
98
+ * there are a lot of them. The watch build rewrites every file in `dist/` on
99
+ * every cycle — Rollup regenerates the whole bundle — and Storybook imports its
100
+ * compiled CSS from `dist/`, so one saved stylesheet lands as several HMR events.
101
+ * Most name Storybook's own virtual modules:
102
+ *
103
+ * │ Vite hmr update
104
+ * │ /@id/__x00__virtual:/@storybook/builder-vite/project-annotations.js,
105
+ * │ /@id/__x00__virtual:/@storybook/builder-vite/vite-app.js
106
+ *
107
+ * Nothing there is actionable, and the reporter has already printed the rebuild
108
+ * line that says the same thing more precisely. Under `concurrently` both
109
+ * processes share one pipe, so these interleave with the build's output and are
110
+ * the last thing making one command look like two.
111
+ *
112
+ * The wrapper delegates to whatever logger is already configured rather than
113
+ * replacing it, so Storybook keeps its own prefixes and styling for every other
114
+ * message. Verbose modes pass everything through, because someone who asked for
115
+ * more output should not have this filtered away.
116
+ *
117
+ * @param {{
118
+ * baseLogger: import('vite').Logger,
119
+ * verbose?: boolean
120
+ * }} options - Logger options.
121
+ * @returns {import('vite').Logger} Wrapped logger.
122
+ */
123
+ export function createDevServerLogger({ baseLogger, verbose } = {}) {
124
+ const passThrough = verbose === undefined ? !isQuiet() : verbose;
125
+
126
+ return {
127
+ get hasWarned() {
128
+ return baseLogger.hasWarned;
129
+ },
130
+
131
+ set hasWarned(value) {
132
+ baseLogger.hasWarned = value;
133
+ },
134
+
135
+ info(message, options) {
136
+ if (!passThrough && HMR_UPDATE_PATTERN.test(stripAnsi(String(message)))) {
137
+ return;
138
+ }
139
+ baseLogger.info(message, options);
140
+ },
141
+
142
+ warn: (message, options) => baseLogger.warn(message, options),
143
+ warnOnce: (message, options) => baseLogger.warnOnce(message, options),
144
+ error: (message, options) => baseLogger.error(message, options),
145
+ clearScreen: (type) => baseLogger.clearScreen(type),
146
+ hasErrorLogged: (error) => baseLogger.hasErrorLogged(error),
147
+ };
148
+ }
149
+
150
+ /**
151
+ * Parse Vite's unresolved CSS asset notice.
152
+ *
153
+ * @param {string} message - Raw log message.
154
+ * @returns {{url: string, importer: string|undefined}|undefined} Parsed notice.
155
+ */
156
+ export function parseUnresolvedAsset(message) {
157
+ if (typeof message !== 'string') return undefined;
158
+
159
+ const match = UNRESOLVED_ASSET_PATTERN.exec(stripAnsi(message).trim());
160
+ if (!match) return undefined;
161
+
162
+ const [, url, importer] = match;
163
+
164
+ return {
165
+ url,
166
+ // Vite reports the URL as its own importer when the referencing stylesheet
167
+ // is not known. Recording that adds nothing, so it is dropped.
168
+ importer: importer === url ? undefined : importer,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Wrap a Vite logger so reportable diagnostics are collected instead of printed.
174
+ *
175
+ * The wrapper delegates rather than spreading, because `hasWarned` is a mutable
176
+ * property that Vite reads back after logging; a spread copy would freeze it.
177
+ *
178
+ * @param {ReturnType<import('./diagnostics.js').createDiagnosticsCollector>} collector - Shared collector.
179
+ * @param {import('vite').Logger} baseLogger - Logger to delegate to.
180
+ * @returns {import('vite').Logger} Wrapped logger.
181
+ */
182
+ export function createReporterLogger(collector, baseLogger, { verbose } = {}) {
183
+ const passRawThrough = verbose === undefined ? isVerbose() : verbose;
184
+
185
+ /**
186
+ * Record a message if it is one the reporter owns.
187
+ *
188
+ * @param {string} message - Raw log message.
189
+ * @returns {boolean} TRUE when the message was captured.
190
+ */
191
+ const capture = (message) => {
192
+ const unresolvedAsset = parseUnresolvedAsset(message);
193
+ if (!unresolvedAsset) return false;
194
+
195
+ collector.recordUnresolvedAsset(unresolvedAsset);
196
+ return true;
197
+ };
198
+
199
+ /**
200
+ * Determine whether a failure dump has already been reported in table form.
201
+ *
202
+ * Output is only ever dropped once the reporter holds the same information
203
+ * in a readable shape; an error it failed to parse still reaches the user.
204
+ *
205
+ * @param {string} message - Raw log message.
206
+ * @returns {boolean} TRUE when the dump is redundant.
207
+ */
208
+ const isRedundantDump = (message) => {
209
+ if (passRawThrough) return false;
210
+ if (!collector.hasCapturedBuildErrors?.()) return false;
211
+
212
+ const plain = stripAnsi(String(message));
213
+
214
+ return (
215
+ RAW_BUILD_DUMP.test(plain) ||
216
+ DART_SASS_FRAME.test(plain) ||
217
+ isBareStackTrace(plain)
218
+ );
219
+ };
220
+
221
+ return {
222
+ get hasWarned() {
223
+ return baseLogger.hasWarned;
224
+ },
225
+
226
+ set hasWarned(value) {
227
+ baseLogger.hasWarned = value;
228
+ },
229
+
230
+ info: (message, options) => baseLogger.info(message, options),
231
+
232
+ warn(message, options) {
233
+ if (!capture(message)) baseLogger.warn(message, options);
234
+ },
235
+
236
+ warnOnce(message, options) {
237
+ if (!capture(message)) baseLogger.warnOnce(message, options);
238
+ },
239
+
240
+ error(message, options) {
241
+ if (isRedundantDump(message)) return;
242
+ baseLogger.error(message, options);
243
+ },
244
+
245
+ clearScreen: (type) => baseLogger.clearScreen(type),
246
+
247
+ hasErrorLogged: (error) => baseLogger.hasErrorLogged(error),
248
+ };
249
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @file Watch-invocation detection for the Emulsify develop reporter.
3
+ *
4
+ * The reporter and its quiet Sass logger should only take over output for the
5
+ * long-running watcher that `npm run develop` starts. Every other command that
6
+ * loads this Vite config — `npm run build`, `storybook build`, `storybook dev`,
7
+ * and the release fixture verifications — must keep its current output exactly
8
+ * as it is today.
9
+ *
10
+ * Vite resolves `command: 'build'` for both `vite build` and
11
+ * `vite build --watch`, so the resolved config cannot distinguish them at the
12
+ * point where `css.preprocessorOptions` has to be decided. The CLI flag is the
13
+ * only signal available that early, so it is read directly. The plugin
14
+ * independently confirms the decision against `config.build.watch` once the
15
+ * config is resolved, which covers projects that enable watch mode through
16
+ * their own config patch rather than the flag.
17
+ */
18
+
19
+ /**
20
+ * Flags that put the Vite CLI into watch mode.
21
+ *
22
+ * @type {string[]}
23
+ */
24
+ const WATCH_FLAGS = ['--watch', '-w'];
25
+
26
+ /**
27
+ * Determine whether the current process was invoked as a Vite watch build.
28
+ *
29
+ * @param {string[]} [argv] - Process arguments.
30
+ * @returns {boolean} TRUE when a watch flag is present.
31
+ */
32
+ export function isWatchInvocation(argv = process.argv) {
33
+ if (!Array.isArray(argv)) return false;
34
+
35
+ return argv.some(
36
+ (arg) =>
37
+ WATCH_FLAGS.includes(arg) ||
38
+ WATCH_FLAGS.some((flag) => arg.startsWith(`${flag}=`)),
39
+ );
40
+ }
@@ -2,7 +2,7 @@
2
2
  * @file Virtual Twig.js extension installer module for Storybook.
3
3
  */
4
4
 
5
- import { generateTwigExtensionInstallersModule } from '../twig-extensions.js';
5
+ import { generateTwigExtensionInstallersModule } from './extensions.js';
6
6
 
7
7
  export const VIRTUAL_TWIG_EXTENSION_INSTALLERS_ID =
8
8
  'virtual:emulsify-twig-extension-installers';
@@ -60,7 +60,7 @@ function normalizeInstaller(moduleValue) {
60
60
  * @param {object} [env={}] - Normalized Emulsify environment or Twig options.
61
61
  * @returns {object} The provided Twig.js instance.
62
62
  */
63
- export function registerConfiguredTwigExtensions(twig, env = {}) {
63
+ export function installProjectTwigExtensions(twig, env = {}) {
64
64
  if (shouldRegisterDrupalTwigFilters(env)) {
65
65
  const installer = normalizeInstaller(require('twig-drupal-filters'));
66
66
  if (installer) {
@@ -98,7 +98,7 @@ const installers = [${installerNames}].filter(
98
98
 
99
99
  export const twigExtensionInstallers = installers;
100
100
 
101
- export function registerConfiguredTwigExtensions(Twig) {
101
+ export function installProjectTwigExtensions(Twig) {
102
102
  for (const installer of installers) {
103
103
  installer(Twig);
104
104
  }