@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.
- package/.storybook/main-static-assets.js +5 -8
- package/.storybook/main-vite.js +11 -3
- package/README.md +14 -6
- package/config/a11y-wcag22.js +11 -0
- package/config/vite/entries.js +7 -2
- package/config/vite/environment.js +4 -0
- package/config/vite/plugins/assets/asset-url-rebase.js +241 -0
- package/config/vite/plugins/assets/copy-src-assets.js +82 -12
- package/config/vite/plugins/assets/copy-twig-files.js +85 -16
- package/config/vite/plugins/assets/css-asset-rebase.js +306 -0
- package/config/vite/plugins/assets/css-asset-relativizer.js +301 -21
- package/config/vite/plugins/assets/development-source-maps.js +273 -0
- package/config/vite/plugins/assets/mirror-components.js +98 -82
- package/config/vite/plugins/assets/output-freshness.js +235 -0
- package/config/vite/plugins/assets/source-file-index.js +13 -13
- package/config/vite/plugins/assets/stable-watch-output.js +165 -0
- package/config/vite/plugins/assets/storybook-output.js +27 -0
- package/config/vite/plugins/index.js +95 -9
- package/config/vite/plugins/reporter/asset-resolver.js +34 -6
- package/config/vite/plugins/reporter/build-errors.js +7 -3
- package/config/vite/plugins/reporter/diagnostics.js +140 -10
- package/config/vite/plugins/reporter/index.js +380 -75
- package/config/vite/plugins/reporter/render.js +297 -44
- package/config/vite/plugins/reporter/sass-logger.js +30 -0
- package/config/vite/plugins/reporter/source-roots.js +101 -21
- package/config/vite/plugins/reporter/strict-mode.js +99 -0
- package/config/vite/plugins/reporter/vite-logger.js +220 -8
- package/config/vite/plugins/reporter/watch-mode.js +6 -2
- package/config/vite/plugins/twig/twig-module.js +35 -258
- package/config/vite/plugins/twig/virtual-twig-asset-sources.js +48 -49
- package/config/vite/project-config.js +121 -21
- package/config/vite/project-structure.js +6 -0
- package/config/vite/utils/asset-roots.js +205 -0
- package/config/vite/utils/css-urls.js +350 -0
- package/config/vite/utils/fs-safe.js +38 -1
- package/config/vite/utils/source-directory-skips.js +13 -0
- package/config/vite/utils/source-maps.js +88 -0
- package/config/vite/utils/twig-component-resolver.js +316 -0
- package/config/vite/vite.config.js +106 -42
- package/package.json +54 -40
- package/scripts/a11y.js +88 -9
- package/scripts/audit/checks/css-asset-references.js +256 -24
- package/scripts/audit/checks/twig-references.js +16 -5
- package/scripts/audit/fix.js +836 -0
- package/scripts/audit/index.js +10 -2
- package/scripts/audit/lib/css.js +41 -35
- package/scripts/audit/lib/story-ast.js +392 -0
- package/scripts/audit/lib/story-render-paths.js +600 -0
- package/scripts/audit/lib/story-selection.js +190 -0
- package/scripts/audit/lib/twig.js +372 -80
- package/scripts/audit/report.js +83 -5
- package/scripts/audit-twig-stories.js +73 -3
- package/scripts/audit.js +87 -2
- 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,
|
|
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
|
|
752
|
-
|
|
753
|
-
options.namespaces
|
|
512
|
+
return resolveComponentReference(
|
|
513
|
+
templatePath,
|
|
514
|
+
options.namespaces,
|
|
515
|
+
componentGroupRootsCache,
|
|
754
516
|
);
|
|
755
517
|
}
|
|
756
518
|
|
|
757
|
-
return
|
|
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
|
-
|
|
533
|
+
resolveComponentReference(
|
|
768
534
|
templatePath,
|
|
769
|
-
options.namespaces
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
1266
|
+
const moduleGraph = this.environment?.moduleGraph;
|
|
1493
1267
|
if (!moduleGraph?.getModulesByFile) {
|
|
1494
1268
|
return undefined;
|
|
1495
1269
|
}
|
|
1496
1270
|
|
|
1497
|
-
const modules = new Set(
|
|
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', '
|
|
32
|
-
['/dist/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
|
-
|
|
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(
|
|
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
|
-
|
|
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);
|