@emulsify/core 3.4.1 → 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 +10 -7
  6. package/.storybook/main.js +417 -65
  7. package/.storybook/manager.js +11 -18
  8. package/.storybook/preview.js +93 -37
  9. package/.storybook/utils.js +70 -69
  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 +5 -0
  14. package/config/eslint.config.js +6 -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 +168 -88
  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 -36
  93. package/.storybook/polyfills/twig-resolver.js +0 -68
  94. package/.storybook/polyfills/twig-source.js +0 -54
  95. package/.storybook/webpack.config.js +0 -193
  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 -17
  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 -268
  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,17 +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 configOverrides from '../../../../config/emulsify-core/storybook/main.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
+ */
55
+
56
+ /**
57
+ * Storybook config type used for editor hints in this plain JS file.
58
+ * @typedef {import('@storybook/core-common').StorybookConfig} StorybookConfig
59
+ */
15
60
 
16
61
  /**
17
62
  * The full path to the current file (ESM compatible).
@@ -23,69 +68,230 @@ const _filename = fileURLToPath(import.meta.url);
23
68
  * The directory name of the current module file.
24
69
  * @type {string}
25
70
  */
26
- const _dirname = path.dirname(_filename);
71
+ const _dirname = path.dirname(_filename);
72
+
73
+ /**
74
+ * Reads an optional HTML fragment relative to this config file.
75
+ *
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.
81
+ */
82
+ function readOptionalHtmlFragment(relativePath) {
83
+ const fragmentPath = resolve(_dirname, relativePath);
84
+
85
+ if (!fs.existsSync(fragmentPath)) {
86
+ return '';
87
+ }
88
+
89
+ return fs.readFileSync(fragmentPath, 'utf8');
90
+ }
91
+
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;
105
+
106
+ return directory && fs.existsSync(directory);
107
+ });
108
+ }
109
+
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
+ );
165
+
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
+ );
192
+ }
193
+
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
+ }
27
211
 
28
212
  /**
29
213
  * Safely apply any user-provided overrides or fall back to an empty object.
30
214
  * @type {object}
31
215
  */
32
- const safeConfigOverrides = configOverrides || {};
216
+ const safeConfigOverrides = await loadConfigOverrides();
217
+
218
+ /**
219
+ * Environment details shared across this Storybook config load.
220
+ * @type {StorybookEnvironment}
221
+ */
222
+ const resolvedStorybookEnv = resolveEnvironment();
33
223
 
34
224
  /**
35
225
  * Primary Storybook configuration object.
36
- * @type {import('@storybook/core-common').StorybookConfig}
226
+ * @type {StorybookConfig}
37
227
  */
38
- const config = {
228
+ const baseConfig = {
39
229
  /**
40
- * 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
+ *
41
235
  * @type {string[]}
42
236
  */
43
- stories: [
44
- '../../../../(src|components)/**/*.stories.@(js|jsx|ts|tsx)',
45
- ],
237
+ stories: buildStoryGlobs(resolvedStorybookEnv),
46
238
 
47
239
  /**
48
- * Directories to serve as static assets in the Storybook build.
49
- * @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}>}
50
246
  */
51
247
  staticDirs: [
52
- '../../../../assets/images',
53
- '../../../../assets/icons',
54
- '../../../../dist',
248
+ ...existingStaticDirs([
249
+ {
250
+ from: path.resolve(process.cwd(), 'assets'),
251
+ to: '/assets',
252
+ },
253
+ path.resolve(process.cwd(), 'dist'),
254
+ ]),
55
255
  ],
56
256
 
57
257
  /**
58
- * 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
+ *
59
263
  * @type {string[]}
60
264
  */
61
265
  addons: [
62
- '../../../@storybook/addon-a11y',
63
- '../../../@storybook/addon-links',
64
- '../../../@storybook/addon-essentials',
65
- '../../../@storybook/addon-themes',
66
- '../../../@storybook/addon-styling-webpack',
266
+ '@storybook/addon-a11y',
267
+ '@storybook/addon-links',
268
+ '@storybook/addon-themes',
67
269
  ],
68
270
 
69
271
  /**
70
- * Core builder configuration for Storybook.
272
+ * Force the Vite builder and disable Storybook telemetry for shared usage.
71
273
  * @type {{builder: string, disableTelemetry: boolean}}
72
274
  */
73
275
  core: {
74
- builder: 'webpack5',
276
+ builder: '@storybook/builder-vite',
75
277
  disableTelemetry: true,
76
278
  },
77
279
 
78
280
  /**
79
- * Framework specification for Storybook (HTML + Webpack5).
281
+ * Tell Storybook to use the React + Vite framework package.
80
282
  * @type {{name: string, options: object}}
81
283
  */
82
284
  framework: {
83
- name: '@storybook/html-webpack5',
285
+ name: '@storybook/react-vite',
84
286
  options: {},
85
287
  },
86
288
 
87
289
  /**
88
- * 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
+ *
89
295
  * @type {{autodocs: boolean}}
90
296
  */
91
297
  docs: {
@@ -93,13 +299,17 @@ const config = {
93
299
  },
94
300
 
95
301
  /**
96
- * Custom styles injected into the Storybook manager (sidebar) head,
97
- * plus any external manager-head.html snippet.
98
- * @param {string} head - Existing head HTML.
99
- * @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.
100
309
  */
101
310
  managerHead: (head) => {
102
- // 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.
103
313
  const inlineStyles = `
104
314
  <style>
105
315
  :root {
@@ -116,7 +326,7 @@ const config = {
116
326
  --colors-purple: #8B1E7E;
117
327
  }
118
328
  .sidebar-container {
119
- 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);
120
330
  }
121
331
  .sidebar-container .sidebar-subheading {
122
332
  color: var(--colors-emulsify-blue-200);
@@ -126,7 +336,7 @@ const config = {
126
336
  .sidebar-container .sidebar-subheading button:focus {
127
337
  color: var(--colors-emulsify-blue-300);
128
338
  }
129
- /** Triangle icon **/
339
+ /* Triangle icon. */
130
340
  .sidebar-container .sidebar-subheading button span {
131
341
  color: var(--colors-emulsify-blue-300);
132
342
  }
@@ -201,44 +411,186 @@ const config = {
201
411
  }
202
412
  </style>
203
413
  `;
204
-
205
- // load external manager-head.html if present
206
- const externalManagerHeadPath = resolve(
207
- _dirname,
208
- '../../../../config/emulsify-core/storybook/manager-head.html'
414
+ const externalManagerHtml = readOptionalHtmlFragment(
415
+ '../../../../config/emulsify-core/storybook/manager-head.html',
209
416
  );
210
- let externalManagerHtml = '';
211
- if (fs.existsSync(externalManagerHeadPath)) {
212
- externalManagerHtml = fs.readFileSync(externalManagerHeadPath, 'utf8');
213
- }
214
417
 
215
418
  return `${head}
216
- ${inlineStyles}
217
- ${externalManagerHtml}`;
419
+ ${inlineStyles}
420
+ ${externalManagerHtml}`;
218
421
  },
219
422
 
220
423
  /**
221
- * Function to load and append an external preview-head.html into the preview iframe.
222
- * @param {string} head - Existing preview head HTML.
223
- * @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.
224
431
  */
225
432
  previewHead: (head) => {
226
- const externalHeadPath = resolve(
227
- _dirname,
228
- '../../../../config/emulsify-core/storybook/preview-head.html'
433
+ const externalHtml = readOptionalHtmlFragment(
434
+ '../../../../config/emulsify-core/storybook/preview-head.html',
229
435
  );
230
436
 
231
- let externalHtml = '';
232
- if (fs.existsSync(externalHeadPath)) {
233
- externalHtml = fs.readFileSync(externalHeadPath, 'utf8');
234
- }
235
-
236
437
  return `${head}
237
- ${externalHtml}`;
438
+ ${externalHtml}`;
238
439
  },
239
440
 
240
- // Merge in user overrides without modifying original logic
241
- ...safeConfigOverrides,
441
+ /**
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.
450
+ */
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
+ );
506
+
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
+ },
242
582
  };
243
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
+
244
596
  export default config;
@@ -1,6 +1,8 @@
1
- // .storybook/manager.js
1
+ /**
2
+ * @file Storybook manager bootstrap and theme selection.
3
+ */
2
4
 
3
- import { addons } from '@storybook/manager-api';
5
+ import { addons } from 'storybook/manager-api';
4
6
  import emulsifyTheme from './emulsifyTheme';
5
7
 
6
8
  /**
@@ -9,37 +11,28 @@ import emulsifyTheme from './emulsifyTheme';
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
- (typeof customTheme === 'object' && Object.keys(customTheme).length === 0);
22
+ (typeof customTheme === 'object' &&
23
+ Object.keys(customTheme).length === 0);
23
24
 
24
- /**
25
- * Apply the chosen theme to Storybook’s manager UI configuration.
26
- * @type {{ theme: object }}
27
- */
28
25
  addons.setConfig({
29
26
  theme: isEmptyObject ? emulsifyTheme : customTheme,
30
27
  });
31
28
  })
32
29
  /**
33
- * 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
+ *
34
32
  * @returns {void}
35
33
  */
36
34
  .catch(() => {
37
35
  addons.setConfig({
38
- /**
39
- * Fallback to the default Emulsify theme on import error.
40
- * @type {{ theme: object }}
41
- */
42
36
  theme: emulsifyTheme,
43
37
  });
44
38
  });
45
-