@emulsify/core 4.2.0 → 4.3.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-vite.js +91 -36
- package/README.md +185 -56
- package/config/a11y.config.js +2 -1
- package/config/vite/entries.js +4 -4
- package/config/vite/plugins/{copy-src-assets.js → assets/copy-src-assets.js} +1 -1
- package/config/vite/plugins/{copy-twig-files.js → assets/copy-twig-files.js} +1 -1
- package/config/vite/plugins/{css-asset-relativizer.js → assets/css-asset-relativizer.js} +9 -0
- package/config/vite/plugins/{mirror-components.js → assets/mirror-components.js} +2 -28
- package/config/vite/plugins/{source-file-index.js → assets/source-file-index.js} +1 -1
- package/config/vite/plugins/{svg-sprite.js → assets/svg-sprite.js} +2 -2
- package/config/vite/plugins/index.js +23 -14
- package/config/vite/plugins/reporter/asset-resolver.js +541 -0
- package/config/vite/plugins/reporter/build-errors.js +284 -0
- package/config/vite/plugins/reporter/diagnostics.js +366 -0
- package/config/vite/plugins/reporter/format.js +199 -0
- package/config/vite/plugins/reporter/index.js +308 -0
- package/config/vite/plugins/reporter/render.js +758 -0
- package/config/vite/plugins/reporter/sass-logger.js +237 -0
- package/config/vite/plugins/reporter/vite-logger.js +188 -0
- package/config/vite/plugins/reporter/watch-mode.js +40 -0
- package/config/vite/plugins/{twig-extension-installers.js → twig/extension-installers.js} +1 -1
- package/config/vite/{twig-extensions.js → plugins/twig/extensions.js} +2 -2
- package/config/vite/plugins/{twig-module.js → twig/twig-module.js} +387 -115
- package/config/vite/plugins/{virtual-twig-asset-sources.js → twig/virtual-twig-asset-sources.js} +37 -136
- package/config/vite/plugins/{virtual-twig-globs.js → twig/virtual-twig-globs.js} +3 -32
- package/config/vite/plugins/{vituum-patch.js → twig/vituum-patch.js} +3 -3
- package/config/vite/plugins.js +1 -1
- package/config/vite/project-config.js +1 -1
- package/config/vite/project-structure.js +1 -1
- package/config/vite/utils/lru.js +77 -0
- package/config/vite/utils/package-version.js +42 -0
- package/config/vite/utils/paths.js +1 -9
- package/config/vite/utils/react-singleton.js +1 -1
- package/config/vite/vite.config.js +51 -5
- package/package.json +93 -58
- package/scripts/a11y.js +115 -23
- package/scripts/audit/checks/core-imports.js +78 -0
- package/scripts/audit/checks/css-asset-references.js +99 -0
- package/scripts/audit/checks/drupal-assumptions.js +48 -0
- package/scripts/audit/checks/files-outside-roots.js +53 -0
- package/scripts/audit/checks/generated-package-scripts.js +113 -0
- package/scripts/audit/checks/legacy-twig-stories.js +33 -0
- package/scripts/audit/checks/package-overrides.js +91 -0
- package/scripts/audit/checks/project-config.js +71 -0
- package/scripts/audit/checks/story-discovery.js +35 -0
- package/scripts/audit/checks/twig-references.js +69 -0
- package/scripts/audit/checks/twig-volume.js +54 -0
- package/scripts/audit/checks/webpack-patterns.js +86 -0
- package/scripts/audit/index.js +177 -0
- package/scripts/audit/lib/css.js +165 -0
- package/scripts/audit/lib/files.js +168 -0
- package/scripts/audit/lib/findings.js +31 -0
- package/scripts/audit/lib/package-json.js +65 -0
- package/scripts/audit/lib/twig.js +227 -0
- package/scripts/audit/report.js +273 -0
- package/scripts/audit-twig-stories.js +115 -78
- package/scripts/audit.js +150 -1632
- package/scripts/check-node-version.js +136 -10
- package/scripts/inspect-components.js +456 -0
- package/scripts/lib/cli.js +179 -0
- package/scripts/lib/fs.js +31 -0
- package/scripts/lib/proc.js +78 -0
- package/scripts/lib/text.js +14 -0
- package/scripts/loadYaml.js +2 -2
- package/src/extensions/shared/attributes.js +3 -3
- package/src/extensions/shared/lists.js +2 -6
- package/src/extensions/shared/root-relative.js +38 -0
- package/src/storybook/index.js +4 -0
- package/src/storybook/render-twig.js +1 -1
- package/src/storybook/render-web-component.js +459 -0
- package/src/storybook/twig/asset-source-runtime.js +193 -0
- package/src/storybook/twig/{source-extensions.js → constants.js} +3 -1
- package/src/storybook/twig/reference-paths.js +2 -13
- package/src/storybook/twig/resolver.js +20 -3
- package/src/storybook/twig/setup.js +12 -2
- package/src/storybook/twig/source-function.js +5 -2
- package/config/vite/utils/unique.js +0 -36
- package/src/storybook/twig/include.js +0 -28
- package/src/storybook/twig/source-events.js +0 -5
|
@@ -10,22 +10,24 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import fs from 'fs';
|
|
13
|
+
import { readFile, stat } from 'fs/promises';
|
|
13
14
|
import { basename, dirname, isAbsolute, relative, resolve } from 'path';
|
|
14
15
|
import Twig from 'twig';
|
|
15
16
|
|
|
16
17
|
import {
|
|
17
18
|
getTwigFunctionMap,
|
|
18
19
|
registerTwigExtensions,
|
|
19
|
-
} from '
|
|
20
|
-
import { toRootRelativePath } from '
|
|
21
|
-
import { resolveProjectStructure } from '
|
|
20
|
+
} from '../../../../src/extensions/twig/index.js';
|
|
21
|
+
import { toRootRelativePath } from '../../../../src/storybook/twig/reference-paths.js';
|
|
22
|
+
import { resolveProjectStructure } from '../../project-structure.js';
|
|
22
23
|
import {
|
|
23
|
-
registerConfiguredTwigExtensions,
|
|
24
24
|
shouldRegisterDrupalTwigFilters,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
25
|
+
installProjectTwigExtensions,
|
|
26
|
+
} from './extensions.js';
|
|
27
|
+
import { firstExistingPath, safeExists } from '../../utils/fs-safe.js';
|
|
28
|
+
import { createLruCache } from '../../utils/lru.js';
|
|
29
|
+
import { toPosixPath } from '../../utils/paths.js';
|
|
30
|
+
import { unique } from '../../../../src/extensions/shared/lists.js';
|
|
29
31
|
|
|
30
32
|
/** Twig token types that can reference another template file. */
|
|
31
33
|
const templateReferenceTokenTypes = [
|
|
@@ -38,6 +40,10 @@ const templateReferenceTokenTypes = [
|
|
|
38
40
|
|
|
39
41
|
const includeFunctionName = 'include';
|
|
40
42
|
const sourceFunctionName = 'source';
|
|
43
|
+
const VIRTUAL_TWIG_DEPENDENCY_PREFIX = 'virtual:emulsify-twig-dep:';
|
|
44
|
+
const RESOLVED_VIRTUAL_TWIG_DEPENDENCY_PREFIX = `\0${VIRTUAL_TWIG_DEPENDENCY_PREFIX}`;
|
|
45
|
+
const maxCompiledTemplateCacheEntries = 2000;
|
|
46
|
+
const maxResolutionCacheEntries = 5000;
|
|
41
47
|
const expressionTokenTypes = {
|
|
42
48
|
arrayEnd: 'Twig.expression.type.array.end',
|
|
43
49
|
arrayStart: 'Twig.expression.type.array.start',
|
|
@@ -63,16 +69,23 @@ const expressionClosingTokenTypes = new Set([
|
|
|
63
69
|
/**
|
|
64
70
|
* Cache compiled Twig templates by absolute path for the life of one build.
|
|
65
71
|
*
|
|
66
|
-
* @type {
|
|
72
|
+
* @type {ReturnType<typeof createLruCache>}
|
|
67
73
|
*/
|
|
68
|
-
const compileCache =
|
|
74
|
+
const compileCache = createLruCache(maxCompiledTemplateCacheEntries);
|
|
69
75
|
|
|
70
76
|
/**
|
|
71
77
|
* Cache resolved Twig include paths by source directory, reference, and roots.
|
|
72
78
|
*
|
|
73
|
-
* @type {
|
|
79
|
+
* @type {ReturnType<typeof createLruCache>}
|
|
74
80
|
*/
|
|
75
|
-
const resolutionCache =
|
|
81
|
+
const resolutionCache = createLruCache(maxResolutionCacheEntries);
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Cache recursively discovered component grouping directories by root.
|
|
85
|
+
*
|
|
86
|
+
* @type {Map<string, string[]>}
|
|
87
|
+
*/
|
|
88
|
+
const componentGroupRootsCache = new Map();
|
|
76
89
|
|
|
77
90
|
/**
|
|
78
91
|
* Track Twig files that have been seen during this build/session.
|
|
@@ -114,6 +127,13 @@ const canMemoizeByEnv = (env) => env && typeof env === 'object';
|
|
|
114
127
|
* @returns {boolean} TRUE when the request is a renderable Twig module.
|
|
115
128
|
*/
|
|
116
129
|
const isTwigModuleRequest = (id) => {
|
|
130
|
+
if (
|
|
131
|
+
id.startsWith(VIRTUAL_TWIG_DEPENDENCY_PREFIX) ||
|
|
132
|
+
id.startsWith(RESOLVED_VIRTUAL_TWIG_DEPENDENCY_PREFIX)
|
|
133
|
+
) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
const [filePath, query = ''] = id.split('?');
|
|
118
138
|
if (!filePath.endsWith('.twig')) return false;
|
|
119
139
|
return !query || query === 'twig' || !/(^|&)(raw|url)\b/.test(query);
|
|
@@ -127,6 +147,24 @@ const isTwigModuleRequest = (id) => {
|
|
|
127
147
|
*/
|
|
128
148
|
const stripRequestQuery = (id) => id.split('?')[0];
|
|
129
149
|
|
|
150
|
+
const encodeVirtualTwigDependencyId = (filePath) =>
|
|
151
|
+
`${VIRTUAL_TWIG_DEPENDENCY_PREFIX}${encodeURIComponent(resolve(filePath))}`;
|
|
152
|
+
|
|
153
|
+
const resolvedVirtualTwigDependencyId = (filePath) =>
|
|
154
|
+
`${RESOLVED_VIRTUAL_TWIG_DEPENDENCY_PREFIX}${encodeURIComponent(
|
|
155
|
+
resolve(filePath),
|
|
156
|
+
)}`;
|
|
157
|
+
|
|
158
|
+
const decodeVirtualTwigDependencyId = (id) => {
|
|
159
|
+
const normalizedId = id.startsWith('\0') ? id.slice(1) : id;
|
|
160
|
+
|
|
161
|
+
return normalizedId.startsWith(VIRTUAL_TWIG_DEPENDENCY_PREFIX)
|
|
162
|
+
? decodeURIComponent(
|
|
163
|
+
normalizedId.slice(VIRTUAL_TWIG_DEPENDENCY_PREFIX.length),
|
|
164
|
+
)
|
|
165
|
+
: '';
|
|
166
|
+
};
|
|
167
|
+
|
|
130
168
|
/**
|
|
131
169
|
* Extract the first argument from a Twig function token parameter list.
|
|
132
170
|
*
|
|
@@ -355,6 +393,43 @@ const isWithinRoot = (root, filePath) => {
|
|
|
355
393
|
);
|
|
356
394
|
};
|
|
357
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
|
+
|
|
358
433
|
/**
|
|
359
434
|
* Find the most specific configured Twig root for a template file.
|
|
360
435
|
*
|
|
@@ -401,6 +476,21 @@ const templateIdForPath = (filePath, options) => {
|
|
|
401
476
|
const makeTemplateInstantiationCode = (templateId, params) =>
|
|
402
477
|
`Twig.twig(${JSON.stringify({ ...params, id: templateId })})`;
|
|
403
478
|
|
|
479
|
+
const makeTemplateInstantiationFromParamsCode = (
|
|
480
|
+
templateIdExpression,
|
|
481
|
+
paramsExpression,
|
|
482
|
+
) => `Twig.twig({ ...${paramsExpression}, id: ${templateIdExpression} })`;
|
|
483
|
+
|
|
484
|
+
const generateTwigDependencyModule = async (filePath, options) => {
|
|
485
|
+
const compiled = await compileTwigTemplate(filePath, options, compileCache);
|
|
486
|
+
|
|
487
|
+
return [
|
|
488
|
+
`export const templateId = ${JSON.stringify(compiled.templateId)};`,
|
|
489
|
+
`export const templateParams = ${JSON.stringify(compiled.templateParams)};`,
|
|
490
|
+
'',
|
|
491
|
+
].join('\n');
|
|
492
|
+
};
|
|
493
|
+
|
|
404
494
|
/**
|
|
405
495
|
* Rewrite static include/import/embed references to module-local template IDs.
|
|
406
496
|
*
|
|
@@ -511,7 +601,11 @@ const parseTwigNamespaceReference = (templatePath, namespaces = {}) => {
|
|
|
511
601
|
};
|
|
512
602
|
|
|
513
603
|
/**
|
|
514
|
-
* Return
|
|
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.
|
|
515
609
|
*
|
|
516
610
|
* @param {string} componentRoot - Absolute component root path.
|
|
517
611
|
* @returns {string[]} Absolute grouping directory paths.
|
|
@@ -519,33 +613,57 @@ const parseTwigNamespaceReference = (templatePath, namespaces = {}) => {
|
|
|
519
613
|
const componentGroupRoots = (componentRoot) => {
|
|
520
614
|
if (!componentRoot) return [];
|
|
521
615
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
return
|
|
525
|
-
|
|
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
|
|
526
635
|
.filter((entry) => entry.isDirectory())
|
|
527
|
-
.
|
|
528
|
-
|
|
529
|
-
|
|
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);
|
|
530
644
|
}
|
|
645
|
+
|
|
646
|
+
componentGroupRootsCache.set(absoluteRoot, groupRoots);
|
|
647
|
+
return groupRoots;
|
|
531
648
|
};
|
|
532
649
|
|
|
533
650
|
/**
|
|
534
|
-
* Resolve a component reference through
|
|
651
|
+
* Resolve a component reference through recursively grouped directories.
|
|
535
652
|
*
|
|
536
653
|
* Project-scoped component IDs can use the component name (`project:button`)
|
|
537
|
-
* even when projects organize components under grouping
|
|
538
|
-
* `
|
|
654
|
+
* even when projects organize components under grouping paths such as
|
|
655
|
+
* `atoms/text`.
|
|
539
656
|
*
|
|
540
657
|
* @param {string} templatePath - Component-relative template reference.
|
|
541
658
|
* @param {string} componentRoot - Absolute component root path.
|
|
542
659
|
* @returns {string|null} Existing template path when found.
|
|
543
660
|
*/
|
|
544
661
|
const resolveGroupedComponentTemplate = (templatePath, componentRoot) =>
|
|
545
|
-
|
|
662
|
+
findExistingComponentTemplateFile(
|
|
546
663
|
componentGroupRoots(componentRoot).flatMap((groupRoot) =>
|
|
547
664
|
buildTemplateFileCandidates(groupRoot, templatePath),
|
|
548
665
|
),
|
|
666
|
+
componentRoot,
|
|
549
667
|
) || null;
|
|
550
668
|
|
|
551
669
|
/**
|
|
@@ -562,8 +680,9 @@ const resolveComponentShorthandReference = (templatePath, componentRoot) => {
|
|
|
562
680
|
templatePath.startsWith('@') && !templatePath.includes('/')
|
|
563
681
|
? templatePath.slice(1)
|
|
564
682
|
: templatePath;
|
|
565
|
-
const directComponentPath =
|
|
683
|
+
const directComponentPath = findExistingComponentTemplateFile(
|
|
566
684
|
buildTemplateFileCandidates(componentRoot, shorthandPath),
|
|
685
|
+
componentRoot,
|
|
567
686
|
);
|
|
568
687
|
if (directComponentPath) {
|
|
569
688
|
return directComponentPath;
|
|
@@ -577,8 +696,9 @@ const resolveComponentShorthandReference = (templatePath, componentRoot) => {
|
|
|
577
696
|
const genericComponentPath = genericNamespace[1];
|
|
578
697
|
|
|
579
698
|
return (
|
|
580
|
-
|
|
699
|
+
findExistingComponentTemplateFile(
|
|
581
700
|
buildTemplateFileCandidates(componentRoot, genericComponentPath),
|
|
701
|
+
componentRoot,
|
|
582
702
|
) || resolveGroupedComponentTemplate(genericComponentPath, componentRoot)
|
|
583
703
|
);
|
|
584
704
|
};
|
|
@@ -614,9 +734,15 @@ const resolveTwigTemplateWithoutCache = (templatePath, fromDir, options) => {
|
|
|
614
734
|
options.namespaces,
|
|
615
735
|
);
|
|
616
736
|
if (namespaced) {
|
|
617
|
-
const namespacedTemplate =
|
|
618
|
-
|
|
619
|
-
|
|
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
|
+
);
|
|
620
746
|
if (namespacedTemplate) {
|
|
621
747
|
return namespacedTemplate;
|
|
622
748
|
}
|
|
@@ -699,12 +825,12 @@ const invalidateKnownResolutionCacheEntries = (filePath) => {
|
|
|
699
825
|
* @param {string} filePath - Absolute template file path.
|
|
700
826
|
* @param {ReturnType<typeof makeTwigPluginOptions>} options - Twig plugin options.
|
|
701
827
|
* @param {typeof compileCache} [cache=compileCache] - Shared compile cache.
|
|
702
|
-
* @returns {{ code: string, includes: string[], sourceReferences: string[] }} Compiled template code and references.
|
|
828
|
+
* @returns {Promise<{ code: string, includes: string[], sourceReferences: string[] }>} Compiled template code and references.
|
|
703
829
|
*/
|
|
704
|
-
const compileTwigTemplate = (filePath, options, cache = compileCache) => {
|
|
830
|
+
const compileTwigTemplate = async (filePath, options, cache = compileCache) => {
|
|
705
831
|
const absoluteFilePath = resolve(filePath);
|
|
706
832
|
knownTwigFiles.add(absoluteFilePath);
|
|
707
|
-
const { mtimeMs } =
|
|
833
|
+
const { mtimeMs } = await stat(absoluteFilePath);
|
|
708
834
|
const cached = cache.get(absoluteFilePath);
|
|
709
835
|
if (cached?.mtimeMs === mtimeMs) {
|
|
710
836
|
return cached.compiled;
|
|
@@ -712,9 +838,9 @@ const compileTwigTemplate = (filePath, options, cache = compileCache) => {
|
|
|
712
838
|
|
|
713
839
|
const compilerTwig = Twig.factory();
|
|
714
840
|
registerTwigExtensions(compilerTwig);
|
|
715
|
-
|
|
841
|
+
installProjectTwigExtensions(compilerTwig, options);
|
|
716
842
|
|
|
717
|
-
const source =
|
|
843
|
+
const source = await readFile(absoluteFilePath, 'utf8');
|
|
718
844
|
const compileOptions = {
|
|
719
845
|
allowInlineIncludes: true,
|
|
720
846
|
namespaces: options.namespaces,
|
|
@@ -914,6 +1040,16 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
914
1040
|
*/
|
|
915
1041
|
const dependencyImporters = new Map();
|
|
916
1042
|
|
|
1043
|
+
/**
|
|
1044
|
+
* Twig entry modules transformed by this plugin instance.
|
|
1045
|
+
*
|
|
1046
|
+
* Structural component changes invalidate these modules because a new or
|
|
1047
|
+
* removed directory can change the target of a shorthand reference.
|
|
1048
|
+
*
|
|
1049
|
+
* @type {Set<string>}
|
|
1050
|
+
*/
|
|
1051
|
+
const transformedTwigModules = new Set();
|
|
1052
|
+
|
|
917
1053
|
/**
|
|
918
1054
|
* Remember that one imported Twig module depends on another Twig file.
|
|
919
1055
|
*
|
|
@@ -948,9 +1084,31 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
948
1084
|
buildStart() {
|
|
949
1085
|
compileCache.clear();
|
|
950
1086
|
resolutionCache.clear();
|
|
1087
|
+
componentGroupRootsCache.clear();
|
|
951
1088
|
knownTwigFiles.clear();
|
|
1089
|
+
transformedTwigModules.clear();
|
|
1090
|
+
},
|
|
1091
|
+
resolveId(id) {
|
|
1092
|
+
if (id.startsWith(VIRTUAL_TWIG_DEPENDENCY_PREFIX)) {
|
|
1093
|
+
return `\0${id}`;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
return null;
|
|
952
1097
|
},
|
|
953
|
-
|
|
1098
|
+
async load(id) {
|
|
1099
|
+
if (!id.startsWith(RESOLVED_VIRTUAL_TWIG_DEPENDENCY_PREFIX)) {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const dependencyPath = decodeVirtualTwigDependencyId(id);
|
|
1104
|
+
if (!dependencyPath) {
|
|
1105
|
+
return null;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
this.addWatchFile(dependencyPath);
|
|
1109
|
+
return generateTwigDependencyModule(dependencyPath, options);
|
|
1110
|
+
},
|
|
1111
|
+
async transform(...args) {
|
|
954
1112
|
const [, id] = args;
|
|
955
1113
|
if (!isTwigModuleRequest(id)) {
|
|
956
1114
|
return null;
|
|
@@ -958,10 +1116,12 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
958
1116
|
|
|
959
1117
|
const filePath = stripRequestQuery(id);
|
|
960
1118
|
const sourceFilePath = resolve(filePath);
|
|
961
|
-
|
|
1119
|
+
transformedTwigModules.add(sourceFilePath);
|
|
1120
|
+
/** @type {Map<string, Awaited<ReturnType<typeof compileTwigTemplate>>>} */
|
|
962
1121
|
const compiledDependencyTemplates = new Map();
|
|
963
1122
|
/** @type {Map<string, Set<string>>} */
|
|
964
1123
|
const compiledDependencyReferences = new Map();
|
|
1124
|
+
const queuedDependencyTemplates = new Set();
|
|
965
1125
|
clearDependencyImporter(sourceFilePath);
|
|
966
1126
|
|
|
967
1127
|
/**
|
|
@@ -996,13 +1156,15 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
996
1156
|
* @param {string[]} templateReferences - Raw references found in tokens.
|
|
997
1157
|
* @param {string} fromFilePath - Absolute importer path.
|
|
998
1158
|
* @param {typeof compileCache} cache - Shared compile cache.
|
|
999
|
-
* @returns {void}
|
|
1159
|
+
* @returns {Promise<void>}
|
|
1000
1160
|
*/
|
|
1001
|
-
const compileDependencyTemplates = (
|
|
1161
|
+
const compileDependencyTemplates = async (
|
|
1002
1162
|
templateReferences,
|
|
1003
1163
|
fromFilePath,
|
|
1004
1164
|
cache,
|
|
1005
1165
|
) => {
|
|
1166
|
+
const dependencyRecords = [];
|
|
1167
|
+
|
|
1006
1168
|
for (const templatePath of templateReferences) {
|
|
1007
1169
|
const dependencyPath = resolveTwigTemplate(
|
|
1008
1170
|
templatePath,
|
|
@@ -1013,22 +1175,38 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
1013
1175
|
const absoluteDependencyPath = resolve(dependencyPath);
|
|
1014
1176
|
addCompiledDependencyReference(absoluteDependencyPath, templatePath);
|
|
1015
1177
|
if (absoluteDependencyPath === sourceFilePath) continue;
|
|
1178
|
+
if (
|
|
1179
|
+
compiledDependencyTemplates.has(absoluteDependencyPath) ||
|
|
1180
|
+
queuedDependencyTemplates.has(absoluteDependencyPath)
|
|
1181
|
+
) {
|
|
1182
|
+
continue;
|
|
1183
|
+
}
|
|
1016
1184
|
|
|
1017
|
-
|
|
1018
|
-
absoluteDependencyPath,
|
|
1019
|
-
);
|
|
1020
|
-
if (dependencyTemplate) continue;
|
|
1185
|
+
queuedDependencyTemplates.add(absoluteDependencyPath);
|
|
1021
1186
|
|
|
1022
1187
|
addDependencyImporter(absoluteDependencyPath, sourceFilePath);
|
|
1023
1188
|
this.addWatchFile(absoluteDependencyPath);
|
|
1024
1189
|
|
|
1025
|
-
|
|
1190
|
+
dependencyRecords.push({ absoluteDependencyPath });
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
const compiledDependencyRecords = await Promise.all(
|
|
1194
|
+
dependencyRecords.map(async ({ absoluteDependencyPath }) => ({
|
|
1026
1195
|
absoluteDependencyPath,
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1196
|
+
compiled: await compileTwigTemplate(
|
|
1197
|
+
absoluteDependencyPath,
|
|
1198
|
+
options,
|
|
1199
|
+
cache,
|
|
1200
|
+
),
|
|
1201
|
+
})),
|
|
1202
|
+
);
|
|
1203
|
+
|
|
1204
|
+
for (const {
|
|
1205
|
+
absoluteDependencyPath,
|
|
1206
|
+
compiled,
|
|
1207
|
+
} of compiledDependencyRecords) {
|
|
1030
1208
|
compiledDependencyTemplates.set(absoluteDependencyPath, compiled);
|
|
1031
|
-
compileDependencyTemplates(
|
|
1209
|
+
await compileDependencyTemplates(
|
|
1032
1210
|
compiled.includes,
|
|
1033
1211
|
absoluteDependencyPath,
|
|
1034
1212
|
cache,
|
|
@@ -1040,51 +1218,56 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
1040
1218
|
* Emit raw Twig source registrations for static source() references.
|
|
1041
1219
|
*
|
|
1042
1220
|
* @param {string} fromFilePath - Absolute file that contains the reference.
|
|
1043
|
-
* @param {ReturnType<typeof compileTwigTemplate
|
|
1044
|
-
* @returns {string[]} Generated source map registration statements.
|
|
1221
|
+
* @param {Awaited<ReturnType<typeof compileTwigTemplate>>} compiledTemplate - Compiled template metadata.
|
|
1222
|
+
* @returns {Promise<string[]>} Generated source map registration statements.
|
|
1045
1223
|
*/
|
|
1046
|
-
const makeSourceTemplateRegistrations = (
|
|
1224
|
+
const makeSourceTemplateRegistrations = async (
|
|
1047
1225
|
fromFilePath,
|
|
1048
1226
|
compiledTemplate,
|
|
1049
|
-
) =>
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1227
|
+
) => {
|
|
1228
|
+
const registrations = await Promise.all(
|
|
1229
|
+
(compiledTemplate.sourceReferences || []).map(async (reference) => {
|
|
1230
|
+
if (
|
|
1231
|
+
reference.startsWith('@assets/') ||
|
|
1232
|
+
reference.startsWith('assets/')
|
|
1233
|
+
) {
|
|
1234
|
+
return [];
|
|
1235
|
+
}
|
|
1057
1236
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1237
|
+
const sourcePath = resolveTwigTemplate(
|
|
1238
|
+
reference,
|
|
1239
|
+
dirname(fromFilePath),
|
|
1240
|
+
options,
|
|
1241
|
+
);
|
|
1242
|
+
if (!sourcePath) return [];
|
|
1243
|
+
|
|
1244
|
+
const absoluteSourcePath = resolve(sourcePath);
|
|
1245
|
+
addDependencyImporter(absoluteSourcePath, sourceFilePath);
|
|
1246
|
+
this.addWatchFile(absoluteSourcePath);
|
|
1247
|
+
const sourceText = await readFile(absoluteSourcePath, 'utf8');
|
|
1248
|
+
|
|
1249
|
+
return unique([
|
|
1250
|
+
reference,
|
|
1251
|
+
templateIdForPath(absoluteSourcePath, options),
|
|
1252
|
+
]).map(
|
|
1253
|
+
(key) =>
|
|
1254
|
+
`__emulsifySourceTemplates.set(${JSON.stringify(
|
|
1255
|
+
key,
|
|
1256
|
+
)}, ${JSON.stringify(sourceText)});`,
|
|
1257
|
+
);
|
|
1258
|
+
}),
|
|
1259
|
+
);
|
|
1260
|
+
|
|
1261
|
+
return registrations.flat();
|
|
1262
|
+
};
|
|
1080
1263
|
|
|
1081
1264
|
try {
|
|
1082
|
-
const compiled = compileTwigTemplate(
|
|
1265
|
+
const compiled = await compileTwigTemplate(
|
|
1083
1266
|
sourceFilePath,
|
|
1084
1267
|
options,
|
|
1085
1268
|
compileCache,
|
|
1086
1269
|
);
|
|
1087
|
-
compileDependencyTemplates(
|
|
1270
|
+
await compileDependencyTemplates(
|
|
1088
1271
|
compiled.includes,
|
|
1089
1272
|
sourceFilePath,
|
|
1090
1273
|
compileCache,
|
|
@@ -1097,28 +1280,40 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
1097
1280
|
.map(([dependencyPath, compiledDependency], index) => ({
|
|
1098
1281
|
dependencyPath,
|
|
1099
1282
|
compiledDependency,
|
|
1100
|
-
|
|
1283
|
+
idVariableName: `__emulsifyDependencyTemplateId${index}`,
|
|
1284
|
+
paramsVariableName: `__emulsifyDependencyTemplateParams${index}`,
|
|
1285
|
+
templateVariableName: `__emulsifyDependency${index}`,
|
|
1286
|
+
virtualModuleId: encodeVirtualTwigDependencyId(dependencyPath),
|
|
1101
1287
|
}));
|
|
1288
|
+
const dependencyTemplateImports = dependencyTemplateRecords
|
|
1289
|
+
.map(
|
|
1290
|
+
({ idVariableName, paramsVariableName, virtualModuleId }) =>
|
|
1291
|
+
`import { templateId as ${idVariableName}, templateParams as ${paramsVariableName} } from ${JSON.stringify(
|
|
1292
|
+
virtualModuleId,
|
|
1293
|
+
)};`,
|
|
1294
|
+
)
|
|
1295
|
+
.join('\n');
|
|
1102
1296
|
const dependencyTemplateCode = dependencyTemplateRecords
|
|
1103
1297
|
.map(
|
|
1104
|
-
({
|
|
1105
|
-
const ${
|
|
1106
|
-
|
|
1107
|
-
|
|
1298
|
+
({ idVariableName, paramsVariableName, templateVariableName }) => `
|
|
1299
|
+
const ${templateVariableName} = ${makeTemplateInstantiationFromParamsCode(
|
|
1300
|
+
idVariableName,
|
|
1301
|
+
paramsVariableName,
|
|
1108
1302
|
)};
|
|
1303
|
+
${templateVariableName}.method = 'emulsify';
|
|
1109
1304
|
`,
|
|
1110
1305
|
)
|
|
1111
1306
|
.join('\n');
|
|
1112
1307
|
const includeTemplateRegistrations = [
|
|
1113
1308
|
...dependencyTemplateRecords.flatMap(
|
|
1114
|
-
({ dependencyPath,
|
|
1309
|
+
({ dependencyPath, templateVariableName }) =>
|
|
1115
1310
|
Array.from(
|
|
1116
1311
|
compiledDependencyReferences.get(dependencyPath) || [],
|
|
1117
1312
|
).map(
|
|
1118
1313
|
(reference) =>
|
|
1119
1314
|
`__emulsifyIncludeTemplates.set(${JSON.stringify(
|
|
1120
1315
|
reference,
|
|
1121
|
-
)}, (context = {}) => ${
|
|
1316
|
+
)}, (context = {}) => ${templateVariableName}.render(context));`,
|
|
1122
1317
|
),
|
|
1123
1318
|
),
|
|
1124
1319
|
...Array.from(
|
|
@@ -1130,32 +1325,49 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
1130
1325
|
)}, (context = {}) => __emulsifyTemplate.render(context));`,
|
|
1131
1326
|
),
|
|
1132
1327
|
].join('\n');
|
|
1133
|
-
const sourceTemplateRegistrations =
|
|
1134
|
-
|
|
1135
|
-
(
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1328
|
+
const sourceTemplateRegistrations = (
|
|
1329
|
+
await Promise.all([
|
|
1330
|
+
...dependencyTemplateRecords.map(
|
|
1331
|
+
({ dependencyPath, compiledDependency }) =>
|
|
1332
|
+
makeSourceTemplateRegistrations(
|
|
1333
|
+
dependencyPath,
|
|
1334
|
+
compiledDependency,
|
|
1335
|
+
),
|
|
1336
|
+
),
|
|
1337
|
+
makeSourceTemplateRegistrations(sourceFilePath, compiled),
|
|
1338
|
+
])
|
|
1339
|
+
)
|
|
1340
|
+
.flat()
|
|
1341
|
+
.join('\n');
|
|
1143
1342
|
const renderErrorPrefix = JSON.stringify(
|
|
1144
1343
|
`An error occurred whilst rendering ${toPosixPath(filePath)}: `,
|
|
1145
1344
|
);
|
|
1146
1345
|
const moduleCode = `
|
|
1346
|
+
${dependencyTemplateImports}
|
|
1147
1347
|
import { factory } from 'twig';
|
|
1148
1348
|
import { registerTwigExtensions } from '@emulsify/core/extensions/twig';
|
|
1149
|
-
|
|
1349
|
+
import { installProjectTwigExtensions } from 'virtual:emulsify-twig-extension-installers';
|
|
1150
1350
|
import { createTwigIncludeFunction } from '@emulsify/core/storybook/twig/include-function';
|
|
1151
1351
|
import { createTwigSourceFunction } from '@emulsify/core/storybook/twig/source-function';
|
|
1152
1352
|
|
|
1153
1353
|
const Twig = factory();
|
|
1154
1354
|
registerTwigExtensions(Twig);
|
|
1155
|
-
|
|
1355
|
+
installProjectTwigExtensions(Twig);
|
|
1356
|
+
Twig.extend((TwigCore) => {
|
|
1357
|
+
TwigCore.Templates.registerLoader(
|
|
1358
|
+
'emulsify',
|
|
1359
|
+
(location, params = {}) => {
|
|
1360
|
+
const templateName = params.path || params.id || location;
|
|
1361
|
+
throw new TwigCore.Error(
|
|
1362
|
+
'Unable to find template ' + templateName + '.',
|
|
1363
|
+
);
|
|
1364
|
+
},
|
|
1365
|
+
);
|
|
1366
|
+
});
|
|
1156
1367
|
|
|
1157
1368
|
${dependencyTemplateCode}
|
|
1158
1369
|
const __emulsifyTemplate = ${compiled.code};
|
|
1370
|
+
__emulsifyTemplate.method = 'emulsify';
|
|
1159
1371
|
const __emulsifyIncludeTemplates = new Map();
|
|
1160
1372
|
const __emulsifySourceTemplates = new Map();
|
|
1161
1373
|
${includeTemplateRegistrations}
|
|
@@ -1192,21 +1404,55 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
1192
1404
|
}
|
|
1193
1405
|
},
|
|
1194
1406
|
handleHotUpdate({ file, server }) {
|
|
1195
|
-
|
|
1407
|
+
const filePath = resolve(file);
|
|
1408
|
+
const componentRoot = options.namespaces?.components
|
|
1409
|
+
? resolve(options.namespaces.components)
|
|
1410
|
+
: null;
|
|
1411
|
+
const cachedComponentRoots = componentRoot
|
|
1412
|
+
? componentGroupRootsCache.get(componentRoot)
|
|
1413
|
+
: undefined;
|
|
1414
|
+
let fileIsDirectory = false;
|
|
1415
|
+
|
|
1416
|
+
try {
|
|
1417
|
+
fileIsDirectory = fs.statSync(filePath).isDirectory();
|
|
1418
|
+
} catch {
|
|
1419
|
+
// Removed paths cannot be inspected.
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
const componentDirectoryChanged =
|
|
1423
|
+
!!componentRoot &&
|
|
1424
|
+
isWithinRoot(componentRoot, filePath) &&
|
|
1425
|
+
(fileIsDirectory || cachedComponentRoots?.includes(filePath));
|
|
1426
|
+
if (componentDirectoryChanged) {
|
|
1427
|
+
componentGroupRootsCache.delete(componentRoot);
|
|
1428
|
+
resolutionCache.clear();
|
|
1429
|
+
compileCache.clear();
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
if (!file.endsWith('.twig') && !componentDirectoryChanged) {
|
|
1196
1433
|
return undefined;
|
|
1197
1434
|
}
|
|
1198
1435
|
|
|
1199
|
-
const filePath = resolve(file);
|
|
1200
1436
|
const fileExists = safeExists(filePath);
|
|
1201
1437
|
const knownFile = knownTwigFiles.has(filePath);
|
|
1202
|
-
|
|
1438
|
+
const dependencyModuleId = resolvedVirtualTwigDependencyId(filePath);
|
|
1203
1439
|
const importers = dependencyImporters.get(filePath);
|
|
1204
|
-
|
|
1440
|
+
const projectRoot = options.projectDir || options.root;
|
|
1441
|
+
const projectPathChanged =
|
|
1442
|
+
!!projectRoot &&
|
|
1443
|
+
isWithinRoot(resolve(projectRoot), filePath) &&
|
|
1444
|
+
fileExists !== knownFile;
|
|
1445
|
+
const structuralChange = componentDirectoryChanged || projectPathChanged;
|
|
1446
|
+
|
|
1447
|
+
if (file.endsWith('.twig')) {
|
|
1448
|
+
compileCache.delete(filePath);
|
|
1449
|
+
}
|
|
1450
|
+
if (!fileExists && file.endsWith('.twig')) {
|
|
1205
1451
|
dependencyImporters.delete(filePath);
|
|
1206
1452
|
knownTwigFiles.delete(filePath);
|
|
1453
|
+
transformedTwigModules.delete(filePath);
|
|
1207
1454
|
}
|
|
1208
1455
|
|
|
1209
|
-
const projectRoot = options.projectDir || options.root;
|
|
1210
1456
|
if (projectRoot && isWithinRoot(resolve(projectRoot), filePath)) {
|
|
1211
1457
|
/**
|
|
1212
1458
|
* Existing files only need entries for their own path and source
|
|
@@ -1220,26 +1466,52 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
1220
1466
|
}
|
|
1221
1467
|
}
|
|
1222
1468
|
|
|
1223
|
-
if (
|
|
1469
|
+
if (
|
|
1470
|
+
componentRoot &&
|
|
1471
|
+
isWithinRoot(componentRoot, filePath) &&
|
|
1472
|
+
structuralChange
|
|
1473
|
+
) {
|
|
1474
|
+
componentGroupRootsCache.delete(componentRoot);
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (structuralChange) {
|
|
1478
|
+
compileCache.clear();
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
const affectedImporters = new Set(importers || []);
|
|
1482
|
+
if (structuralChange) {
|
|
1483
|
+
for (const transformedModule of transformedTwigModules) {
|
|
1484
|
+
affectedImporters.add(transformedModule);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
if (!affectedImporters.size) {
|
|
1224
1489
|
return undefined;
|
|
1225
1490
|
}
|
|
1226
1491
|
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1492
|
+
const moduleGraph = server?.moduleGraph;
|
|
1493
|
+
if (!moduleGraph?.getModulesByFile) {
|
|
1494
|
+
return undefined;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
const modules = new Set(moduleGraph.getModulesByFile(filePath) || []);
|
|
1498
|
+
const dependencyModule = moduleGraph.getModuleById?.(dependencyModuleId);
|
|
1499
|
+
if (dependencyModule) {
|
|
1500
|
+
moduleGraph.invalidateModule?.(dependencyModule);
|
|
1501
|
+
modules.add(dependencyModule);
|
|
1502
|
+
}
|
|
1503
|
+
for (const importer of affectedImporters) {
|
|
1231
1504
|
compileCache.delete(importer);
|
|
1232
1505
|
|
|
1233
|
-
const importerModules =
|
|
1234
|
-
server.moduleGraph.getModulesByFile(importer) || [];
|
|
1506
|
+
const importerModules = moduleGraph.getModulesByFile(importer) || [];
|
|
1235
1507
|
|
|
1236
1508
|
for (const module of importerModules) {
|
|
1237
|
-
|
|
1509
|
+
moduleGraph.invalidateModule?.(module);
|
|
1238
1510
|
modules.add(module);
|
|
1239
1511
|
}
|
|
1240
1512
|
}
|
|
1241
1513
|
|
|
1242
|
-
return Array.from(modules);
|
|
1514
|
+
return modules.size ? Array.from(modules) : undefined;
|
|
1243
1515
|
},
|
|
1244
1516
|
};
|
|
1245
1517
|
}
|