@emulsify/core 4.2.0 → 4.2.1
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.
|
@@ -74,6 +74,13 @@ const compileCache = new Map();
|
|
|
74
74
|
*/
|
|
75
75
|
const resolutionCache = new Map();
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Cache recursively discovered component grouping directories by root.
|
|
79
|
+
*
|
|
80
|
+
* @type {Map<string, string[]>}
|
|
81
|
+
*/
|
|
82
|
+
const componentGroupRootsCache = new Map();
|
|
83
|
+
|
|
77
84
|
/**
|
|
78
85
|
* Track Twig files that have been seen during this build/session.
|
|
79
86
|
*
|
|
@@ -355,6 +362,43 @@ const isWithinRoot = (root, filePath) => {
|
|
|
355
362
|
);
|
|
356
363
|
};
|
|
357
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Return the first component template candidate contained by its configured root.
|
|
367
|
+
*
|
|
368
|
+
* Both lexical and real paths are checked so `..` segments and symlinks cannot
|
|
369
|
+
* escape the component root.
|
|
370
|
+
*
|
|
371
|
+
* @param {string[]} paths - Candidate absolute paths.
|
|
372
|
+
* @param {string} componentRoot - Absolute component root path.
|
|
373
|
+
* @returns {string|undefined} Existing component template path.
|
|
374
|
+
*/
|
|
375
|
+
const findExistingComponentTemplateFile = (paths, componentRoot) => {
|
|
376
|
+
const absoluteRoot = resolve(componentRoot);
|
|
377
|
+
let realRoot;
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
realRoot = fs.realpathSync(absoluteRoot);
|
|
381
|
+
} catch {
|
|
382
|
+
return undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return paths.filter(Boolean).find((filePath) => {
|
|
386
|
+
const absoluteFilePath = resolve(filePath);
|
|
387
|
+
if (!isWithinRoot(absoluteRoot, absoluteFilePath)) {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
return (
|
|
393
|
+
fs.statSync(absoluteFilePath).isFile() &&
|
|
394
|
+
isWithinRoot(realRoot, fs.realpathSync(absoluteFilePath))
|
|
395
|
+
);
|
|
396
|
+
} catch {
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
});
|
|
400
|
+
};
|
|
401
|
+
|
|
358
402
|
/**
|
|
359
403
|
* Find the most specific configured Twig root for a template file.
|
|
360
404
|
*
|
|
@@ -511,7 +555,11 @@ const parseTwigNamespaceReference = (templatePath, namespaces = {}) => {
|
|
|
511
555
|
};
|
|
512
556
|
|
|
513
557
|
/**
|
|
514
|
-
* Return
|
|
558
|
+
* Return grouping directories below the configured component root.
|
|
559
|
+
*
|
|
560
|
+
* Breadth-first traversal preserves direct and one-level behavior before
|
|
561
|
+
* searching deeper groups. Siblings use code-point order so duplicate
|
|
562
|
+
* shorthand names resolve consistently across filesystems.
|
|
515
563
|
*
|
|
516
564
|
* @param {string} componentRoot - Absolute component root path.
|
|
517
565
|
* @returns {string[]} Absolute grouping directory paths.
|
|
@@ -519,33 +567,57 @@ const parseTwigNamespaceReference = (templatePath, namespaces = {}) => {
|
|
|
519
567
|
const componentGroupRoots = (componentRoot) => {
|
|
520
568
|
if (!componentRoot) return [];
|
|
521
569
|
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
return
|
|
525
|
-
|
|
570
|
+
const absoluteRoot = resolve(componentRoot);
|
|
571
|
+
if (componentGroupRootsCache.has(absoluteRoot)) {
|
|
572
|
+
return componentGroupRootsCache.get(absoluteRoot);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
const groupRoots = [];
|
|
576
|
+
const pendingDirectories = [absoluteRoot];
|
|
577
|
+
|
|
578
|
+
for (let index = 0; index < pendingDirectories.length; index += 1) {
|
|
579
|
+
const directory = pendingDirectories[index];
|
|
580
|
+
let entries;
|
|
581
|
+
|
|
582
|
+
try {
|
|
583
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
584
|
+
} catch {
|
|
585
|
+
continue;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const childDirectories = entries
|
|
526
589
|
.filter((entry) => entry.isDirectory())
|
|
527
|
-
.
|
|
528
|
-
|
|
529
|
-
|
|
590
|
+
.sort(({ name: left }, { name: right }) =>
|
|
591
|
+
left === right ? 0 : left < right ? -1 : 1,
|
|
592
|
+
)
|
|
593
|
+
.map((entry) => resolve(directory, entry.name))
|
|
594
|
+
.filter((childDirectory) => isWithinRoot(absoluteRoot, childDirectory));
|
|
595
|
+
|
|
596
|
+
groupRoots.push(...childDirectories);
|
|
597
|
+
pendingDirectories.push(...childDirectories);
|
|
530
598
|
}
|
|
599
|
+
|
|
600
|
+
componentGroupRootsCache.set(absoluteRoot, groupRoots);
|
|
601
|
+
return groupRoots;
|
|
531
602
|
};
|
|
532
603
|
|
|
533
604
|
/**
|
|
534
|
-
* Resolve a component reference through
|
|
605
|
+
* Resolve a component reference through recursively grouped directories.
|
|
535
606
|
*
|
|
536
607
|
* Project-scoped component IDs can use the component name (`project:button`)
|
|
537
|
-
* even when projects organize components under grouping
|
|
538
|
-
* `
|
|
608
|
+
* even when projects organize components under grouping paths such as
|
|
609
|
+
* `atoms/text`.
|
|
539
610
|
*
|
|
540
611
|
* @param {string} templatePath - Component-relative template reference.
|
|
541
612
|
* @param {string} componentRoot - Absolute component root path.
|
|
542
613
|
* @returns {string|null} Existing template path when found.
|
|
543
614
|
*/
|
|
544
615
|
const resolveGroupedComponentTemplate = (templatePath, componentRoot) =>
|
|
545
|
-
|
|
616
|
+
findExistingComponentTemplateFile(
|
|
546
617
|
componentGroupRoots(componentRoot).flatMap((groupRoot) =>
|
|
547
618
|
buildTemplateFileCandidates(groupRoot, templatePath),
|
|
548
619
|
),
|
|
620
|
+
componentRoot,
|
|
549
621
|
) || null;
|
|
550
622
|
|
|
551
623
|
/**
|
|
@@ -562,8 +634,9 @@ const resolveComponentShorthandReference = (templatePath, componentRoot) => {
|
|
|
562
634
|
templatePath.startsWith('@') && !templatePath.includes('/')
|
|
563
635
|
? templatePath.slice(1)
|
|
564
636
|
: templatePath;
|
|
565
|
-
const directComponentPath =
|
|
637
|
+
const directComponentPath = findExistingComponentTemplateFile(
|
|
566
638
|
buildTemplateFileCandidates(componentRoot, shorthandPath),
|
|
639
|
+
componentRoot,
|
|
567
640
|
);
|
|
568
641
|
if (directComponentPath) {
|
|
569
642
|
return directComponentPath;
|
|
@@ -577,8 +650,9 @@ const resolveComponentShorthandReference = (templatePath, componentRoot) => {
|
|
|
577
650
|
const genericComponentPath = genericNamespace[1];
|
|
578
651
|
|
|
579
652
|
return (
|
|
580
|
-
|
|
653
|
+
findExistingComponentTemplateFile(
|
|
581
654
|
buildTemplateFileCandidates(componentRoot, genericComponentPath),
|
|
655
|
+
componentRoot,
|
|
582
656
|
) || resolveGroupedComponentTemplate(genericComponentPath, componentRoot)
|
|
583
657
|
);
|
|
584
658
|
};
|
|
@@ -614,9 +688,15 @@ const resolveTwigTemplateWithoutCache = (templatePath, fromDir, options) => {
|
|
|
614
688
|
options.namespaces,
|
|
615
689
|
);
|
|
616
690
|
if (namespaced) {
|
|
617
|
-
const namespacedTemplate =
|
|
618
|
-
|
|
619
|
-
|
|
691
|
+
const namespacedTemplate =
|
|
692
|
+
namespaced.namespace === 'components'
|
|
693
|
+
? findExistingComponentTemplateFile(
|
|
694
|
+
buildTemplateFileCandidates(namespaced.root, namespaced.path),
|
|
695
|
+
namespaced.root,
|
|
696
|
+
)
|
|
697
|
+
: findExistingTemplateFile(
|
|
698
|
+
buildTemplateFileCandidates(namespaced.root, namespaced.path),
|
|
699
|
+
);
|
|
620
700
|
if (namespacedTemplate) {
|
|
621
701
|
return namespacedTemplate;
|
|
622
702
|
}
|
|
@@ -914,6 +994,16 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
914
994
|
*/
|
|
915
995
|
const dependencyImporters = new Map();
|
|
916
996
|
|
|
997
|
+
/**
|
|
998
|
+
* Twig entry modules transformed by this plugin instance.
|
|
999
|
+
*
|
|
1000
|
+
* Structural component changes invalidate these modules because a new or
|
|
1001
|
+
* removed directory can change the target of a shorthand reference.
|
|
1002
|
+
*
|
|
1003
|
+
* @type {Set<string>}
|
|
1004
|
+
*/
|
|
1005
|
+
const transformedTwigModules = new Set();
|
|
1006
|
+
|
|
917
1007
|
/**
|
|
918
1008
|
* Remember that one imported Twig module depends on another Twig file.
|
|
919
1009
|
*
|
|
@@ -948,7 +1038,9 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
948
1038
|
buildStart() {
|
|
949
1039
|
compileCache.clear();
|
|
950
1040
|
resolutionCache.clear();
|
|
1041
|
+
componentGroupRootsCache.clear();
|
|
951
1042
|
knownTwigFiles.clear();
|
|
1043
|
+
transformedTwigModules.clear();
|
|
952
1044
|
},
|
|
953
1045
|
transform(...args) {
|
|
954
1046
|
const [, id] = args;
|
|
@@ -958,6 +1050,7 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
958
1050
|
|
|
959
1051
|
const filePath = stripRequestQuery(id);
|
|
960
1052
|
const sourceFilePath = resolve(filePath);
|
|
1053
|
+
transformedTwigModules.add(sourceFilePath);
|
|
961
1054
|
/** @type {Map<string, ReturnType<typeof compileTwigTemplate>>} */
|
|
962
1055
|
const compiledDependencyTemplates = new Map();
|
|
963
1056
|
/** @type {Map<string, Set<string>>} */
|
|
@@ -1106,6 +1199,7 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
1106
1199
|
compiledDependency.templateId,
|
|
1107
1200
|
compiledDependency.templateParams,
|
|
1108
1201
|
)};
|
|
1202
|
+
${variableName}.method = 'emulsify';
|
|
1109
1203
|
`,
|
|
1110
1204
|
)
|
|
1111
1205
|
.join('\n');
|
|
@@ -1153,9 +1247,21 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
1153
1247
|
const Twig = factory();
|
|
1154
1248
|
registerTwigExtensions(Twig);
|
|
1155
1249
|
registerConfiguredTwigExtensions(Twig);
|
|
1250
|
+
Twig.extend((TwigCore) => {
|
|
1251
|
+
TwigCore.Templates.registerLoader(
|
|
1252
|
+
'emulsify',
|
|
1253
|
+
(location, params = {}) => {
|
|
1254
|
+
const templateName = params.path || params.id || location;
|
|
1255
|
+
throw new TwigCore.Error(
|
|
1256
|
+
'Unable to find template ' + templateName + '.',
|
|
1257
|
+
);
|
|
1258
|
+
},
|
|
1259
|
+
);
|
|
1260
|
+
});
|
|
1156
1261
|
|
|
1157
1262
|
${dependencyTemplateCode}
|
|
1158
1263
|
const __emulsifyTemplate = ${compiled.code};
|
|
1264
|
+
__emulsifyTemplate.method = 'emulsify';
|
|
1159
1265
|
const __emulsifyIncludeTemplates = new Map();
|
|
1160
1266
|
const __emulsifySourceTemplates = new Map();
|
|
1161
1267
|
${includeTemplateRegistrations}
|
|
@@ -1192,21 +1298,54 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
1192
1298
|
}
|
|
1193
1299
|
},
|
|
1194
1300
|
handleHotUpdate({ file, server }) {
|
|
1195
|
-
|
|
1301
|
+
const filePath = resolve(file);
|
|
1302
|
+
const componentRoot = options.namespaces?.components
|
|
1303
|
+
? resolve(options.namespaces.components)
|
|
1304
|
+
: null;
|
|
1305
|
+
const cachedComponentRoots = componentRoot
|
|
1306
|
+
? componentGroupRootsCache.get(componentRoot)
|
|
1307
|
+
: undefined;
|
|
1308
|
+
let fileIsDirectory = false;
|
|
1309
|
+
|
|
1310
|
+
try {
|
|
1311
|
+
fileIsDirectory = fs.statSync(filePath).isDirectory();
|
|
1312
|
+
} catch {
|
|
1313
|
+
// Removed paths cannot be inspected.
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const componentDirectoryChanged =
|
|
1317
|
+
!!componentRoot &&
|
|
1318
|
+
isWithinRoot(componentRoot, filePath) &&
|
|
1319
|
+
(fileIsDirectory || cachedComponentRoots?.includes(filePath));
|
|
1320
|
+
if (componentDirectoryChanged) {
|
|
1321
|
+
componentGroupRootsCache.delete(componentRoot);
|
|
1322
|
+
resolutionCache.clear();
|
|
1323
|
+
compileCache.clear();
|
|
1324
|
+
}
|
|
1325
|
+
|
|
1326
|
+
if (!file.endsWith('.twig') && !componentDirectoryChanged) {
|
|
1196
1327
|
return undefined;
|
|
1197
1328
|
}
|
|
1198
1329
|
|
|
1199
|
-
const filePath = resolve(file);
|
|
1200
1330
|
const fileExists = safeExists(filePath);
|
|
1201
1331
|
const knownFile = knownTwigFiles.has(filePath);
|
|
1202
|
-
compileCache.delete(filePath);
|
|
1203
1332
|
const importers = dependencyImporters.get(filePath);
|
|
1204
|
-
|
|
1333
|
+
const projectRoot = options.projectDir || options.root;
|
|
1334
|
+
const projectPathChanged =
|
|
1335
|
+
!!projectRoot &&
|
|
1336
|
+
isWithinRoot(resolve(projectRoot), filePath) &&
|
|
1337
|
+
fileExists !== knownFile;
|
|
1338
|
+
const structuralChange = componentDirectoryChanged || projectPathChanged;
|
|
1339
|
+
|
|
1340
|
+
if (file.endsWith('.twig')) {
|
|
1341
|
+
compileCache.delete(filePath);
|
|
1342
|
+
}
|
|
1343
|
+
if (!fileExists && file.endsWith('.twig')) {
|
|
1205
1344
|
dependencyImporters.delete(filePath);
|
|
1206
1345
|
knownTwigFiles.delete(filePath);
|
|
1346
|
+
transformedTwigModules.delete(filePath);
|
|
1207
1347
|
}
|
|
1208
1348
|
|
|
1209
|
-
const projectRoot = options.projectDir || options.root;
|
|
1210
1349
|
if (projectRoot && isWithinRoot(resolve(projectRoot), filePath)) {
|
|
1211
1350
|
/**
|
|
1212
1351
|
* Existing files only need entries for their own path and source
|
|
@@ -1220,26 +1359,47 @@ export function emulsifyTwigModulePlugin(options) {
|
|
|
1220
1359
|
}
|
|
1221
1360
|
}
|
|
1222
1361
|
|
|
1223
|
-
if (
|
|
1362
|
+
if (
|
|
1363
|
+
componentRoot &&
|
|
1364
|
+
isWithinRoot(componentRoot, filePath) &&
|
|
1365
|
+
structuralChange
|
|
1366
|
+
) {
|
|
1367
|
+
componentGroupRootsCache.delete(componentRoot);
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
if (structuralChange) {
|
|
1371
|
+
compileCache.clear();
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
const affectedImporters = new Set(importers || []);
|
|
1375
|
+
if (structuralChange) {
|
|
1376
|
+
for (const transformedModule of transformedTwigModules) {
|
|
1377
|
+
affectedImporters.add(transformedModule);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
if (!affectedImporters.size) {
|
|
1224
1382
|
return undefined;
|
|
1225
1383
|
}
|
|
1226
1384
|
|
|
1227
|
-
const
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1385
|
+
const moduleGraph = server?.moduleGraph;
|
|
1386
|
+
if (!moduleGraph?.getModulesByFile) {
|
|
1387
|
+
return undefined;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const modules = new Set(moduleGraph.getModulesByFile(filePath) || []);
|
|
1391
|
+
for (const importer of affectedImporters) {
|
|
1231
1392
|
compileCache.delete(importer);
|
|
1232
1393
|
|
|
1233
|
-
const importerModules =
|
|
1234
|
-
server.moduleGraph.getModulesByFile(importer) || [];
|
|
1394
|
+
const importerModules = moduleGraph.getModulesByFile(importer) || [];
|
|
1235
1395
|
|
|
1236
1396
|
for (const module of importerModules) {
|
|
1237
|
-
|
|
1397
|
+
moduleGraph.invalidateModule?.(module);
|
|
1238
1398
|
modules.add(module);
|
|
1239
1399
|
}
|
|
1240
1400
|
}
|
|
1241
1401
|
|
|
1242
|
-
return Array.from(modules);
|
|
1402
|
+
return modules.size ? Array.from(modules) : undefined;
|
|
1243
1403
|
},
|
|
1244
1404
|
};
|
|
1245
1405
|
}
|
package/package.json
CHANGED
|
@@ -91,9 +91,26 @@ function findGroupedComponentEntry(map, candidates, env) {
|
|
|
91
91
|
|
|
92
92
|
for (const { rootRel, suffix } of groupedComponentSuffixes(candidates, env)) {
|
|
93
93
|
const rootPrefix = `${rootRel}/`;
|
|
94
|
-
const
|
|
95
|
-
([key]) => key.startsWith(rootPrefix) && key.endsWith(suffix)
|
|
96
|
-
|
|
94
|
+
const matches = entries
|
|
95
|
+
.filter(([key]) => key.startsWith(rootPrefix) && key.endsWith(suffix))
|
|
96
|
+
.sort(([leftKey], [rightKey]) => {
|
|
97
|
+
const leftGroupingPath = leftKey.slice(
|
|
98
|
+
rootPrefix.length,
|
|
99
|
+
-suffix.length,
|
|
100
|
+
);
|
|
101
|
+
const rightGroupingPath = rightKey.slice(
|
|
102
|
+
rootPrefix.length,
|
|
103
|
+
-suffix.length,
|
|
104
|
+
);
|
|
105
|
+
const leftDepth = leftGroupingPath.split('/').filter(Boolean).length;
|
|
106
|
+
const rightDepth = rightGroupingPath.split('/').filter(Boolean).length;
|
|
107
|
+
|
|
108
|
+
if (leftDepth !== rightDepth) {
|
|
109
|
+
return leftDepth - rightDepth;
|
|
110
|
+
}
|
|
111
|
+
return leftKey === rightKey ? 0 : leftKey < rightKey ? -1 : 1;
|
|
112
|
+
});
|
|
113
|
+
const match = matches[0];
|
|
97
114
|
if (match) {
|
|
98
115
|
return { key: match[0], value: match[1] };
|
|
99
116
|
}
|