@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.
- package/.storybook/main-vite.js +102 -37
- package/.storybook/main.js +20 -0
- package/.storybook/ready-reporter.js +230 -0
- package/README.md +186 -56
- package/config/a11y.config.js +2 -1
- package/config/vite/entries.js +4 -4
- package/config/vite/plugins/assets/copy-src-assets.js +120 -0
- package/config/vite/plugins/assets/copy-twig-files.js +130 -0
- 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 +367 -0
- package/config/vite/plugins/reporter/format.js +240 -0
- package/config/vite/plugins/reporter/index.js +417 -0
- package/config/vite/plugins/reporter/render.js +1332 -0
- package/config/vite/plugins/reporter/sass-logger.js +237 -0
- package/config/vite/plugins/reporter/source-roots.js +561 -0
- package/config/vite/plugins/reporter/verbosity.js +119 -0
- package/config/vite/plugins/reporter/vite-logger.js +249 -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 +91 -5
- package/package.json +95 -57
- 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/plugins/copy-src-assets.js +0 -76
- package/config/vite/plugins/copy-twig-files.js +0 -84
- 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
package/.storybook/main-vite.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
130
|
-
|
|
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
|
-
...
|
|
170
|
-
...
|
|
220
|
+
...baseOptimizeDeps,
|
|
221
|
+
...storybookOptimizeDeps,
|
|
171
222
|
include: optimizeDepsInclude,
|
|
172
223
|
exclude: mergeTwigRuntimeOptimizeDepsExcludes(
|
|
173
|
-
|
|
174
|
-
|
|
224
|
+
baseOptimizeDeps.exclude,
|
|
225
|
+
storybookOptimizeDeps.exclude,
|
|
175
226
|
),
|
|
176
|
-
|
|
177
|
-
...(
|
|
178
|
-
...(
|
|
227
|
+
rolldownOptions: {
|
|
228
|
+
...(baseOptimizeDeps.rolldownOptions || {}),
|
|
229
|
+
...(storybookOptimizeDeps.rolldownOptions || {}),
|
|
179
230
|
plugins: [
|
|
180
|
-
...(
|
|
181
|
-
...(
|
|
231
|
+
...(baseOptimizeDeps.rolldownOptions?.plugins || []),
|
|
232
|
+
...(storybookOptimizeDeps.rolldownOptions?.plugins || []),
|
|
182
233
|
makeTwigVirtualModuleOptimizerPlugin(),
|
|
183
234
|
],
|
|
184
|
-
|
|
185
|
-
...(
|
|
186
|
-
...(
|
|
187
|
-
// Pre-bundle `.js` dependencies
|
|
188
|
-
//
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
277
|
+
mergedOptimizeDeps.exclude,
|
|
213
278
|
),
|
|
214
|
-
|
|
215
|
-
...(
|
|
216
|
-
|
|
217
|
-
...(
|
|
279
|
+
rolldownOptions: {
|
|
280
|
+
...(mergedOptimizeDeps.rolldownOptions || {}),
|
|
281
|
+
moduleTypes: {
|
|
282
|
+
...(mergedOptimizeDeps.rolldownOptions?.moduleTypes || {}),
|
|
218
283
|
'.js': 'jsx',
|
|
219
284
|
},
|
|
220
285
|
},
|
package/.storybook/main.js
CHANGED
|
@@ -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
|
+
}
|