@emulsify/core 4.4.0 → 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/README.md +10 -1
- package/config/a11y-wcag22.js +11 -0
- package/config/vite/plugins/assets/source-file-index.js +6 -12
- package/config/vite/plugins/twig/twig-module.js +35 -258
- package/config/vite/utils/source-directory-skips.js +13 -0
- package/config/vite/utils/twig-component-resolver.js +316 -0
- package/package.json +31 -28
- package/scripts/a11y.js +88 -9
- package/scripts/audit/checks/twig-references.js +16 -5
- 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 +361 -51
- package/scripts/audit-twig-stories.js +73 -3
package/README.md
CHANGED
|
@@ -50,6 +50,11 @@ See [Version Evolution](docs/version-evolution.md) for major-version history
|
|
|
50
50
|
and the [4.3.0 release notes](docs/releases/4.3.0.md) for the compatibility
|
|
51
51
|
changes and additions in that release.
|
|
52
52
|
|
|
53
|
+
The [compatibility and support policy](docs/version-evolution.md#compatibility-and-support-policy)
|
|
54
|
+
separates current requirements and the existing 4.x compatibility rule from
|
|
55
|
+
maintenance promises. Pending decisions live in the
|
|
56
|
+
[maintainer register](docs/maintainer-decisions.md).
|
|
57
|
+
|
|
53
58
|
## Authoring Models
|
|
54
59
|
|
|
55
60
|
Emulsify Core supports Twig and React authoring workflows plus a focused
|
|
@@ -269,7 +274,11 @@ Do not add comments to JSON files, lockfiles, binary assets, generated output,
|
|
|
269
274
|
legal documents, or dependency files. Those formats either do not support
|
|
270
275
|
comments or should remain exact artifacts.
|
|
271
276
|
|
|
272
|
-
Please also follow the issue template and pull request templates provided.
|
|
277
|
+
Please also follow the issue template and pull request templates provided.
|
|
278
|
+
The issue links below are for ordinary bugs and feature requests, not
|
|
279
|
+
confidential vulnerability details. See the
|
|
280
|
+
[security-reporting status](docs/maintainer-decisions.md#security-reporting-for-older-lines)
|
|
281
|
+
before preparing a security report.
|
|
273
282
|
|
|
274
283
|
1. [Emulsify Drupal](https://github.com/emulsify-ds/emulsify-drupal/issues)
|
|
275
284
|
2. [Emulsify Tools (Drupal module)](https://www.drupal.org/project/issues/emulsify_tools)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional additions to Pa11y's axe rules for WCAG 2.2 level A and AA.
|
|
3
|
+
* The reviewed Pa11y axe-core 4.11.4 and Core root axe-core 4.13.0 each expose
|
|
4
|
+
* target-size as their only wcag22a/wcag22aa rule.
|
|
5
|
+
* Keep this reviewed list explicit instead of enabling future rules implicitly.
|
|
6
|
+
*/
|
|
7
|
+
export default {
|
|
8
|
+
pa11y: {
|
|
9
|
+
rules: ['target-size'],
|
|
10
|
+
},
|
|
11
|
+
};
|
|
@@ -9,16 +9,7 @@ import { readdirSync } from 'fs';
|
|
|
9
9
|
import { join, relative, sep } from 'path';
|
|
10
10
|
|
|
11
11
|
import { relativeFrom } from '../../project-structure.js';
|
|
12
|
-
|
|
13
|
-
const DEFAULT_SKIP_DIRS = [
|
|
14
|
-
'node_modules',
|
|
15
|
-
'.git',
|
|
16
|
-
'.cache',
|
|
17
|
-
'.vite',
|
|
18
|
-
'.out',
|
|
19
|
-
'.coverage',
|
|
20
|
-
'dist',
|
|
21
|
-
];
|
|
12
|
+
import { DEFAULT_SKIP_DIRS } from '../../utils/source-directory-skips.js';
|
|
22
13
|
|
|
23
14
|
/**
|
|
24
15
|
* Depth-first walk to list every file under a given root.
|
|
@@ -91,13 +82,16 @@ export const isComponentMetadataFile = (filePath) =>
|
|
|
91
82
|
|
|
92
83
|
/**
|
|
93
84
|
* Determine whether a file should be copied by the static asset pass.
|
|
85
|
+
* Server-side and uncompiled module sources can share roots with static assets
|
|
86
|
+
* without being published by this pass. Unknown non-code extensions still copy.
|
|
94
87
|
*
|
|
95
88
|
* @param {string} filePath - Absolute or relative file path.
|
|
96
89
|
* @returns {boolean} TRUE for non-code source assets.
|
|
97
90
|
*/
|
|
98
91
|
export const isStaticSourceAsset = (filePath) =>
|
|
99
|
-
!/\.(
|
|
100
|
-
|
|
92
|
+
!/\.([cm]?[jt]sx?|scss|twig|map|php\d?|phtml|inc|module|theme|install|profile|engine)$/i.test(
|
|
93
|
+
filePath,
|
|
94
|
+
) && !isComponentMetadataFile(filePath);
|
|
101
95
|
|
|
102
96
|
/**
|
|
103
97
|
* Build the roots that should not be crawled during a global source pass.
|
|
@@ -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);
|