@emulsify/core 4.3.2 → 4.5.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 (54) hide show
  1. package/.storybook/main-static-assets.js +5 -8
  2. package/.storybook/main-vite.js +11 -3
  3. package/README.md +14 -6
  4. package/config/a11y-wcag22.js +11 -0
  5. package/config/vite/entries.js +7 -2
  6. package/config/vite/environment.js +4 -0
  7. package/config/vite/plugins/assets/asset-url-rebase.js +241 -0
  8. package/config/vite/plugins/assets/copy-src-assets.js +82 -12
  9. package/config/vite/plugins/assets/copy-twig-files.js +85 -16
  10. package/config/vite/plugins/assets/css-asset-rebase.js +306 -0
  11. package/config/vite/plugins/assets/css-asset-relativizer.js +301 -21
  12. package/config/vite/plugins/assets/development-source-maps.js +273 -0
  13. package/config/vite/plugins/assets/mirror-components.js +98 -82
  14. package/config/vite/plugins/assets/output-freshness.js +235 -0
  15. package/config/vite/plugins/assets/source-file-index.js +13 -13
  16. package/config/vite/plugins/assets/stable-watch-output.js +165 -0
  17. package/config/vite/plugins/assets/storybook-output.js +27 -0
  18. package/config/vite/plugins/index.js +95 -9
  19. package/config/vite/plugins/reporter/asset-resolver.js +34 -6
  20. package/config/vite/plugins/reporter/build-errors.js +7 -3
  21. package/config/vite/plugins/reporter/diagnostics.js +140 -10
  22. package/config/vite/plugins/reporter/index.js +380 -75
  23. package/config/vite/plugins/reporter/render.js +297 -44
  24. package/config/vite/plugins/reporter/sass-logger.js +30 -0
  25. package/config/vite/plugins/reporter/source-roots.js +101 -21
  26. package/config/vite/plugins/reporter/strict-mode.js +99 -0
  27. package/config/vite/plugins/reporter/vite-logger.js +220 -8
  28. package/config/vite/plugins/reporter/watch-mode.js +6 -2
  29. package/config/vite/plugins/twig/twig-module.js +35 -258
  30. package/config/vite/plugins/twig/virtual-twig-asset-sources.js +48 -49
  31. package/config/vite/project-config.js +121 -21
  32. package/config/vite/project-structure.js +6 -0
  33. package/config/vite/utils/asset-roots.js +205 -0
  34. package/config/vite/utils/css-urls.js +350 -0
  35. package/config/vite/utils/fs-safe.js +38 -1
  36. package/config/vite/utils/source-directory-skips.js +13 -0
  37. package/config/vite/utils/source-maps.js +88 -0
  38. package/config/vite/utils/twig-component-resolver.js +316 -0
  39. package/config/vite/vite.config.js +106 -42
  40. package/package.json +54 -40
  41. package/scripts/a11y.js +88 -9
  42. package/scripts/audit/checks/css-asset-references.js +256 -24
  43. package/scripts/audit/checks/twig-references.js +16 -5
  44. package/scripts/audit/fix.js +836 -0
  45. package/scripts/audit/index.js +10 -2
  46. package/scripts/audit/lib/css.js +41 -35
  47. package/scripts/audit/lib/story-ast.js +392 -0
  48. package/scripts/audit/lib/story-render-paths.js +600 -0
  49. package/scripts/audit/lib/story-selection.js +190 -0
  50. package/scripts/audit/lib/twig.js +372 -80
  51. package/scripts/audit/report.js +83 -5
  52. package/scripts/audit-twig-stories.js +73 -3
  53. package/scripts/audit.js +87 -2
  54. package/src/storybook/twig/source-function.js +14 -10
@@ -11,7 +11,7 @@
11
11
 
12
12
  import fs from 'fs';
13
13
  import { readFile, stat } from 'fs/promises';
14
- import { basename, dirname, isAbsolute, relative, resolve } from 'path';
14
+ import { basename, dirname, relative, resolve } from 'path';
15
15
  import Twig from 'twig';
16
16
 
17
17
  import {
@@ -27,6 +27,12 @@ import {
27
27
  import { firstExistingPath, safeExists } from '../../utils/fs-safe.js';
28
28
  import { createLruCache } from '../../utils/lru.js';
29
29
  import { toPosixPath } from '../../utils/paths.js';
30
+ import {
31
+ buildTemplateFileCandidates,
32
+ isWithinRoot,
33
+ parseTwigNamespaceReference,
34
+ resolveComponentReference,
35
+ } from '../../utils/twig-component-resolver.js';
30
36
  import { unique } from '../../../../src/extensions/shared/lists.js';
31
37
 
32
38
  /** Twig token types that can reference another template file. */
@@ -338,29 +344,6 @@ const collectStaticSourceReferences = (tokens = []) => [
338
344
  ),
339
345
  ];
340
346
 
341
- /**
342
- * Build likely filesystem candidates for a Twig template reference.
343
- *
344
- * @param {string} baseDir - Directory used as the resolution root.
345
- * @param {string} templatePath - Template path from Twig source.
346
- * @returns {string[]} Candidate absolute paths.
347
- */
348
- const buildTemplateFileCandidates = (baseDir, templatePath) => {
349
- const normalizedTemplatePath = toPosixPath(templatePath);
350
- const withoutTwigExt = normalizedTemplatePath.replace(/\.twig$/i, '');
351
- const stem = basename(withoutTwigExt);
352
-
353
- return unique(
354
- [
355
- resolve(baseDir, normalizedTemplatePath),
356
- resolve(baseDir, `${normalizedTemplatePath}.twig`),
357
- resolve(baseDir, `${normalizedTemplatePath}.html.twig`),
358
- resolve(baseDir, withoutTwigExt, `${stem}.twig`),
359
- resolve(baseDir, withoutTwigExt, `${stem}.html.twig`),
360
- ].filter(Boolean),
361
- );
362
- };
363
-
364
347
  /**
365
348
  * Return the first candidate that exists as a file.
366
349
  *
@@ -376,60 +359,6 @@ const findExistingTemplateFile = (paths) =>
376
359
  }
377
360
  });
378
361
 
379
- /**
380
- * Determine whether a file path is equal to or below a candidate root.
381
- *
382
- * @param {string} root - Absolute root path.
383
- * @param {string} filePath - Absolute file path.
384
- * @returns {boolean} TRUE when the file belongs to the root.
385
- */
386
- const isWithinRoot = (root, filePath) => {
387
- const rootRelativePath = relative(root, filePath);
388
- return (
389
- rootRelativePath === '' ||
390
- (!!rootRelativePath &&
391
- !rootRelativePath.startsWith('..') &&
392
- !isAbsolute(rootRelativePath))
393
- );
394
- };
395
-
396
- /**
397
- * Return the first component template candidate contained by its configured root.
398
- *
399
- * Both lexical and real paths are checked so `..` segments and symlinks cannot
400
- * escape the component root.
401
- *
402
- * @param {string[]} paths - Candidate absolute paths.
403
- * @param {string} componentRoot - Absolute component root path.
404
- * @returns {string|undefined} Existing component template path.
405
- */
406
- const findExistingComponentTemplateFile = (paths, componentRoot) => {
407
- const absoluteRoot = resolve(componentRoot);
408
- let realRoot;
409
-
410
- try {
411
- realRoot = fs.realpathSync(absoluteRoot);
412
- } catch {
413
- return undefined;
414
- }
415
-
416
- return paths.filter(Boolean).find((filePath) => {
417
- const absoluteFilePath = resolve(filePath);
418
- if (!isWithinRoot(absoluteRoot, absoluteFilePath)) {
419
- return false;
420
- }
421
-
422
- try {
423
- return (
424
- fs.statSync(absoluteFilePath).isFile() &&
425
- isWithinRoot(realRoot, fs.realpathSync(absoluteFilePath))
426
- );
427
- } catch {
428
- return false;
429
- }
430
- });
431
- };
432
-
433
362
  /**
434
363
  * Find the most specific configured Twig root for a template file.
435
364
  *
@@ -548,161 +477,6 @@ const rewriteTemplateReferencesToIds = (tokens = [], fromFilePath, options) =>
548
477
  return nextToken;
549
478
  });
550
479
 
551
- /**
552
- * Resolve Twig namespace syntax to a namespace root and relative path.
553
- *
554
- * @param {string} templatePath - Template reference from Twig source.
555
- * @param {Record<string, string>} [namespaces={}] - Namespace root map.
556
- * @returns {{ namespace: string, root: string, path: string }|null}
557
- * Namespace lookup result.
558
- */
559
- const parseTwigNamespaceReference = (templatePath, namespaces = {}) => {
560
- const namespaceNames = Object.keys(namespaces);
561
- const atNamespace = templatePath.match(/^@([^/]+)\/(.+)$/);
562
- if (atNamespace && namespaces[atNamespace[1]]) {
563
- return {
564
- namespace: atNamespace[1],
565
- root: namespaces[atNamespace[1]],
566
- path: atNamespace[2],
567
- };
568
- }
569
-
570
- const doubleColon = templatePath.match(/^([^:]+)::(.+)$/);
571
- if (doubleColon && namespaces[doubleColon[1]]) {
572
- return {
573
- namespace: doubleColon[1],
574
- root: namespaces[doubleColon[1]],
575
- path: doubleColon[2],
576
- };
577
- }
578
-
579
- const singleColon = templatePath.match(/^([^:/.]+):(.+)$/);
580
- if (singleColon && namespaces[singleColon[1]]) {
581
- return {
582
- namespace: singleColon[1],
583
- root: namespaces[singleColon[1]],
584
- path: singleColon[2],
585
- };
586
- }
587
-
588
- const slashNamespace = namespaceNames.find((namespace) =>
589
- templatePath.startsWith(`${namespace}/`),
590
- );
591
- if (slashNamespace) {
592
- return {
593
- namespace: slashNamespace,
594
- // Namespace names come from the normalized Twig namespace map.
595
- root: namespaces[slashNamespace],
596
- path: templatePath.slice(slashNamespace.length + 1),
597
- };
598
- }
599
-
600
- return null;
601
- };
602
-
603
- /**
604
- * Return grouping directories below the configured component root.
605
- *
606
- * Breadth-first traversal preserves direct and one-level behavior before
607
- * searching deeper groups. Siblings use code-point order so duplicate
608
- * shorthand names resolve consistently across filesystems.
609
- *
610
- * @param {string} componentRoot - Absolute component root path.
611
- * @returns {string[]} Absolute grouping directory paths.
612
- */
613
- const componentGroupRoots = (componentRoot) => {
614
- if (!componentRoot) return [];
615
-
616
- const absoluteRoot = resolve(componentRoot);
617
- if (componentGroupRootsCache.has(absoluteRoot)) {
618
- return componentGroupRootsCache.get(absoluteRoot);
619
- }
620
-
621
- const groupRoots = [];
622
- const pendingDirectories = [absoluteRoot];
623
-
624
- for (let index = 0; index < pendingDirectories.length; index += 1) {
625
- const directory = pendingDirectories[index];
626
- let entries;
627
-
628
- try {
629
- entries = fs.readdirSync(directory, { withFileTypes: true });
630
- } catch {
631
- continue;
632
- }
633
-
634
- const childDirectories = entries
635
- .filter((entry) => entry.isDirectory())
636
- .sort(({ name: left }, { name: right }) =>
637
- left === right ? 0 : left < right ? -1 : 1,
638
- )
639
- .map((entry) => resolve(directory, entry.name))
640
- .filter((childDirectory) => isWithinRoot(absoluteRoot, childDirectory));
641
-
642
- groupRoots.push(...childDirectories);
643
- pendingDirectories.push(...childDirectories);
644
- }
645
-
646
- componentGroupRootsCache.set(absoluteRoot, groupRoots);
647
- return groupRoots;
648
- };
649
-
650
- /**
651
- * Resolve a component reference through recursively grouped directories.
652
- *
653
- * Project-scoped component IDs can use the component name (`project:button`)
654
- * even when projects organize components under grouping paths such as
655
- * `atoms/text`.
656
- *
657
- * @param {string} templatePath - Component-relative template reference.
658
- * @param {string} componentRoot - Absolute component root path.
659
- * @returns {string|null} Existing template path when found.
660
- */
661
- const resolveGroupedComponentTemplate = (templatePath, componentRoot) =>
662
- findExistingComponentTemplateFile(
663
- componentGroupRoots(componentRoot).flatMap((groupRoot) =>
664
- buildTemplateFileCandidates(groupRoot, templatePath),
665
- ),
666
- componentRoot,
667
- ) || null;
668
-
669
- /**
670
- * Resolve shorthand component references against the components namespace.
671
- *
672
- * @param {string} templatePath - Template reference from Twig source.
673
- * @param {string} componentRoot - Absolute component root path.
674
- * @returns {string|null} Existing template path when found.
675
- */
676
- const resolveComponentShorthandReference = (templatePath, componentRoot) => {
677
- if (!componentRoot || templatePath.startsWith('.')) return null;
678
-
679
- const shorthandPath =
680
- templatePath.startsWith('@') && !templatePath.includes('/')
681
- ? templatePath.slice(1)
682
- : templatePath;
683
- const directComponentPath = findExistingComponentTemplateFile(
684
- buildTemplateFileCandidates(componentRoot, shorthandPath),
685
- componentRoot,
686
- );
687
- if (directComponentPath) {
688
- return directComponentPath;
689
- }
690
-
691
- const genericNamespace = templatePath.match(/^@?[^/:]+[:/](.+)$/);
692
- if (!genericNamespace) {
693
- return null;
694
- }
695
-
696
- const genericComponentPath = genericNamespace[1];
697
-
698
- return (
699
- findExistingComponentTemplateFile(
700
- buildTemplateFileCandidates(componentRoot, genericComponentPath),
701
- componentRoot,
702
- ) || resolveGroupedComponentTemplate(genericComponentPath, componentRoot)
703
- );
704
- };
705
-
706
480
  /**
707
481
  * Build a stable key segment for include resolution cache entries.
708
482
  *
@@ -734,27 +508,19 @@ const resolveTwigTemplateWithoutCache = (templatePath, fromDir, options) => {
734
508
  options.namespaces,
735
509
  );
736
510
  if (namespaced) {
737
- const namespacedTemplate =
738
- namespaced.namespace === 'components'
739
- ? findExistingComponentTemplateFile(
740
- buildTemplateFileCandidates(namespaced.root, namespaced.path),
741
- namespaced.root,
742
- )
743
- : findExistingTemplateFile(
744
- buildTemplateFileCandidates(namespaced.root, namespaced.path),
745
- );
746
- if (namespacedTemplate) {
747
- return namespacedTemplate;
748
- }
749
-
750
511
  if (namespaced.namespace === 'components') {
751
- return resolveGroupedComponentTemplate(
752
- namespaced.path,
753
- options.namespaces?.components,
512
+ return resolveComponentReference(
513
+ templatePath,
514
+ options.namespaces,
515
+ componentGroupRootsCache,
754
516
  );
755
517
  }
756
518
 
757
- return null;
519
+ return (
520
+ findExistingTemplateFile(
521
+ buildTemplateFileCandidates(namespaced.root, namespaced.path),
522
+ ) || null
523
+ );
758
524
  }
759
525
 
760
526
  const relativeTemplate = findExistingTemplateFile([
@@ -764,9 +530,10 @@ const resolveTwigTemplateWithoutCache = (templatePath, fromDir, options) => {
764
530
 
765
531
  return (
766
532
  relativeTemplate ||
767
- resolveComponentShorthandReference(
533
+ resolveComponentReference(
768
534
  templatePath,
769
- options.namespaces?.components,
535
+ options.namespaces,
536
+ componentGroupRootsCache,
770
537
  )
771
538
  );
772
539
  };
@@ -996,7 +763,7 @@ export function makeTwigPluginOptions(env) {
996
763
  namespaces: makeTwigNamespaces(env),
997
764
  functions: getTwigFunctionMap(),
998
765
  registerDrupalTwigFilters: shouldRegisterDrupalTwigFilters(env),
999
- // Twig updates are handled by emulsifyTwigModulePlugin.handleHotUpdate.
766
+ // Twig updates are handled by emulsifyTwigModulePlugin.hotUpdate.
1000
767
  // Vituum's full reload would defeat HMR by reloading the whole iframe on
1001
768
  // every Twig save before module graph invalidation can update the story.
1002
769
  reload: () => false,
@@ -1403,7 +1170,7 @@ export function emulsifyTwigModulePlugin(options) {
1403
1170
  };
1404
1171
  }
1405
1172
  },
1406
- handleHotUpdate({ file, server }) {
1173
+ hotUpdate({ type, file, modules: changedModules = [] }) {
1407
1174
  const filePath = resolve(file);
1408
1175
  const componentRoot = options.namespaces?.components
1409
1176
  ? resolve(options.namespaces.components)
@@ -1442,7 +1209,11 @@ export function emulsifyTwigModulePlugin(options) {
1442
1209
  !!projectRoot &&
1443
1210
  isWithinRoot(resolve(projectRoot), filePath) &&
1444
1211
  fileExists !== knownFile;
1445
- const structuralChange = componentDirectoryChanged || projectPathChanged;
1212
+ const structuralChange =
1213
+ type === 'create' ||
1214
+ type === 'delete' ||
1215
+ componentDirectoryChanged ||
1216
+ projectPathChanged;
1446
1217
 
1447
1218
  if (file.endsWith('.twig')) {
1448
1219
  compileCache.delete(filePath);
@@ -1459,7 +1230,7 @@ export function emulsifyTwigModulePlugin(options) {
1459
1230
  * directory invalidated. New or deleted files can change previous
1460
1231
  * resolution misses, so those events clear the full resolution cache.
1461
1232
  */
1462
- if (fileExists && knownFile) {
1233
+ if (type === 'update' && fileExists && knownFile) {
1463
1234
  invalidateKnownResolutionCacheEntries(filePath);
1464
1235
  } else {
1465
1236
  resolutionCache.clear();
@@ -1475,6 +1246,9 @@ export function emulsifyTwigModulePlugin(options) {
1475
1246
  }
1476
1247
 
1477
1248
  if (structuralChange) {
1249
+ // Every environment receives the event, including after shared known-
1250
+ // file state was cleared. Namespace roots can also be outside a project.
1251
+ resolutionCache.clear();
1478
1252
  compileCache.clear();
1479
1253
  }
1480
1254
 
@@ -1489,12 +1263,15 @@ export function emulsifyTwigModulePlugin(options) {
1489
1263
  return undefined;
1490
1264
  }
1491
1265
 
1492
- const moduleGraph = server?.moduleGraph;
1266
+ const moduleGraph = this.environment?.moduleGraph;
1493
1267
  if (!moduleGraph?.getModulesByFile) {
1494
1268
  return undefined;
1495
1269
  }
1496
1270
 
1497
- const modules = new Set(moduleGraph.getModulesByFile(filePath) || []);
1271
+ const modules = new Set([
1272
+ ...changedModules,
1273
+ ...(moduleGraph.getModulesByFile(filePath) || []),
1274
+ ]);
1498
1275
  const dependencyModule = moduleGraph.getModuleById?.(dependencyModuleId);
1499
1276
  if (dependencyModule) {
1500
1277
  moduleGraph.invalidateModule?.(dependencyModule);
@@ -8,6 +8,11 @@
8
8
  import { readFileSync, readdirSync } from 'fs';
9
9
  import { relative, resolve } from 'path';
10
10
  import { fileURLToPath } from 'url';
11
+ import {
12
+ resolveAssetRoots,
13
+ toAbsoluteAssetRoot,
14
+ } from '../../utils/asset-roots.js';
15
+ import { isStorybookOutput } from '../assets/storybook-output.js';
11
16
  import { safeExists } from '../../utils/fs-safe.js';
12
17
  import { toPosixPath } from '../../utils/paths.js';
13
18
  import { unique } from '../../../../src/extensions/shared/lists.js';
@@ -27,43 +32,15 @@ const ASSET_SOURCE_RUNTIME_URL = new URL(
27
32
  const ASSET_SOURCE_RUNTIME_PATH = fileURLToPath(ASSET_SOURCE_RUNTIME_URL);
28
33
  const GENERATED_ASSET_ALIASES = new Set(['icons.svg']);
29
34
  const GENERATED_ASSET_ROOTS = ['/dist/assets'];
35
+ // Keys stay Vite root-relative because the runtime looks asset sources up by
36
+ // root-relative key. The values are the public URLs Storybook fetches, and they
37
+ // stay relative to the preview document so a static build works at a domain
38
+ // root and under any deployment subpath.
30
39
  const PUBLIC_ASSET_ROOTS = new Map([
31
- ['/assets', '/assets'],
32
- ['/dist/assets', '/assets'],
40
+ ['/assets', './assets'],
41
+ ['/dist/assets', './assets'],
33
42
  ]);
34
43
 
35
- /**
36
- * Resolve a configured asset root to an absolute filesystem path.
37
- *
38
- * @param {string} projectDir - Absolute project root.
39
- * @param {string} assetRoot - Absolute, project-relative, or Vite root-relative asset root.
40
- * @returns {string} Absolute filesystem path.
41
- */
42
- function toAbsoluteAssetRoot(projectDir, assetRoot) {
43
- const normalizedProjectDir = toPosixPath(projectDir || '').replace(
44
- /\/+$/,
45
- '',
46
- );
47
- const normalizedRoot = toPosixPath(assetRoot || '').replace(/\/+$/, '');
48
-
49
- if (!normalizedRoot) return '';
50
- if (
51
- normalizedProjectDir &&
52
- (normalizedRoot === normalizedProjectDir ||
53
- normalizedRoot.startsWith(`${normalizedProjectDir}/`))
54
- ) {
55
- return normalizedRoot;
56
- }
57
- if (normalizedRoot.startsWith('/') && normalizedProjectDir) {
58
- if (safeExists(normalizedRoot)) {
59
- return normalizedRoot;
60
- }
61
- return `${normalizedProjectDir}${normalizedRoot}`;
62
- }
63
-
64
- return toPosixPath(resolve(projectDir || '.', normalizedRoot));
65
- }
66
-
67
44
  /**
68
45
  * Resolve existing project asset roots for Storybook source() text imports.
69
46
  *
@@ -71,17 +48,8 @@ function toAbsoluteAssetRoot(projectDir, assetRoot) {
71
48
  * @returns {string[]} Existing Vite root-relative asset root paths.
72
49
  */
73
50
  export function assetSourceRoots(env) {
74
- const configuredRoots =
75
- Array.isArray(env?.projectStructure?.assetRoots) &&
76
- env.projectStructure.assetRoots.length
77
- ? env.projectStructure.assetRoots
78
- : [];
79
- const fallbackRoots = ['/assets', '/src/assets'];
80
-
81
51
  return unique(
82
- [...configuredRoots, ...fallbackRoots]
83
- .map((root) => toAbsoluteAssetRoot(env?.projectDir, root))
84
- .filter((root) => root && safeExists(root))
52
+ resolveAssetRoots(env)
85
53
  .map((root) => toRootRelativePath(env?.projectDir, root))
86
54
  .filter(Boolean),
87
55
  );
@@ -116,20 +84,33 @@ function generatedAssetRootPrefixes() {
116
84
  /**
117
85
  * Build Vite glob patterns from text asset roots.
118
86
  *
87
+ * Project roots stay recursive. Generated roots contain build output, so only
88
+ * the aliases Twig can resolve from those roots belong in the source map.
89
+ *
119
90
  * @param {{ projectDir?: string, projectStructure?: { assetRoots?: string[] } }} env - Emulsify environment.
120
91
  * @returns {string[]} Root-relative text asset glob patterns.
121
92
  */
122
93
  export function assetSourceGlobPatterns(env) {
123
94
  const extensions = Array.from(INLINE_ASSET_EXTS).join(',');
124
-
125
- return [...assetSourceRoots(env), ...generatedAssetSourceRoots(env)].map(
95
+ const projectPatterns = assetSourceRoots(env).map(
126
96
  (root) => `${root === '/' ? '' : root}/**/*.{${extensions}}`,
127
97
  );
98
+ const generatedPatterns = generatedAssetSourceRoots(env).flatMap((root) =>
99
+ Array.from(
100
+ GENERATED_ASSET_ALIASES,
101
+ (alias) => `${root === '/' ? '' : root}/${alias}`,
102
+ ),
103
+ );
104
+
105
+ return unique([...projectPatterns, ...generatedPatterns]);
128
106
  }
129
107
 
130
108
  /**
131
109
  * Return a public URL base for asset roots served by Storybook staticDirs.
132
110
  *
111
+ * The base is relative to Storybook's preview document rather than the domain
112
+ * root, so generated fetch URLs resolve under any deployment subpath.
113
+ *
133
114
  * @param {string} root - Vite root-relative asset source root.
134
115
  * @returns {string} Public URL base, or an empty string for non-public roots.
135
116
  */
@@ -226,9 +207,13 @@ export function publicAssetSourceEntries(env) {
226
207
  * Generate the virtual module source for lazy text asset maps.
227
208
  *
228
209
  * @param {{ projectDir?: string, projectStructure?: { assetRoots?: string[] } }} env - Emulsify environment.
210
+ * @param {{ inlineTextAssets?: boolean }} [options={}] - Generation options.
229
211
  * @returns {string} JavaScript module source.
230
212
  */
231
- export function generateVirtualTwigAssetSourcesModule(env) {
213
+ export function generateVirtualTwigAssetSourcesModule(
214
+ env,
215
+ { inlineTextAssets = true } = {},
216
+ ) {
232
217
  const rootPrefixes = assetSourceRoots(env).map((root) =>
233
218
  `${root === '/' ? '' : root}/`.replace(/\/{2,}/g, '/'),
234
219
  );
@@ -239,7 +224,12 @@ export function generateVirtualTwigAssetSourcesModule(env) {
239
224
  ...generatedRootPrefixes,
240
225
  ...generatedAssetRootPrefixes(),
241
226
  ]);
242
- const patterns = assetSourceGlobPatterns(env);
227
+ // Each glob entry becomes a lazy chunk carrying one asset's bytes as a
228
+ // JavaScript string. Storybook needs them so source('@assets/...') can inline
229
+ // an SVG synchronously; a theme build does not, and emitting them there just
230
+ // copies the asset tree into dist/ in another form. The fetch entries below
231
+ // keep source() working at runtime without bundling anything.
232
+ const patterns = inlineTextAssets ? assetSourceGlobPatterns(env) : [];
243
233
  const globEntries = patterns.length
244
234
  ? patterns
245
235
  .map(
@@ -304,8 +294,15 @@ export const getAssetText = assetSourceRuntime.getAssetText;
304
294
  * @returns {import('vite').PluginOption} Virtual module plugin.
305
295
  */
306
296
  export function virtualTwigAssetSourcesPlugin(env) {
297
+ let inlineTextAssets = true;
298
+
307
299
  return {
308
300
  name: 'emulsify-virtual-twig-asset-sources',
301
+
302
+ configResolved(config) {
303
+ inlineTextAssets = isStorybookOutput(config);
304
+ },
305
+
309
306
  resolveId(id) {
310
307
  if (id === VIRTUAL_TWIG_ASSET_SOURCES_ID) {
311
308
  return RESOLVED_VIRTUAL_TWIG_ASSET_SOURCES_ID;
@@ -318,7 +315,9 @@ export function virtualTwigAssetSourcesPlugin(env) {
318
315
  },
319
316
  load(id) {
320
317
  if (id === RESOLVED_VIRTUAL_TWIG_ASSET_SOURCES_ID) {
321
- return generateVirtualTwigAssetSourcesModule(env);
318
+ return generateVirtualTwigAssetSourcesModule(env, {
319
+ inlineTextAssets,
320
+ });
322
321
  }
323
322
  if (id === RESOLVED_VIRTUAL_TWIG_ASSET_SOURCE_RUNTIME_ID) {
324
323
  this.addWatchFile(ASSET_SOURCE_RUNTIME_PATH);