@emulsify/core 3.5.0 → 4.0.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 (110) hide show
  1. package/.cli/init.js +40 -31
  2. package/.storybook/_drupal.js +129 -8
  3. package/.storybook/css-components.js +13 -0
  4. package/.storybook/css-dist.js +5 -0
  5. package/.storybook/emulsifyTheme.js +9 -6
  6. package/.storybook/main.js +397 -106
  7. package/.storybook/manager.js +9 -16
  8. package/.storybook/preview.js +88 -110
  9. package/.storybook/utils.js +69 -74
  10. package/README.md +110 -59
  11. package/config/.stylelintrc.json +2 -6
  12. package/config/a11y.config.js +9 -5
  13. package/config/babel.config.js +6 -11
  14. package/config/eslint.config.js +31 -3
  15. package/config/postcss.config.js +5 -0
  16. package/config/vite/entries.js +227 -0
  17. package/config/vite/environment.js +39 -0
  18. package/config/vite/platforms.js +70 -0
  19. package/config/vite/plugins/copy-src-assets.js +76 -0
  20. package/config/vite/plugins/copy-twig-files.js +84 -0
  21. package/config/vite/plugins/css-asset-relativizer.js +40 -0
  22. package/config/vite/plugins/index.js +105 -0
  23. package/config/vite/plugins/mirror-components.js +358 -0
  24. package/config/vite/plugins/require-context.js +311 -0
  25. package/config/vite/plugins/source-file-index.js +184 -0
  26. package/config/vite/plugins/svg-sprite.js +117 -0
  27. package/config/vite/plugins/twig-extension-installers.js +36 -0
  28. package/config/vite/plugins/twig-module.js +1251 -0
  29. package/config/vite/plugins/virtual-twig-asset-sources.js +404 -0
  30. package/config/vite/plugins/virtual-twig-globs.js +136 -0
  31. package/config/vite/plugins/vituum-patch.js +167 -0
  32. package/config/vite/plugins/yaml-module.js +133 -0
  33. package/config/vite/plugins.js +12 -0
  34. package/config/vite/project-config.js +192 -0
  35. package/config/vite/project-extensions.js +177 -0
  36. package/config/vite/project-structure.js +447 -0
  37. package/config/vite/twig-extensions.js +109 -0
  38. package/config/vite/utils/fs-safe.js +66 -0
  39. package/config/vite/utils/paths.js +40 -0
  40. package/config/vite/utils/react-singleton.js +85 -0
  41. package/config/vite/utils/unique.js +36 -0
  42. package/config/vite/vite.config.js +161 -0
  43. package/package.json +164 -75
  44. package/scripts/a11y.js +70 -16
  45. package/scripts/audit-twig-stories.js +378 -0
  46. package/scripts/audit.js +1602 -0
  47. package/scripts/check-node-version.js +18 -0
  48. package/scripts/loadYaml.js +5 -1
  49. package/src/extensions/index.js +8 -0
  50. package/src/extensions/react/index.js +12 -0
  51. package/src/extensions/react/register.js +45 -0
  52. package/src/extensions/shared/attributes.js +308 -0
  53. package/src/extensions/shared/html.js +41 -0
  54. package/src/extensions/shared/lists.js +38 -0
  55. package/src/extensions/shared/object.js +22 -0
  56. package/src/extensions/twig/function-map.js +20 -0
  57. package/src/extensions/twig/functions/add-attributes.js +39 -0
  58. package/src/extensions/twig/functions/bem.js +166 -0
  59. package/src/extensions/twig/index.js +13 -0
  60. package/src/extensions/twig/register.js +52 -0
  61. package/src/extensions/twig/tag-map.js +16 -0
  62. package/src/extensions/twig/tags/switch.js +266 -0
  63. package/src/storybook/index.js +14 -0
  64. package/src/storybook/main-config.js +132 -0
  65. package/src/storybook/platform-behaviors.js +60 -0
  66. package/src/storybook/preview-parameters.js +81 -0
  67. package/src/storybook/render-twig.js +295 -0
  68. package/src/storybook/twig/drupal-filters.js +7 -0
  69. package/src/storybook/twig/include-function.js +109 -0
  70. package/src/storybook/twig/include.js +28 -0
  71. package/src/storybook/twig/reference-paths.js +294 -0
  72. package/src/storybook/twig/resolver.js +318 -0
  73. package/src/storybook/twig/setup.js +39 -0
  74. package/src/storybook/twig/source-events.js +5 -0
  75. package/src/storybook/twig/source-extensions.js +24 -0
  76. package/src/storybook/twig/source-function.js +239 -0
  77. package/src/storybook/twig/source.js +39 -0
  78. package/.all-contributorsrc +0 -45
  79. package/.editorconfig +0 -5
  80. package/.github/ISSUE_TEMPLATE/BUG_REPORT_TEMPLATE.md +0 -18
  81. package/.github/ISSUE_TEMPLATE/FEATURE_REQUEST_TEMPLATE.md +0 -11
  82. package/.github/PULL_REQUEST_TEMPLATE.md +0 -19
  83. package/.github/dependabot.yml +0 -6
  84. package/.github/workflows/addtoprojects.yml +0 -21
  85. package/.github/workflows/contributors.yml +0 -37
  86. package/.github/workflows/lint.yml +0 -22
  87. package/.github/workflows/semantic-release.yml +0 -24
  88. package/.husky/commit-msg +0 -2
  89. package/.husky/pre-commit +0 -2
  90. package/.nvmrc +0 -1
  91. package/.prettierignore +0 -4
  92. package/.storybook/polyfills/twig-include.js +0 -40
  93. package/.storybook/polyfills/twig-resolver.js +0 -70
  94. package/.storybook/polyfills/twig-source.js +0 -65
  95. package/.storybook/webpack.config.js +0 -269
  96. package/CODE_OF_CONDUCT.md +0 -56
  97. package/commitlint.config.js +0 -5
  98. package/config/jest.config.js +0 -19
  99. package/config/webpack/app.js +0 -1
  100. package/config/webpack/loaders.js +0 -167
  101. package/config/webpack/optimizers.js +0 -26
  102. package/config/webpack/plugins.js +0 -283
  103. package/config/webpack/resolves.js +0 -157
  104. package/config/webpack/sdc-loader.js +0 -16
  105. package/config/webpack/webpack.common.js +0 -272
  106. package/config/webpack/webpack.dev.js +0 -41
  107. package/config/webpack/webpack.prod.js +0 -6
  108. package/release.config.cjs +0 -30
  109. package/scripts/a11y.test.js +0 -172
  110. package/scripts/loadYaml.test.js +0 -30
@@ -1,20 +1,62 @@
1
- // .storybook/main.js
2
-
3
1
  /**
4
- * Storybook main configuration file.
5
- * This configures stories, static directories, addons, core builder,
6
- * framework, documentation settings, manager head styles, and overrides.
2
+ * Central Storybook configuration for Emulsify.
3
+ *
4
+ * This shared config defines the default Storybook behavior for consumers of
5
+ * the package, then lets a project layer local overrides on top at the end.
6
+ * The main custom behavior here is:
7
+ * - injecting manager/preview head markup
8
+ * - adapting the shared Vite config for Storybook
9
+ * - wiring Twig template discovery into the Storybook build
10
+ *
7
11
  * @module .storybook/main
8
12
  */
9
13
 
10
- import { resolve } from 'path';
11
14
  import fs from 'fs';
12
- import path from 'path';
13
- import { fileURLToPath } from 'url';
14
- import { createRequire } from 'module';
15
- import extendWebpackConfig from './webpack.config.js';
15
+ import path, { resolve } from 'path';
16
+ import { fileURLToPath, pathToFileURL } from 'url';
17
+ import viteConfig from '../config/vite/vite.config.js';
18
+ import { resolveEnvironment } from '../config/vite/environment.js';
19
+ import {
20
+ mergeReactSingletonOptimizeDeps,
21
+ mergeReactSingletonResolve,
22
+ } from '../config/vite/utils/react-singleton.js';
23
+ import { twigExtensionModuleSpecifiers } from '../config/vite/twig-extensions.js';
24
+ import {
25
+ applyStorybookConfigOverrides,
26
+ normalizeStorybookConfigOverrideModule,
27
+ } from '../src/storybook/main-config.js';
28
+
29
+ // Twig glob maps are provided by config/vite/plugins/virtual-twig-globs.js.
30
+
31
+ const twigVirtualModuleIds = [
32
+ 'virtual:emulsify-twig-globs',
33
+ 'virtual:emulsify-twig-asset-sources',
34
+ 'virtual:emulsify-twig-extension-installers',
35
+ ];
36
+
37
+ const twigRuntimeOptimizeDepsExclude = [
38
+ ...twigVirtualModuleIds,
39
+ '@emulsify/core/storybook/twig/source-function',
40
+ '@emulsify/core/storybook/twig/source',
41
+ '@emulsify/core/storybook/twig/resolver',
42
+ ];
43
+
44
+ /**
45
+ * Minimal subset of the resolved Emulsify environment used by this file.
46
+ *
47
+ * @typedef {object} StorybookEnvironment
48
+ * @property {string} projectDir - Absolute path to the consuming project root.
49
+ * @property {boolean} [structureOverrides] - Whether custom structure roots are enabled.
50
+ * @property {string[]} [structureRoots] - Absolute component root paths when overrides are active.
51
+ * @property {string[]} [componentRoots] - Absolute component roots in resolution order.
52
+ * @property {Record<string, string>} [namespaceRoots] - Twig namespace roots.
53
+ * @property {string} [srcDir] - Absolute path to the project's `src` directory when present.
54
+ */
16
55
 
17
- const require = createRequire(import.meta.url);
56
+ /**
57
+ * Storybook config type used for editor hints in this plain JS file.
58
+ * @typedef {import('@storybook/core-common').StorybookConfig} StorybookConfig
59
+ */
18
60
 
19
61
  /**
20
62
  * The full path to the current file (ESM compatible).
@@ -29,114 +71,227 @@ const _filename = fileURLToPath(import.meta.url);
29
71
  const _dirname = path.dirname(_filename);
30
72
 
31
73
  /**
32
- * Migrate the consumer Storybook theme import from "@storybook/theming" to
33
- * "storybook/theming" when needed.
74
+ * Reads an optional HTML fragment relative to this config file.
34
75
  *
35
- * This runs opportunistically during startup and never throws so Storybook
36
- * startup is resilient across all projects.
76
+ * Missing files are treated as empty content so downstream projects can opt in
77
+ * to extra markup without making Storybook fail on startup.
78
+ *
79
+ * @param {string} relativePath - Relative path from this file to the HTML fragment.
80
+ * @returns {string} File contents when the fragment exists, otherwise an empty string.
37
81
  */
38
- const migrateConsumerThemeImport = () => {
39
- try {
40
- const themeConfigPath = resolve(
41
- _dirname,
42
- '../../../../config/emulsify-core/storybook/theme.js',
43
- );
82
+ function readOptionalHtmlFragment(relativePath) {
83
+ const fragmentPath = resolve(_dirname, relativePath);
84
+
85
+ if (!fs.existsSync(fragmentPath)) {
86
+ return '';
87
+ }
44
88
 
45
- if (!fs.existsSync(themeConfigPath)) {
46
- return;
47
- }
89
+ return fs.readFileSync(fragmentPath, 'utf8');
90
+ }
48
91
 
49
- const originalThemeConfig = fs.readFileSync(themeConfigPath, 'utf8');
92
+ /**
93
+ * Keeps Storybook static directory config aligned to the consuming project.
94
+ *
95
+ * Storybook errors when a declared static directory is absent, so only expose
96
+ * project asset directories that exist in the current workspace.
97
+ *
98
+ * @param {Array<string|{from: string, to: string}>} staticDirs - Static directory entries.
99
+ * @returns {Array<string|{from: string, to: string}>} Existing static directory entries.
100
+ */
101
+ function existingStaticDirs(staticDirs) {
102
+ return staticDirs.filter((staticDir) => {
103
+ const directory =
104
+ typeof staticDir === 'string' ? staticDir : staticDir.from;
50
105
 
51
- if (!originalThemeConfig.includes('@storybook/theming')) {
52
- return;
53
- }
106
+ return directory && fs.existsSync(directory);
107
+ });
108
+ }
54
109
 
55
- const migratedThemeConfig = originalThemeConfig.replace(
56
- /(['"])@storybook\/theming\1/g,
57
- '$1storybook/theming$1',
58
- );
110
+ /**
111
+ * Merge Storybook and project optimizeDeps excludes with Core Twig runtime IDs.
112
+ *
113
+ * Storybook's dependency optimizer runs before normal Vite virtual module
114
+ * resolution. Core Twig runtime modules import virtual IDs that must stay in
115
+ * the Vite module graph so Emulsify's virtual plugins can resolve them.
116
+ *
117
+ * @param {...string[]} excludeLists - Existing optimizeDeps exclude arrays.
118
+ * @returns {string[]} Merged exclude list.
119
+ */
120
+ function mergeTwigRuntimeOptimizeDepsExcludes(...excludeLists) {
121
+ return Array.from(
122
+ new Set([
123
+ ...excludeLists.flatMap((excludeList) =>
124
+ Array.isArray(excludeList) ? excludeList : [],
125
+ ),
126
+ ...twigRuntimeOptimizeDepsExclude,
127
+ ]),
128
+ );
129
+ }
130
+
131
+ /**
132
+ * Keep Emulsify Twig virtual imports out of Storybook dependency prebundles.
133
+ *
134
+ * @returns {import('esbuild').Plugin} Esbuild plugin for optimizeDeps.
135
+ */
136
+ function makeTwigVirtualModuleOptimizerPlugin() {
137
+ return {
138
+ name: 'emulsify-twig-virtual-modules',
139
+ setup(build) {
140
+ build.onResolve(
141
+ { filter: /^virtual:emulsify-twig-(?:globs|asset-sources)$/ },
142
+ (args) => ({
143
+ path: args.path,
144
+ external: true,
145
+ }),
146
+ );
147
+ },
148
+ };
149
+ }
150
+
151
+ /**
152
+ * Reads optional project-level Storybook overrides.
153
+ *
154
+ * Downstream projects can provide this file, but the shared config also needs
155
+ * to load in package-level smoke tests where that project file is absent.
156
+ *
157
+ * @returns {Promise<{ config: object|Function, extendConfig?: Function, replaceAddons: boolean }>}
158
+ * Consumer overrides.
159
+ */
160
+ async function loadConfigOverrides() {
161
+ const overridePath = resolve(
162
+ _dirname,
163
+ '../../../../config/emulsify-core/storybook/main.js',
164
+ );
59
165
 
60
- if (migratedThemeConfig !== originalThemeConfig) {
61
- fs.writeFileSync(themeConfigPath, migratedThemeConfig, 'utf8');
62
- }
63
- } catch {
64
- // Ignore migration failures so Storybook startup is never blocked.
166
+ if (!fs.existsSync(overridePath)) {
167
+ return normalizeStorybookConfigOverrideModule();
168
+ }
169
+
170
+ const configOverrides = await import(pathToFileURL(overridePath).href);
171
+ return normalizeStorybookConfigOverrideModule(configOverrides);
172
+ }
173
+
174
+ /**
175
+ * Builds Storybook story globs from normalized project roots.
176
+ *
177
+ * Stories remain colocated with components, whether the project uses the
178
+ * recommended `src/components` layout, legacy root `components`, or explicit
179
+ * structure implementation directories.
180
+ *
181
+ * @param {StorybookEnvironment} env - Resolved project paths used by Storybook.
182
+ * @returns {string[]} Storybook story globs.
183
+ */
184
+ function buildStoryGlobs(env) {
185
+ if (Array.isArray(env.projectStructure?.storyRoots)) {
186
+ return env.projectStructure.storyRoots.map((root) =>
187
+ path
188
+ .resolve(root, '**/*.stories.@(js|jsx|ts|tsx)')
189
+ .split(path.sep)
190
+ .join('/'),
191
+ );
65
192
  }
66
- };
67
193
 
68
- migrateConsumerThemeImport();
194
+ const roots =
195
+ env.structureOverrides &&
196
+ Array.isArray(env.structureRoots) &&
197
+ env.structureRoots.length
198
+ ? env.structureRoots
199
+ : [
200
+ path.resolve(env.projectDir, 'src'),
201
+ path.resolve(env.projectDir, 'components'),
202
+ ];
203
+
204
+ return Array.from(new Set(roots.filter(Boolean))).map((root) =>
205
+ path
206
+ .resolve(root, '**/*.stories.@(js|jsx|ts|tsx)')
207
+ .split(path.sep)
208
+ .join('/'),
209
+ );
210
+ }
69
211
 
70
212
  /**
71
213
  * Safely apply any user-provided overrides or fall back to an empty object.
72
214
  * @type {object}
73
215
  */
74
- const safeConfigOverrides = (() => {
75
- try {
76
- const overridesModule = require('../../../../config/emulsify-core/storybook/main.js');
77
- return overridesModule.default || overridesModule || {};
78
- } catch {
79
- return {};
80
- }
81
- })();
216
+ const safeConfigOverrides = await loadConfigOverrides();
217
+
218
+ /**
219
+ * Environment details shared across this Storybook config load.
220
+ * @type {StorybookEnvironment}
221
+ */
222
+ const resolvedStorybookEnv = resolveEnvironment();
82
223
 
83
224
  /**
84
225
  * Primary Storybook configuration object.
85
- * @type {import('storybook/internal/types').StorybookConfig}
226
+ * @type {StorybookConfig}
86
227
  */
87
- const config = {
228
+ const baseConfig = {
88
229
  /**
89
- * Patterns for locating story files under src or components directories.
230
+ * Discover stories from both supported component roots.
231
+ *
232
+ * This shared config supports projects that keep stories under `src` as well
233
+ * as projects that expose a top-level `components` directory.
234
+ *
90
235
  * @type {string[]}
91
236
  */
92
- stories: ['../../../../@(src|components)/**/*.stories.@(js|jsx|ts|tsx)'],
237
+ stories: buildStoryGlobs(resolvedStorybookEnv),
93
238
 
94
239
  /**
95
- * Directories to serve as static assets in the Storybook build.
96
- * @type {string[]}
240
+ * Mount shared assets into Storybook's static file server.
241
+ *
242
+ * Anything referenced by URL inside stories should live in one of these
243
+ * directories so it works in both `storybook dev` and static builds.
244
+ *
245
+ * @type {Array<string|{from: string, to: string}>}
97
246
  */
98
247
  staticDirs: [
99
- '../../../../assets/images',
100
- '../../../../assets/icons',
101
- '../../../../dist',
102
- '../../../../assets/videos',
248
+ ...existingStaticDirs([
249
+ {
250
+ from: path.resolve(process.cwd(), 'assets'),
251
+ to: '/assets',
252
+ },
253
+ path.resolve(process.cwd(), 'dist'),
254
+ ]),
103
255
  ],
104
256
 
105
257
  /**
106
- * List of Storybook addons to enable various features.
258
+ * Enable the default addon set used by Emulsify.
259
+ *
260
+ * `a11y` adds accessibility tooling, `links` supports story-to-story
261
+ * navigation, and `themes` exposes theme switching in the Storybook UI.
262
+ *
107
263
  * @type {string[]}
108
264
  */
109
265
  addons: [
110
266
  '@storybook/addon-a11y',
111
267
  '@storybook/addon-links',
112
268
  '@storybook/addon-themes',
113
- '@storybook/addon-styling-webpack',
114
269
  ],
115
270
 
116
271
  /**
117
- * Core builder configuration for Storybook.
118
- * Storybook 9 splits the HTML renderer from the webpack builder, so the
119
- * builder must be declared explicitly instead of relying on html-webpack5.
120
- * @type {{builder: {name: string}, disableTelemetry: boolean}}
272
+ * Force the Vite builder and disable Storybook telemetry for shared usage.
273
+ * @type {{builder: string, disableTelemetry: boolean}}
121
274
  */
122
275
  core: {
123
- builder: {
124
- name: '@storybook/builder-webpack5',
125
- },
276
+ builder: '@storybook/builder-vite',
126
277
  disableTelemetry: true,
127
278
  },
128
279
 
129
280
  /**
130
- * Framework specification for Storybook's HTML renderer.
281
+ * Tell Storybook to use the React + Vite framework package.
131
282
  * @type {{name: string, options: object}}
132
283
  */
133
284
  framework: {
134
- name: '@storybook/server-webpack5',
285
+ name: '@storybook/react-vite',
135
286
  options: {},
136
287
  },
137
288
 
138
289
  /**
139
- * Documentation settings for Storybook autodocs.
290
+ * Disable automatic docs generation.
291
+ *
292
+ * Storybook will only render documentation pages that are authored
293
+ * explicitly instead of generating them from component metadata.
294
+ *
140
295
  * @type {{autodocs: boolean}}
141
296
  */
142
297
  docs: {
@@ -144,13 +299,17 @@ const config = {
144
299
  },
145
300
 
146
301
  /**
147
- * Custom styles injected into the Storybook manager (sidebar) head,
148
- * plus any external manager-head.html snippet.
149
- * @param {string} head - Existing head HTML.
150
- * @returns {string} Modified head HTML.
302
+ * Appends Emulsify branding to the Storybook manager UI.
303
+ *
304
+ * This only affects Storybook's chrome, such as the sidebar, toolbar, and
305
+ * addon panels. It does not affect the iframe where stories actually render.
306
+ *
307
+ * @param {string} head - Existing manager head markup provided by Storybook.
308
+ * @returns {string} Manager head markup with Emulsify additions appended.
151
309
  */
152
310
  managerHead: (head) => {
153
- // inline theme styles
311
+ // Keep the manager styling inline so consumers inherit the branded UI
312
+ // without having to maintain a separate manager-only stylesheet.
154
313
  const inlineStyles = `
155
314
  <style>
156
315
  :root {
@@ -167,7 +326,7 @@ const config = {
167
326
  --colors-purple: #8B1E7E;
168
327
  }
169
328
  .sidebar-container {
170
- background: url('https://raw.githubusercontent.com/fourkitchens/emulsify-core/main/assets/images/corner-bkg.png?token=GHSAT0AAAAAACIEXLVDMX56QK3ZIZWHWHTEZNYFYIA') no-repeat top left;
329
+ background-color: var(--colors-emulsify-blue-900);
171
330
  }
172
331
  .sidebar-container .sidebar-subheading {
173
332
  color: var(--colors-emulsify-blue-200);
@@ -177,7 +336,7 @@ const config = {
177
336
  .sidebar-container .sidebar-subheading button:focus {
178
337
  color: var(--colors-emulsify-blue-300);
179
338
  }
180
- /** Triangle icon **/
339
+ /* Triangle icon. */
181
340
  .sidebar-container .sidebar-subheading button span {
182
341
  color: var(--colors-emulsify-blue-300);
183
342
  }
@@ -252,54 +411,186 @@ const config = {
252
411
  }
253
412
  </style>
254
413
  `;
255
-
256
- // load external manager-head.html if present
257
- const externalManagerHeadPath = resolve(
258
- _dirname,
414
+ const externalManagerHtml = readOptionalHtmlFragment(
259
415
  '../../../../config/emulsify-core/storybook/manager-head.html',
260
416
  );
261
- let externalManagerHtml = '';
262
- if (fs.existsSync(externalManagerHeadPath)) {
263
- externalManagerHtml = fs.readFileSync(externalManagerHeadPath, 'utf8');
264
- }
265
417
 
266
418
  return `${head}
267
- ${inlineStyles}
268
- ${externalManagerHtml}`;
419
+ ${inlineStyles}
420
+ ${externalManagerHtml}`;
269
421
  },
270
422
 
271
423
  /**
272
- * Function to load and append an external preview-head.html into the preview iframe.
273
- * @param {string} head - Existing preview head HTML.
274
- * @returns {string} Combined head HTML including external snippet if present.
424
+ * Appends project-level head markup to the story preview iframe.
425
+ *
426
+ * This is the place for preview-only fonts, scripts, or meta tags that the
427
+ * rendered component output depends on.
428
+ *
429
+ * @param {string} head - Existing preview head markup provided by Storybook.
430
+ * @returns {string} Preview head markup with optional project HTML appended.
275
431
  */
276
432
  previewHead: (head) => {
277
- const externalHeadPath = resolve(
278
- _dirname,
433
+ const externalHtml = readOptionalHtmlFragment(
279
434
  '../../../../config/emulsify-core/storybook/preview-head.html',
280
435
  );
281
436
 
282
- let externalHtml = '';
283
- if (fs.existsSync(externalHeadPath)) {
284
- externalHtml = fs.readFileSync(externalHeadPath, 'utf8');
285
- }
286
-
287
437
  return `${head}
288
- ${externalHtml}`;
438
+ ${externalHtml}`;
289
439
  },
290
440
 
291
441
  /**
292
- * Forward Storybook 9's webpack hook to the existing shared webpack helper so
293
- * custom Twig, Sass, YAML, and resolver behavior still applies.
294
- * @param {object} storybookConfig - Storybook's generated webpack config.
295
- * @param {object} options - Storybook webpack hook options.
296
- * @returns {Promise<object>} The merged webpack config.
442
+ * Merges Storybook's generated Vite config with Emulsify's shared Vite config.
443
+ *
444
+ * Storybook supplies a baseline config, but Emulsify still needs to expose
445
+ * the resolved environment, expand filesystem access, and expose the Twig
446
+ * virtual glob module used by the runtime resolver.
447
+ *
448
+ * @param {import('vite').UserConfig} config - Storybook's generated Vite config.
449
+ * @returns {Promise<import('vite').UserConfig>} Final Vite config used by Storybook.
297
450
  */
298
- webpackFinal: async (storybookConfig, options) =>
299
- extendWebpackConfig({ config: storybookConfig, ...options }),
451
+ async viteFinal(config) {
452
+ const { mergeConfig } = await import('vite');
453
+ /** @type {StorybookEnvironment} */
454
+ const env = resolvedStorybookEnv;
455
+
456
+ // Keep using the `serve` branch of the shared Vite config here. Storybook
457
+ // has historically consumed that branch, while `mode` still reflects
458
+ // whether Storybook is running in development or production.
459
+ const mode = config?.mode || 'development';
460
+ const baseViteConfig =
461
+ typeof viteConfig === 'function'
462
+ ? await viteConfig({ command: 'serve', mode })
463
+ : viteConfig;
464
+ const existingDefine = (config && config.define) || {};
465
+ const viteDefine = (baseViteConfig && baseViteConfig.define) || {};
466
+
467
+ // Allow Storybook's dev server to read component sources from the project
468
+ // root and any structure override paths used by Emulsify consumers.
469
+ const allowList = new Set([
470
+ ...(config?.server?.fs?.allow || []),
471
+ env.projectDir,
472
+ path.resolve(env.projectDir, 'src'),
473
+ path.resolve(env.projectDir, 'components'),
474
+ path.resolve(env.projectDir, 'dist'),
475
+ ...(Array.isArray(env.projectStructure?.sourceRoots)
476
+ ? env.projectStructure.sourceRoots
477
+ : []),
478
+ ...(Array.isArray(env.componentRoots) ? env.componentRoots : []),
479
+ ...(Array.isArray(env.structureRoots) ? env.structureRoots : []),
480
+ ...(env.namespaceRoots && typeof env.namespaceRoots === 'object'
481
+ ? Object.values(env.namespaceRoots)
482
+ : []),
483
+ ...(Array.isArray(env.projectStructure?.assetRoots)
484
+ ? env.projectStructure.assetRoots
485
+ : []),
486
+ ]);
487
+
488
+ // Twig files are loaded through custom resolvers/plugins, so they need to
489
+ // be treated as importable assets by Storybook's Vite pipeline.
490
+ const assetsInclude = Array.from(
491
+ new Set([
492
+ ...(config.assetsInclude || []),
493
+ ...(baseViteConfig.assetsInclude || []),
494
+ '**/*.twig',
495
+ ]),
496
+ );
497
+ const optimizeDepsInclude = mergeReactSingletonOptimizeDeps(
498
+ baseViteConfig?.optimizeDeps?.include,
499
+ config?.optimizeDeps?.include,
500
+ [
501
+ 'twig',
502
+ '@emulsify/core/extensions/twig',
503
+ ...twigExtensionModuleSpecifiers(env),
504
+ ],
505
+ );
300
506
 
301
- // Merge in user overrides without modifying original logic
302
- ...safeConfigOverrides,
507
+ const mergedConfig = mergeConfig(config, {
508
+ ...baseViteConfig,
509
+ resolve: mergeReactSingletonResolve(baseViteConfig, config),
510
+ define: {
511
+ // Preserve shared and Storybook-provided constants, then publish the
512
+ // resolved Emulsify environment to client-side code.
513
+ ...viteDefine,
514
+ ...existingDefine,
515
+ __EMULSIFY_ENV__: JSON.stringify(env),
516
+ 'globalThis.__EMULSIFY_ENV__': JSON.stringify(env),
517
+ },
518
+ server: {
519
+ ...(baseViteConfig?.server || {}),
520
+ fs: {
521
+ allow: Array.from(allowList),
522
+ },
523
+ },
524
+ assetsInclude,
525
+ plugins: [...(baseViteConfig?.plugins || [])],
526
+ esbuild: {
527
+ // Some downstream code is authored as `.js` files containing JSX, so
528
+ // keep Storybook's esbuild settings aligned with the shared Vite config.
529
+ jsx: 'automatic',
530
+ loader: 'jsx',
531
+ include: /.*\.jsx?$/,
532
+ exclude: [],
533
+ },
534
+ optimizeDeps: {
535
+ ...(baseViteConfig?.optimizeDeps || {}),
536
+ ...(config?.optimizeDeps || {}),
537
+ include: optimizeDepsInclude,
538
+ exclude: mergeTwigRuntimeOptimizeDepsExcludes(
539
+ baseViteConfig?.optimizeDeps?.exclude,
540
+ config?.optimizeDeps?.exclude,
541
+ ),
542
+ esbuildOptions: {
543
+ ...(baseViteConfig?.optimizeDeps?.esbuildOptions || {}),
544
+ ...(config?.optimizeDeps?.esbuildOptions || {}),
545
+ plugins: [
546
+ ...(baseViteConfig?.optimizeDeps?.esbuildOptions?.plugins || []),
547
+ ...(config?.optimizeDeps?.esbuildOptions?.plugins || []),
548
+ makeTwigVirtualModuleOptimizerPlugin(),
549
+ ],
550
+ loader: {
551
+ ...(baseViteConfig?.optimizeDeps?.esbuildOptions?.loader || {}),
552
+ ...(config?.optimizeDeps?.esbuildOptions?.loader || {}),
553
+ // Pre-bundle `.js` dependencies with the JSX loader for packages
554
+ // that ship JSX without a `.jsx` extension.
555
+ '.js': 'jsx',
556
+ },
557
+ },
558
+ },
559
+ });
560
+
561
+ return {
562
+ ...mergedConfig,
563
+ resolve: mergeReactSingletonResolve(mergedConfig),
564
+ optimizeDeps: {
565
+ ...(mergedConfig.optimizeDeps || {}),
566
+ include: mergeReactSingletonOptimizeDeps(
567
+ mergedConfig.optimizeDeps?.include,
568
+ ),
569
+ exclude: mergeTwigRuntimeOptimizeDepsExcludes(
570
+ mergedConfig.optimizeDeps?.exclude,
571
+ ),
572
+ esbuildOptions: {
573
+ ...(mergedConfig.optimizeDeps?.esbuildOptions || {}),
574
+ loader: {
575
+ ...(mergedConfig.optimizeDeps?.esbuildOptions?.loader || {}),
576
+ '.js': 'jsx',
577
+ },
578
+ },
579
+ },
580
+ };
581
+ },
303
582
  };
304
583
 
584
+ /**
585
+ * Primary Storybook configuration after project overrides have been applied.
586
+ * Project `addons` append to Emulsify defaults unless replacement is requested.
587
+ *
588
+ * @type {StorybookConfig}
589
+ */
590
+ const config = await applyStorybookConfigOverrides(
591
+ baseConfig,
592
+ safeConfigOverrides,
593
+ { env: resolvedStorybookEnv },
594
+ );
595
+
305
596
  export default config;
@@ -1,7 +1,9 @@
1
- // .storybook/manager.js
1
+ /**
2
+ * @file Storybook manager bootstrap and theme selection.
3
+ */
2
4
 
3
5
  import { addons } from 'storybook/manager-api';
4
- import emulsifyTheme from './emulsifyTheme.js';
6
+ import emulsifyTheme from './emulsifyTheme';
5
7
 
6
8
  /**
7
9
  * Dynamically import the user-provided Storybook theme override.
@@ -9,37 +11,28 @@ import emulsifyTheme from './emulsifyTheme.js';
9
11
  */
10
12
  import('../../../../config/emulsify-core/storybook/theme')
11
13
  /**
12
- * Handle successful dynamic import of the theme module.
14
+ * Apply a project theme override when one exists.
15
+ *
13
16
  * @param {{ default: object }} module - The imported theme module.
14
17
  */
15
18
  .then(({ default: customTheme }) => {
16
- /**
17
- * Determine if the imported theme object is empty or not.
18
- * @type {boolean}
19
- */
19
+ // Empty override files should still fall back to the package theme.
20
20
  const isEmptyObject =
21
21
  !customTheme ||
22
22
  (typeof customTheme === 'object' &&
23
23
  Object.keys(customTheme).length === 0);
24
24
 
25
- /**
26
- * Apply the chosen theme to Storybook’s manager UI configuration.
27
- * @type {{ theme: object }}
28
- */
29
25
  addons.setConfig({
30
26
  theme: isEmptyObject ? emulsifyTheme : customTheme,
31
27
  });
32
28
  })
33
29
  /**
34
- * Handle failure of the dynamic import (e.g., file not found).
30
+ * Fall back to the default theme when the project override is absent.
31
+ *
35
32
  * @returns {void}
36
33
  */
37
34
  .catch(() => {
38
35
  addons.setConfig({
39
- /**
40
- * Fallback to the default Emulsify theme on import error.
41
- * @type {{ theme: object }}
42
- */
43
36
  theme: emulsifyTheme,
44
37
  });
45
38
  });