@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
@@ -1,16 +1,18 @@
1
1
  import path from 'path';
2
2
  import viteConfig from '../config/vite/vite.config.js';
3
- import { twigExtensionModuleSpecifiers } from '../config/vite/twig-extensions.js';
3
+ import { twigExtensionModuleSpecifiers } from '../config/vite/plugins/twig/extensions.js';
4
4
  import {
5
5
  mergeReactSingletonOptimizeDeps,
6
6
  mergeReactSingletonResolve,
7
7
  } from '../config/vite/utils/react-singleton.js';
8
+ import { createDevServerLogger } from '../config/vite/plugins/reporter/vite-logger.js';
8
9
  import { makeGeneratedDistFilesPlugin } from './main-static-assets.js';
9
10
 
10
- // Twig glob maps are provided by config/vite/plugins/virtual-twig-globs.js.
11
+ // Twig glob maps are provided by config/vite/plugins/twig/virtual-twig-globs.js.
11
12
  const twigVirtualModuleIds = [
12
13
  'virtual:emulsify-twig-globs',
13
14
  'virtual:emulsify-twig-asset-sources',
15
+ 'virtual:emulsify-twig-asset-source-runtime',
14
16
  'virtual:emulsify-twig-extension-installers',
15
17
  ];
16
18
 
@@ -52,22 +54,64 @@ function mergeTwigRuntimeOptimizeDepsExcludes(...excludeLists) {
52
54
  );
53
55
  }
54
56
 
57
+ /**
58
+ * Drop Vite's legacy `rollupOptions` alias from an `optimizeDeps` object.
59
+ *
60
+ * Vite 8 installs a compat shim on resolved `optimizeDeps` that defines
61
+ * `rollupOptions` as an *enumerable accessor* forwarding to `rolldownOptions`.
62
+ * Spreading such an object — which is how Storybook's config is merged here —
63
+ * copies the alias out as an ordinary data property. Vite then sees an
64
+ * `optimizeDeps` carrying two distinct objects and logs:
65
+ *
66
+ * `optimizeDeps.rollupOptions` / `ssr.optimizeDeps.rollupOptions` is
67
+ * deprecated. Use `optimizeDeps.rolldownOptions` instead.
68
+ *
69
+ * `ssr.optimizeDeps` inherits from `optimizeDeps`, so clearing the alias here
70
+ * settles both. Anything the alias was carrying is folded into
71
+ * `rolldownOptions` rather than discarded, so behavior is unchanged.
72
+ *
73
+ * That deprecation is emitted with `console.warn` rather than through Vite's
74
+ * logger, so it cannot be intercepted downstream — it has to be prevented.
75
+ *
76
+ * @param {object} [optimizeDeps] - Resolved or partial optimizeDeps config.
77
+ * @returns {object} Copy with the alias resolved into `rolldownOptions`.
78
+ */
79
+ export function stripRollupOptionsAlias(optimizeDeps) {
80
+ if (!optimizeDeps) return {};
81
+
82
+ const { rollupOptions, rolldownOptions, ...rest } = optimizeDeps;
83
+ const resolved = rolldownOptions || rollupOptions;
84
+
85
+ return resolved ? { ...rest, rolldownOptions: resolved } : rest;
86
+ }
87
+
88
+ /**
89
+ * Virtual module IDs that must stay external during dependency optimization.
90
+ *
91
+ * @type {RegExp}
92
+ */
93
+ const TWIG_VIRTUAL_MODULE_PATTERN =
94
+ /^virtual:emulsify-twig-(?:globs|asset-sources|asset-source-runtime)$/;
95
+
55
96
  /**
56
97
  * Keep Emulsify Twig virtual imports out of Storybook dependency prebundles.
57
98
  *
58
- * @returns {import('esbuild').Plugin} Esbuild plugin for optimizeDeps.
99
+ * Vite 8 optimizes dependencies with Rolldown rather than esbuild, so this is a
100
+ * Rolldown plugin using the Rollup-compatible `resolveId` hook. It replaces the
101
+ * previous esbuild `onResolve` plugin, which reached Vite through the
102
+ * deprecated `optimizeDeps.esbuildOptions` escape hatch.
103
+ *
104
+ * @returns {import('rolldown').Plugin} Rolldown plugin for optimizeDeps.
59
105
  */
60
106
  function makeTwigVirtualModuleOptimizerPlugin() {
61
107
  return {
62
108
  name: 'emulsify-twig-virtual-modules',
63
- setup(build) {
64
- build.onResolve(
65
- { filter: /^virtual:emulsify-twig-(?:globs|asset-sources)$/ },
66
- (args) => ({
67
- path: args.path,
68
- external: true,
69
- }),
70
- );
109
+ resolveId(source) {
110
+ if (!TWIG_VIRTUAL_MODULE_PATTERN.test(source)) return null;
111
+
112
+ // Marking these external leaves them in the module graph for Emulsify's
113
+ // virtual plugins to resolve during the normal Vite pipeline.
114
+ return { id: source, external: true };
71
115
  },
72
116
  };
73
117
  }
@@ -80,7 +124,7 @@ function makeTwigVirtualModuleOptimizerPlugin() {
80
124
  */
81
125
  export function createViteFinal(resolvedStorybookEnv) {
82
126
  return async function viteFinal(config) {
83
- const { mergeConfig } = await import('vite');
127
+ const { createLogger, mergeConfig } = await import('vite');
84
128
  const env = resolvedStorybookEnv;
85
129
  const storybookBuildConfig = config?.build || {};
86
130
 
@@ -125,9 +169,16 @@ export function createViteFinal(resolvedStorybookEnv) {
125
169
  '**/*.twig',
126
170
  ]),
127
171
  );
172
+ // Clear Vite's legacy `rollupOptions` alias before either object is
173
+ // spread, so the merged result never carries both it and `rolldownOptions`.
174
+ const baseOptimizeDeps = stripRollupOptionsAlias(
175
+ baseViteConfig?.optimizeDeps,
176
+ );
177
+ const storybookOptimizeDeps = stripRollupOptionsAlias(config?.optimizeDeps);
178
+
128
179
  const optimizeDepsInclude = mergeReactSingletonOptimizeDeps(
129
- baseViteConfig?.optimizeDeps?.include,
130
- config?.optimizeDeps?.include,
180
+ baseOptimizeDeps.include,
181
+ storybookOptimizeDeps.include,
131
182
  [
132
183
  'twig',
133
184
  '@emulsify/core/extensions/twig',
@@ -166,34 +217,48 @@ export function createViteFinal(resolvedStorybookEnv) {
166
217
  exclude: [],
167
218
  },
168
219
  optimizeDeps: {
169
- ...(baseViteConfig?.optimizeDeps || {}),
170
- ...(config?.optimizeDeps || {}),
220
+ ...baseOptimizeDeps,
221
+ ...storybookOptimizeDeps,
171
222
  include: optimizeDepsInclude,
172
223
  exclude: mergeTwigRuntimeOptimizeDepsExcludes(
173
- baseViteConfig?.optimizeDeps?.exclude,
174
- config?.optimizeDeps?.exclude,
224
+ baseOptimizeDeps.exclude,
225
+ storybookOptimizeDeps.exclude,
175
226
  ),
176
- esbuildOptions: {
177
- ...(baseViteConfig?.optimizeDeps?.esbuildOptions || {}),
178
- ...(config?.optimizeDeps?.esbuildOptions || {}),
227
+ rolldownOptions: {
228
+ ...(baseOptimizeDeps.rolldownOptions || {}),
229
+ ...(storybookOptimizeDeps.rolldownOptions || {}),
179
230
  plugins: [
180
- ...(baseViteConfig?.optimizeDeps?.esbuildOptions?.plugins || []),
181
- ...(config?.optimizeDeps?.esbuildOptions?.plugins || []),
231
+ ...(baseOptimizeDeps.rolldownOptions?.plugins || []),
232
+ ...(storybookOptimizeDeps.rolldownOptions?.plugins || []),
182
233
  makeTwigVirtualModuleOptimizerPlugin(),
183
234
  ],
184
- loader: {
185
- ...(baseViteConfig?.optimizeDeps?.esbuildOptions?.loader || {}),
186
- ...(config?.optimizeDeps?.esbuildOptions?.loader || {}),
187
- // Pre-bundle `.js` dependencies with the JSX loader for packages
188
- // that ship JSX without a `.jsx` extension.
235
+ moduleTypes: {
236
+ ...(baseOptimizeDeps.rolldownOptions?.moduleTypes || {}),
237
+ ...(storybookOptimizeDeps.rolldownOptions?.moduleTypes || {}),
238
+ // Pre-bundle `.js` dependencies as JSX for packages that ship JSX
239
+ // without a `.jsx` extension. Rolldown's `moduleTypes` is the
240
+ // successor to esbuild's `loader` map.
189
241
  '.js': 'jsx',
190
242
  },
191
243
  },
192
244
  },
193
245
  });
194
246
 
247
+ const mergedOptimizeDeps = stripRollupOptionsAlias(
248
+ mergedConfig.optimizeDeps,
249
+ );
250
+
195
251
  return {
196
252
  ...mergedConfig,
253
+
254
+ // Assigned after the merge rather than inside it, because `mergeConfig`
255
+ // deep-merges plain objects and a logger is an object of functions — merging
256
+ // Storybook's with ours would produce a hybrid belonging to neither. Spread
257
+ // last, it simply wins, and it delegates to whatever was already configured
258
+ // so Storybook keeps its prefixes for every message it is allowed to print.
259
+ customLogger: createDevServerLogger({
260
+ baseLogger: mergedConfig.customLogger || createLogger(),
261
+ }),
197
262
  build: {
198
263
  ...(mergedConfig.build || {}),
199
264
  ...(storybookBuildConfig.outDir
@@ -204,17 +269,17 @@ export function createViteFinal(resolvedStorybookEnv) {
204
269
  },
205
270
  resolve: mergeReactSingletonResolve(mergedConfig),
206
271
  optimizeDeps: {
207
- ...(mergedConfig.optimizeDeps || {}),
208
- include: mergeReactSingletonOptimizeDeps(
209
- mergedConfig.optimizeDeps?.include,
210
- ),
272
+ // `mergeConfig` walks enumerable keys, so the alias can reappear on the
273
+ // merged result even though both inputs were cleaned above.
274
+ ...mergedOptimizeDeps,
275
+ include: mergeReactSingletonOptimizeDeps(mergedOptimizeDeps.include),
211
276
  exclude: mergeTwigRuntimeOptimizeDepsExcludes(
212
- mergedConfig.optimizeDeps?.exclude,
277
+ mergedOptimizeDeps.exclude,
213
278
  ),
214
- esbuildOptions: {
215
- ...(mergedConfig.optimizeDeps?.esbuildOptions || {}),
216
- loader: {
217
- ...(mergedConfig.optimizeDeps?.esbuildOptions?.loader || {}),
279
+ rolldownOptions: {
280
+ ...(mergedOptimizeDeps.rolldownOptions || {}),
281
+ moduleTypes: {
282
+ ...(mergedOptimizeDeps.rolldownOptions?.moduleTypes || {}),
218
283
  '.js': 'jsx',
219
284
  },
220
285
  },
@@ -21,6 +21,10 @@ import {
21
21
  } from '../src/storybook/main-config.js';
22
22
  import { buildAssetStaticDirs } from './main-static-assets.js';
23
23
  import { createViteFinal } from './main-vite.js';
24
+ import {
25
+ installReadyReporter,
26
+ requestedPortFromArgv,
27
+ } from './ready-reporter.js';
24
28
 
25
29
  /**
26
30
  * Minimal subset of the resolved Emulsify environment used by this file.
@@ -143,6 +147,22 @@ const safeConfigOverrides = await loadConfigOverrides();
143
147
  */
144
148
  const resolvedStorybookEnv = resolveEnvironment();
145
149
 
150
+ /**
151
+ * Render Storybook's startup announcement in the Emulsify reporter's voice.
152
+ *
153
+ * Installed here because this module is itself a Storybook preset: `loadAllPresets`
154
+ * imports it, so a top-level await runs well before the dev server announces
155
+ * itself. Only the ready box is affected, and only when the internal logger is
156
+ * reachable — see `.storybook/ready-reporter.js` for why this is interception
157
+ * rather than `--quiet`.
158
+ *
159
+ * Restricted to `storybook dev`. A static `storybook build` prints no ready box,
160
+ * and the develop reporter deliberately leaves one-shot builds alone.
161
+ */
162
+ if (process.argv.includes('dev')) {
163
+ await installReadyReporter({ requestedPort: requestedPortFromArgv() });
164
+ }
165
+
146
166
  /**
147
167
  * Primary Storybook configuration object.
148
168
  * @type {StorybookConfig}
@@ -0,0 +1,230 @@
1
+ /**
2
+ * @file Route Storybook's startup announcement through the Emulsify reporter.
3
+ *
4
+ * `npm run develop` runs Vite and Storybook side by side under `concurrently`.
5
+ * Once the reporter quiets Vite and Rolldown, Storybook's rounded box is the
6
+ * only remaining block drawn in another tool's visual language, which makes one
7
+ * command look like two.
8
+ *
9
+ * ## Why interception rather than `--quiet`
10
+ *
11
+ * Storybook has a switch for this: `outputStartupInformation()` is called behind
12
+ * `options.quiet ||`, so `storybook dev --quiet` removes the box outright. It was
13
+ * not used, for two reasons.
14
+ *
15
+ * The first is distribution. The `storybook` script lives in each consuming
16
+ * project's `package.json`, so `--quiet` would make a cosmetic improvement
17
+ * depend on every consumer editing a script — and a project that did not would
18
+ * get no improvement at all. Interception ships with the package.
19
+ *
20
+ * The second is that the box carries information nothing else does. Consumers
21
+ * run `storybook dev --ci`, and `--ci` makes Storybook fall forward to the next
22
+ * free port without prompting. The resolved port only appears in the box. A
23
+ * developer whose previous session is still holding 6006 would otherwise open
24
+ * the requested port and see a stale instance, with nothing anywhere saying so.
25
+ * Parsing the box is what makes {@link renderReady}'s port drift warning
26
+ * possible.
27
+ *
28
+ * ## Failure behavior
29
+ *
30
+ * Everything here is best-effort. `storybook/internal/node-logger` is an
31
+ * internal subpath and the box payload is a formatted string, so both can change
32
+ * between Storybook releases. Every failure path leaves Storybook's own logger
33
+ * untouched, which means the worst outcome is the box that prints today.
34
+ */
35
+
36
+ import {
37
+ createStyler,
38
+ supportsColor,
39
+ supportsUnicode,
40
+ } from '../config/vite/plugins/reporter/format.js';
41
+ import { renderReady } from '../config/vite/plugins/reporter/render.js';
42
+
43
+ /**
44
+ * Matches the URL rows inside Storybook's startup box.
45
+ *
46
+ * The box lists `- Local:`, `- On your network:`, and optionally
47
+ * `- Other allowed hosts:`, each followed by whitespace and a URL. Matching the
48
+ * label rather than the position keeps the parse working if a row is added or
49
+ * reordered.
50
+ *
51
+ * @type {RegExp}
52
+ */
53
+ const URL_ROW_PATTERN = /^\s*-\s*(Local|On your network):\s*(\S+)/gim;
54
+
55
+ /**
56
+ * Identifies the startup box among every other boxed message Storybook draws.
57
+ *
58
+ * @type {RegExp}
59
+ */
60
+ const READY_PATTERN = /Storybook\s+ready/i;
61
+
62
+ /**
63
+ * Remove ANSI escape sequences so pattern matching sees plain text.
64
+ *
65
+ * @param {string} value - Possibly styled text.
66
+ * @returns {string} Plain text.
67
+ */
68
+ const stripAnsi = (value) =>
69
+ // eslint-disable-next-line no-control-regex
70
+ String(value).replace(/\[[0-9;]*m/g, '');
71
+
72
+ /**
73
+ * Extract the local and network URLs from a startup box payload.
74
+ *
75
+ * @param {string} message - Box contents.
76
+ * @returns {{local?: string, network?: string}} Parsed URLs.
77
+ */
78
+ export function parseReadyUrls(message) {
79
+ const urls = {};
80
+ if (typeof message !== 'string') return urls;
81
+
82
+ const plain = stripAnsi(message);
83
+ URL_ROW_PATTERN.lastIndex = 0;
84
+
85
+ let match = URL_ROW_PATTERN.exec(plain);
86
+ while (match) {
87
+ const [, label, url] = match;
88
+ urls[label.toLowerCase() === 'local' ? 'local' : 'network'] = url;
89
+ match = URL_ROW_PATTERN.exec(plain);
90
+ }
91
+
92
+ return urls;
93
+ }
94
+
95
+ /**
96
+ * Read the port out of a URL.
97
+ *
98
+ * @param {string|undefined} url - URL to inspect.
99
+ * @returns {string|undefined} Port, when present.
100
+ */
101
+ export function portFromUrl(url) {
102
+ if (!url) return undefined;
103
+
104
+ try {
105
+ const { port } = new URL(url);
106
+ return port || undefined;
107
+ } catch {
108
+ return undefined;
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Determine whether a boxed message is Storybook's startup announcement.
114
+ *
115
+ * @param {string} message - Box contents.
116
+ * @returns {boolean} TRUE when the message is the ready box.
117
+ */
118
+ export function isReadyBox(message) {
119
+ return typeof message === 'string' && READY_PATTERN.test(stripAnsi(message));
120
+ }
121
+
122
+ /**
123
+ * Replace Storybook's startup box with the Emulsify ready panel.
124
+ *
125
+ * Only the ready box is intercepted. Every other boxed message — and there are
126
+ * several, including migration notices and error summaries — passes through to
127
+ * Storybook untouched, because this exists to unify one visual element rather
128
+ * than to take over Storybook's output.
129
+ *
130
+ * @param {{
131
+ * logger?: {logBox?: Function, log?: Function},
132
+ * requestedPort?: string|number,
133
+ * write?: (line: string) => void,
134
+ * colorEnabled?: boolean,
135
+ * unicodeEnabled?: boolean
136
+ * }} [options] - Interception options.
137
+ * @returns {boolean} TRUE when the logger was wrapped.
138
+ */
139
+ export function interceptReadyBox({
140
+ logger,
141
+ requestedPort,
142
+ write = (line) => process.stdout.write(`${line}\n`),
143
+ colorEnabled,
144
+ unicodeEnabled,
145
+ } = {}) {
146
+ if (typeof logger?.logBox !== 'function') return false;
147
+
148
+ const styler = createStyler(
149
+ colorEnabled === undefined ? supportsColor() : colorEnabled,
150
+ );
151
+ const unicode =
152
+ unicodeEnabled === undefined ? supportsUnicode() : unicodeEnabled;
153
+
154
+ const originalLogBox = logger.logBox.bind(logger);
155
+
156
+ logger.logBox = (message, boxOptions) => {
157
+ if (!isReadyBox(message)) return originalLogBox(message, boxOptions);
158
+
159
+ try {
160
+ const urls = parseReadyUrls(message);
161
+ const actual = portFromUrl(urls.local);
162
+
163
+ const lines = renderReady({
164
+ service: 'storybook',
165
+ urls,
166
+ portDrift:
167
+ requestedPort && actual
168
+ ? { requested: requestedPort, actual }
169
+ : undefined,
170
+ unicode,
171
+ styler,
172
+ });
173
+
174
+ lines.forEach((line) => write(line));
175
+ return undefined;
176
+ } catch {
177
+ // A parse failure must not cost the developer the URLs they need.
178
+ return originalLogBox(message, boxOptions);
179
+ }
180
+ };
181
+
182
+ return true;
183
+ }
184
+
185
+ /**
186
+ * Install the interception against Storybook's real node logger.
187
+ *
188
+ * Called for its side effect during preset evaluation, which happens well before
189
+ * the server announces itself.
190
+ *
191
+ * @param {{requestedPort?: string|number}} [options] - Interception options.
192
+ * @returns {Promise<boolean>} TRUE when interception was installed.
193
+ */
194
+ export async function installReadyReporter({ requestedPort } = {}) {
195
+ try {
196
+ const { logger } = await import('storybook/internal/node-logger');
197
+ return interceptReadyBox({ logger, requestedPort });
198
+ } catch {
199
+ // Storybook changed the subpath or the export. Its own box still prints.
200
+ return false;
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Read the port requested on the command line.
206
+ *
207
+ * Storybook resolves `-p` / `--port` itself, but by the time the box is drawn the
208
+ * value has been replaced by whatever port was actually free. The request has to
209
+ * be captured from argv to have anything to compare against.
210
+ *
211
+ * @param {string[]} [argv] - Process arguments.
212
+ * @returns {string|undefined} Requested port.
213
+ */
214
+ export function requestedPortFromArgv(argv = process.argv) {
215
+ if (!Array.isArray(argv)) return undefined;
216
+
217
+ for (let index = 0; index < argv.length; index += 1) {
218
+ const arg = argv[index];
219
+
220
+ if (arg === '-p' || arg === '--port') {
221
+ const value = argv[index + 1];
222
+ return value && /^\d+$/.test(value) ? value : undefined;
223
+ }
224
+
225
+ const inline = /^--port=(\d+)$/.exec(arg);
226
+ if (inline) return inline[1];
227
+ }
228
+
229
+ return undefined;
230
+ }