@bamboocss/parser 1.25.0 → 1.28.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/dist/index.cjs +265 -9
- package/dist/index.d.cts +40 -2
- package/dist/index.d.mts +40 -2
- package/dist/index.mjs +265 -9
- package/package.json +10 -10
package/dist/index.cjs
CHANGED
|
@@ -410,6 +410,11 @@ function createReportMaps() {
|
|
|
410
410
|
}
|
|
411
411
|
//#endregion
|
|
412
412
|
//#region src/get-module-specifier-value.ts
|
|
413
|
+
/**
|
|
414
|
+
* Both declaration kinds carry a specifier, and `export { x } from './m'` is how a barrel
|
|
415
|
+
* forwards a recipe — so this reads either. An `export { x }` with no `from` returns
|
|
416
|
+
* undefined, which is the same answer the throwing case gives.
|
|
417
|
+
*/
|
|
413
418
|
const getModuleSpecifierValue = (node) => {
|
|
414
419
|
try {
|
|
415
420
|
return node.getModuleSpecifierValue();
|
|
@@ -418,6 +423,194 @@ const getModuleSpecifierValue = (node) => {
|
|
|
418
423
|
}
|
|
419
424
|
};
|
|
420
425
|
//#endregion
|
|
426
|
+
//#region src/imported-recipes.ts
|
|
427
|
+
/**
|
|
428
|
+
* A module's exported recipes, memoized.
|
|
429
|
+
*
|
|
430
|
+
* Per *target*, so a barrel imported by two hundred files is walked once. Cleared alongside
|
|
431
|
+
* the box-node cache, which is the same invalidation this needs: both memoize a conclusion
|
|
432
|
+
* drawn from another file's contents.
|
|
433
|
+
*/
|
|
434
|
+
const exportedRecipes = /* @__PURE__ */ new Map();
|
|
435
|
+
const clearImportedRecipeCache = () => {
|
|
436
|
+
exportedRecipes.clear();
|
|
437
|
+
};
|
|
438
|
+
/** The names a file bound `cva`/`sva` to, or empty when it imports neither. */
|
|
439
|
+
const recipeFactoryAliases = (sourceFile, imports) => {
|
|
440
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
441
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
442
|
+
if (declaration.isTypeOnly()) continue;
|
|
443
|
+
const mod = getModuleSpecifierValue(declaration);
|
|
444
|
+
if (!mod) continue;
|
|
445
|
+
for (const specifier of declaration.getNamedImports()) {
|
|
446
|
+
if (specifier.isTypeOnly()) continue;
|
|
447
|
+
const name = specifier.getNameNode().getText();
|
|
448
|
+
if (name !== "cva" && name !== "sva") continue;
|
|
449
|
+
const alias = specifier.getAliasNode()?.getText() || name;
|
|
450
|
+
if (!imports.match({
|
|
451
|
+
name,
|
|
452
|
+
alias,
|
|
453
|
+
mod,
|
|
454
|
+
kind: "named"
|
|
455
|
+
})) continue;
|
|
456
|
+
aliases.add(alias);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return aliases;
|
|
460
|
+
};
|
|
461
|
+
/** Recipes this module declares itself, by the name it declared them under. */
|
|
462
|
+
const declaredRecipes = (sourceFile, imports) => {
|
|
463
|
+
const declared = /* @__PURE__ */ new Map();
|
|
464
|
+
const exported = /* @__PURE__ */ new Set();
|
|
465
|
+
const factories = recipeFactoryAliases(sourceFile, imports);
|
|
466
|
+
if (factories.size === 0) return {
|
|
467
|
+
declared,
|
|
468
|
+
exported
|
|
469
|
+
};
|
|
470
|
+
const filePath = sourceFile.getFilePath();
|
|
471
|
+
for (const statement of sourceFile.compilerNode.statements) {
|
|
472
|
+
if (!ts_morph.ts.isVariableStatement(statement)) continue;
|
|
473
|
+
if (!(statement.declarationList.flags & ts_morph.ts.NodeFlags.Const)) continue;
|
|
474
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
475
|
+
const initializer = declaration.initializer;
|
|
476
|
+
if (!initializer || !ts_morph.ts.isCallExpression(initializer)) continue;
|
|
477
|
+
const callee = initializer.expression;
|
|
478
|
+
if (!ts_morph.ts.isIdentifier(callee) || !ts_morph.ts.isIdentifier(declaration.name)) continue;
|
|
479
|
+
if (!factories.has(callee.text)) continue;
|
|
480
|
+
declared.set(declaration.name.text, {
|
|
481
|
+
filePath,
|
|
482
|
+
name: declaration.name.text
|
|
483
|
+
});
|
|
484
|
+
if (statement.modifiers?.some((modifier) => modifier.kind === ts_morph.ts.SyntaxKind.ExportKeyword)) exported.add(declaration.name.text);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
declared,
|
|
489
|
+
exported
|
|
490
|
+
};
|
|
491
|
+
};
|
|
492
|
+
/**
|
|
493
|
+
* Every name this module exports that is bound to a recipe, and where it was declared.
|
|
494
|
+
*
|
|
495
|
+
* Follows `export { x } from './m'`, `export * from './m'`, and `import { x } … export { x }`,
|
|
496
|
+
* because a barrel is how these are reached in practice. The origin is carried through each
|
|
497
|
+
* hop rather than recomputed, so a consumer learns the *declaring* module however many
|
|
498
|
+
* re-exports stand between them.
|
|
499
|
+
*
|
|
500
|
+
* A star export contributes only names nothing else exports, which is what the language does:
|
|
501
|
+
* an explicit export shadows one arriving through `export *`. Resolving that the other way
|
|
502
|
+
* folded a call against a config it would never have run.
|
|
503
|
+
*/
|
|
504
|
+
const walkExports = (sourceFile, imports, resolveModule, seen) => {
|
|
505
|
+
const cached = exportedRecipes.get(sourceFile);
|
|
506
|
+
if (cached) return {
|
|
507
|
+
names: cached,
|
|
508
|
+
complete: true
|
|
509
|
+
};
|
|
510
|
+
if (seen.has(sourceFile)) return {
|
|
511
|
+
names: /* @__PURE__ */ new Map(),
|
|
512
|
+
complete: false
|
|
513
|
+
};
|
|
514
|
+
seen.add(sourceFile);
|
|
515
|
+
const names = /* @__PURE__ */ new Map();
|
|
516
|
+
/** Reachable under a local name — declarations, plus recipes this module imports. */
|
|
517
|
+
const local = /* @__PURE__ */ new Map();
|
|
518
|
+
/** Held back so an explicit export of the same name wins. */
|
|
519
|
+
const starred = /* @__PURE__ */ new Map();
|
|
520
|
+
/** Names two different star exports both carry, which resolve to neither. */
|
|
521
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
522
|
+
let complete = true;
|
|
523
|
+
const { declared, exported } = declaredRecipes(sourceFile, imports);
|
|
524
|
+
for (const [name, origin] of declared) {
|
|
525
|
+
local.set(name, origin);
|
|
526
|
+
if (exported.has(name)) names.set(name, origin);
|
|
527
|
+
}
|
|
528
|
+
const imported = walkImports(sourceFile, imports, resolveModule, seen);
|
|
529
|
+
complete &&= imported.complete;
|
|
530
|
+
for (const [alias, origin] of imported.bindings) if (!local.has(alias)) local.set(alias, origin);
|
|
531
|
+
for (const declaration of sourceFile.getExportDeclarations()) {
|
|
532
|
+
if (declaration.isTypeOnly()) continue;
|
|
533
|
+
const specifier = getModuleSpecifierValue(declaration);
|
|
534
|
+
const target = specifier ? resolveModule(specifier, sourceFile) : void 0;
|
|
535
|
+
if (specifier && !target) continue;
|
|
536
|
+
if (declaration.getNamespaceExport()) continue;
|
|
537
|
+
if (declaration.isNamespaceExport()) {
|
|
538
|
+
if (!target) continue;
|
|
539
|
+
const walk = walkExports(target, imports, resolveModule, seen);
|
|
540
|
+
complete &&= walk.complete;
|
|
541
|
+
for (const [name, origin] of walk.names) {
|
|
542
|
+
const existing = starred.get(name);
|
|
543
|
+
if (existing && (existing.filePath !== origin.filePath || existing.name !== origin.name)) {
|
|
544
|
+
ambiguous.add(name);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
starred.set(name, origin);
|
|
548
|
+
}
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
let source = local;
|
|
552
|
+
if (target) {
|
|
553
|
+
const walk = walkExports(target, imports, resolveModule, seen);
|
|
554
|
+
complete &&= walk.complete;
|
|
555
|
+
source = walk.names;
|
|
556
|
+
}
|
|
557
|
+
for (const exportSpecifier of declaration.getNamedExports()) {
|
|
558
|
+
if (exportSpecifier.isTypeOnly()) continue;
|
|
559
|
+
const name = exportSpecifier.getNameNode().getText();
|
|
560
|
+
const origin = source.get(name);
|
|
561
|
+
if (origin) names.set(exportSpecifier.getAliasNode()?.getText() || name, origin);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
for (const [name, origin] of starred) if (!names.has(name) && !ambiguous.has(name)) names.set(name, origin);
|
|
565
|
+
if (complete) exportedRecipes.set(sourceFile, names);
|
|
566
|
+
return {
|
|
567
|
+
names,
|
|
568
|
+
complete
|
|
569
|
+
};
|
|
570
|
+
};
|
|
571
|
+
/**
|
|
572
|
+
* Local names in this file bound, through an import, to a recipe declared elsewhere.
|
|
573
|
+
*
|
|
574
|
+
* Reports truncation for the same reason `walkExports` does: `export { x }` with no `from`
|
|
575
|
+
* resolves through here, so a walk cut short by a cycle can leave a name out — and caching
|
|
576
|
+
* that as the whole answer is what makes visibility depend on where the walk began.
|
|
577
|
+
*/
|
|
578
|
+
const walkImports = (sourceFile, imports, resolveModule, seen) => {
|
|
579
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
580
|
+
let complete = true;
|
|
581
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
582
|
+
if (declaration.isTypeOnly()) continue;
|
|
583
|
+
const named = declaration.getNamedImports();
|
|
584
|
+
if (named.length === 0) continue;
|
|
585
|
+
const specifier = getModuleSpecifierValue(declaration);
|
|
586
|
+
if (!specifier) continue;
|
|
587
|
+
const target = resolveModule(specifier, sourceFile);
|
|
588
|
+
if (!target || target === sourceFile) continue;
|
|
589
|
+
const walk = walkExports(target, imports, resolveModule, seen);
|
|
590
|
+
complete &&= walk.complete;
|
|
591
|
+
const { names } = walk;
|
|
592
|
+
if (names.size === 0) continue;
|
|
593
|
+
for (const importSpecifier of named) {
|
|
594
|
+
if (importSpecifier.isTypeOnly()) continue;
|
|
595
|
+
const origin = names.get(importSpecifier.getNameNode().getText());
|
|
596
|
+
if (!origin) continue;
|
|
597
|
+
bindings.set(importSpecifier.getAliasNode()?.getText() || importSpecifier.getNameNode().getText(), origin);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return {
|
|
601
|
+
bindings,
|
|
602
|
+
complete
|
|
603
|
+
};
|
|
604
|
+
};
|
|
605
|
+
/**
|
|
606
|
+
* Local names in this file bound, through an import, to a recipe declared elsewhere.
|
|
607
|
+
*
|
|
608
|
+
* Only specifiers the caller's resolver can place inside the project. A recipe outside the
|
|
609
|
+
* project's `include` is never parsed, so it has no rules in the stylesheet and could not be
|
|
610
|
+
* folded against even if the binding resolved.
|
|
611
|
+
*/
|
|
612
|
+
const importedRecipeBindings = (sourceFile, imports, resolveModule) => walkImports(sourceFile, imports, resolveModule, new Set([sourceFile])).bindings;
|
|
613
|
+
//#endregion
|
|
421
614
|
//#region src/get-import-declarations.ts
|
|
422
615
|
function getImportDeclarations(context, sourceFile) {
|
|
423
616
|
const { imports, tsOptions } = context;
|
|
@@ -860,14 +1053,16 @@ const fallbackImpl = (...values) => values.some((value) => value === void 0) ? v
|
|
|
860
1053
|
const evaluateOptions = { environment: defaultEnv };
|
|
861
1054
|
function createParser(context) {
|
|
862
1055
|
const { jsx, imports, recipes } = context;
|
|
863
|
-
return function parse(sourceFile, encoder, options) {
|
|
1056
|
+
return function parse(sourceFile, encoder, options, resolveModule) {
|
|
864
1057
|
if (!sourceFile) return;
|
|
865
1058
|
const importDeclarations = getImportDeclarations(context, sourceFile);
|
|
866
1059
|
const file = imports.file(importDeclarations);
|
|
867
1060
|
const filePath = sourceFile.getFilePath();
|
|
868
1061
|
_bamboocss_logger.logger.debug("ast:import", !file.isEmpty() ? `Found import { ${file.toString()} } in ${filePath}` : `No import found in ${filePath}`);
|
|
869
1062
|
const parserResult = new ParserResult(context, encoder);
|
|
870
|
-
|
|
1063
|
+
const importedRecipes = resolveModule ? importedRecipeBindings(sourceFile, imports, resolveModule) : /* @__PURE__ */ new Map();
|
|
1064
|
+
if (file.isEmpty() && !jsx.isEnabled && importedRecipes.size === 0) return parserResult;
|
|
1065
|
+
for (const binding of importedRecipes.keys()) file.addLocalRecipe(binding);
|
|
871
1066
|
if (file.importsRecipeFactory()) for (const statement of sourceFile.compilerNode.statements) {
|
|
872
1067
|
if (!ts_morph.ts.isVariableStatement(statement)) continue;
|
|
873
1068
|
if (!(statement.declarationList.flags & ts_morph.ts.NodeFlags.Const)) continue;
|
|
@@ -882,6 +1077,27 @@ function createParser(context) {
|
|
|
882
1077
|
file.addLocalRecipe(declaration.name.text);
|
|
883
1078
|
}
|
|
884
1079
|
}
|
|
1080
|
+
/**
|
|
1081
|
+
* `css(recipe.raw(props), …)` loses what the recipe would have contributed.
|
|
1082
|
+
*
|
|
1083
|
+
* `.raw` on a recipe or pattern takes props and returns styles; the build reads it as the
|
|
1084
|
+
* identity `css.raw` means. Resolving it properly would mean running the recipe here, and
|
|
1085
|
+
* emitting the wrong styles is worse than emitting none — so this says so rather than
|
|
1086
|
+
* guessing.
|
|
1087
|
+
*
|
|
1088
|
+
* Defined out here rather than inside `getEvaluateOptions`, which runs per call node.
|
|
1089
|
+
*/
|
|
1090
|
+
const reportUnresolvedRaw = (base, at) => {
|
|
1091
|
+
const { line, column } = sourceFile.getLineAndColumnAtPos(at.getStart());
|
|
1092
|
+
parserResult.unresolved.push({
|
|
1093
|
+
kind: "atomic",
|
|
1094
|
+
prop: base,
|
|
1095
|
+
filePath,
|
|
1096
|
+
line,
|
|
1097
|
+
column,
|
|
1098
|
+
reason: "unresolved-raw"
|
|
1099
|
+
});
|
|
1100
|
+
};
|
|
885
1101
|
(0, _bamboocss_extractor.extract)({
|
|
886
1102
|
ast: sourceFile,
|
|
887
1103
|
tokens: context.tokens ? {
|
|
@@ -920,8 +1136,14 @@ function createParser(context) {
|
|
|
920
1136
|
}
|
|
921
1137
|
if (!ts_morph.Node.isPropertyAccessExpression(propAccessExpr)) return evaluateOptions;
|
|
922
1138
|
let name = propAccessExpr.getText();
|
|
1139
|
+
const rawBase = name.endsWith(".raw") ? name.slice(0, -4) : void 0;
|
|
1140
|
+
if (rawBase && file.isLocalRecipe(rawBase)) {
|
|
1141
|
+
reportUnresolvedRaw(rawBase, node);
|
|
1142
|
+
return evaluateOptions;
|
|
1143
|
+
}
|
|
923
1144
|
if (!file.isRawFn(name)) return evaluateOptions;
|
|
924
1145
|
name = name.replace(".raw", "");
|
|
1146
|
+
if (file.isValidRecipe(name) || file.isValidPattern(name)) reportUnresolvedRaw(name, node);
|
|
925
1147
|
return { environment: Object.assign({}, defaultEnv, { extra: { [name]: { raw: (v) => v } } }) };
|
|
926
1148
|
},
|
|
927
1149
|
flags: { skipTraverseFiles: false }
|
|
@@ -937,7 +1159,8 @@ function createParser(context) {
|
|
|
937
1159
|
if (query.kind === "call-expression") parserResult.setCvaCall(alias, {
|
|
938
1160
|
name: alias,
|
|
939
1161
|
box: query.box.value[0] ?? _bamboocss_extractor.box.fallback(query.box),
|
|
940
|
-
data: combineResult((0, _bamboocss_extractor.unbox)(query.box.value[0]))
|
|
1162
|
+
data: combineResult((0, _bamboocss_extractor.unbox)(query.box.value[0])),
|
|
1163
|
+
origin: importedRecipes.get(alias)
|
|
941
1164
|
});
|
|
942
1165
|
});
|
|
943
1166
|
return;
|
|
@@ -1009,6 +1232,17 @@ function createParser(context) {
|
|
|
1009
1232
|
}
|
|
1010
1233
|
//#endregion
|
|
1011
1234
|
//#region src/project.ts
|
|
1235
|
+
/**
|
|
1236
|
+
* Everything memoized against another file's contents.
|
|
1237
|
+
*
|
|
1238
|
+
* Both caches answer a question about a *different* module than the one being parsed — what
|
|
1239
|
+
* an identifier resolved to, and which recipes a module exports — so both go stale on exactly
|
|
1240
|
+
* the same events, and clearing one without the other leaves the pair disagreeing.
|
|
1241
|
+
*/
|
|
1242
|
+
const invalidateResolutions = () => {
|
|
1243
|
+
(0, _bamboocss_extractor.clearBoxNodeCache)();
|
|
1244
|
+
clearImportedRecipeCache();
|
|
1245
|
+
};
|
|
1012
1246
|
const normalizeCompilerOptions = (raw) => {
|
|
1013
1247
|
if (!raw) return {};
|
|
1014
1248
|
const { options } = ts_morph.ts.convertCompilerOptionsFromJson(raw, process.cwd());
|
|
@@ -1092,6 +1326,18 @@ var Project = class {
|
|
|
1092
1326
|
* `.d.ts` into the project. The graph only tracks files bamboo already scans.
|
|
1093
1327
|
*/
|
|
1094
1328
|
moduleResolutionCache;
|
|
1329
|
+
/**
|
|
1330
|
+
* Everything memoized against the shape of the file tree, including the negative half.
|
|
1331
|
+
*
|
|
1332
|
+
* `resolveModuleName` caches failures too, so a specifier that resolved to nothing before
|
|
1333
|
+
* its target existed stays unresolved for the life of the process. That silently dropped a
|
|
1334
|
+
* dependency edge; now it would also leave a recipe permanently invisible to the module
|
|
1335
|
+
* importing it, since resolution is what finds one.
|
|
1336
|
+
*/
|
|
1337
|
+
invalidate = (fileTreeChanged = true) => {
|
|
1338
|
+
invalidateResolutions();
|
|
1339
|
+
if (fileTreeChanged) this.moduleResolutionCache = void 0;
|
|
1340
|
+
};
|
|
1095
1341
|
resolveImport = (decl) => {
|
|
1096
1342
|
const moduleName = decl.getModuleSpecifierValue();
|
|
1097
1343
|
if (!moduleName) return;
|
|
@@ -1100,6 +1346,16 @@ var Project = class {
|
|
|
1100
1346
|
const name = ts_morph.ts.resolveModuleName(moduleName, decl.getSourceFile().getFilePath(), compilerOptions, this.project.getModuleResolutionHost(), this.moduleResolutionCache).resolvedModule?.resolvedFileName;
|
|
1101
1347
|
return name ? this.project.getSourceFile(name) : void 0;
|
|
1102
1348
|
};
|
|
1349
|
+
/**
|
|
1350
|
+
* `resolveImport` in the shape a caller can use, for a specifier read off any declaration.
|
|
1351
|
+
*
|
|
1352
|
+
* Shares the module resolution cache above, so a barrel resolved while tracking
|
|
1353
|
+
* dependencies is not resolved again while looking for recipes.
|
|
1354
|
+
*/
|
|
1355
|
+
resolveModule = (specifier, from) => this.resolveImport({
|
|
1356
|
+
getModuleSpecifierValue: () => specifier,
|
|
1357
|
+
getSourceFile: () => from
|
|
1358
|
+
});
|
|
1103
1359
|
trackDependencies = (filePath, sourceFile) => {
|
|
1104
1360
|
const importer = this.normalizePath(sourceFile.getFilePath());
|
|
1105
1361
|
this.canonicalPaths.set(this.normalizePath(filePath), importer);
|
|
@@ -1146,7 +1402,7 @@ var Project = class {
|
|
|
1146
1402
|
};
|
|
1147
1403
|
createSourceFile = (filePath) => {
|
|
1148
1404
|
const { readFile } = this.options;
|
|
1149
|
-
|
|
1405
|
+
this.invalidate();
|
|
1150
1406
|
return this.project.createSourceFile(filePath, readFile(filePath), {
|
|
1151
1407
|
overwrite: true,
|
|
1152
1408
|
scriptKind: ts_morph.ScriptKind.TSX
|
|
@@ -1157,7 +1413,7 @@ var Project = class {
|
|
|
1157
1413
|
for (const file of files) this.createSourceFile(file);
|
|
1158
1414
|
};
|
|
1159
1415
|
addSourceFile = (filePath, content) => {
|
|
1160
|
-
(
|
|
1416
|
+
this.invalidate(!(filePath.includes("/") && this.project.getSourceFile(filePath)));
|
|
1161
1417
|
return this.project.createSourceFile(filePath, content, {
|
|
1162
1418
|
overwrite: true,
|
|
1163
1419
|
scriptKind: ts_morph.ScriptKind.TSX
|
|
@@ -1166,18 +1422,18 @@ var Project = class {
|
|
|
1166
1422
|
removeSourceFile = (filePath) => {
|
|
1167
1423
|
const sourceFile = this.project.getSourceFile(filePath);
|
|
1168
1424
|
if (sourceFile) {
|
|
1169
|
-
|
|
1425
|
+
this.invalidate();
|
|
1170
1426
|
return this.project.removeSourceFile(sourceFile);
|
|
1171
1427
|
}
|
|
1172
1428
|
return false;
|
|
1173
1429
|
};
|
|
1174
1430
|
reloadSourceFile = (filePath) => {
|
|
1175
|
-
|
|
1431
|
+
this.invalidate(false);
|
|
1176
1432
|
return this.getSourceFile(filePath)?.refreshFromFileSystemSync();
|
|
1177
1433
|
};
|
|
1178
1434
|
reloadSourceFiles = () => {
|
|
1179
1435
|
const files = this.getFiles();
|
|
1180
|
-
|
|
1436
|
+
this.invalidate();
|
|
1181
1437
|
for (const file of files) this.getSourceFile(file)?.refreshFromFileSystemSync() ?? this.project.addSourceFileAtPath(file);
|
|
1182
1438
|
};
|
|
1183
1439
|
get readFile() {
|
|
@@ -1211,7 +1467,7 @@ var Project = class {
|
|
|
1211
1467
|
}
|
|
1212
1468
|
}) ?? this.transformFile(filePath, original);
|
|
1213
1469
|
if (original !== transformed) sourceFile.replaceWithText(transformed);
|
|
1214
|
-
const result = this.parser(sourceFile, encoder, options)?.setFilePath(filePath);
|
|
1470
|
+
const result = this.parser(sourceFile, encoder, options, this.resolveModule)?.setFilePath(filePath);
|
|
1215
1471
|
hooks["parser:after"]?.({
|
|
1216
1472
|
filePath,
|
|
1217
1473
|
result
|
package/dist/index.d.cts
CHANGED
|
@@ -136,6 +136,9 @@ declare class Generator extends Context {
|
|
|
136
136
|
* Get CSS for a specific theme
|
|
137
137
|
*/
|
|
138
138
|
//#endregion
|
|
139
|
+
//#region src/imported-recipes.d.ts
|
|
140
|
+
type ResolveModule = (specifier: string, from: SourceFile) => SourceFile | undefined;
|
|
141
|
+
//#endregion
|
|
139
142
|
//#region src/unresolved-styles.d.ts
|
|
140
143
|
interface UnresolvedStyle {
|
|
141
144
|
/**
|
|
@@ -162,7 +165,17 @@ interface UnresolvedStyle {
|
|
|
162
165
|
* pair of alternatives.
|
|
163
166
|
* - `too-many-combinations` — more ternary branches than it will enumerate.
|
|
164
167
|
*/
|
|
165
|
-
reason: 'unresolvable-value' | 'missing-property' | 'unenumerable-keys'
|
|
168
|
+
reason: 'unresolvable-value' | 'missing-property' | 'unenumerable-keys'
|
|
169
|
+
/**
|
|
170
|
+
* `css(recipe.raw(props), …)` — the build reads `.raw` as returning what it was passed.
|
|
171
|
+
*
|
|
172
|
+
* True of `css.raw`, which is the identity it was written for. A recipe or pattern's
|
|
173
|
+
* `.raw` takes *props* and returns *styles*, so the build composes the props instead: the
|
|
174
|
+
* recipe's own declarations never reach the stylesheet, and its variant names are handed
|
|
175
|
+
* to the encoder as if they were properties. The browser then asks for classes no rule
|
|
176
|
+
* backs, and the element renders without them.
|
|
177
|
+
*/
|
|
178
|
+
| 'unresolved-raw';
|
|
166
179
|
}
|
|
167
180
|
declare const findUnresolvedStyles: (item: ResultItem, kind: "atomic") => UnresolvedStyle[];
|
|
168
181
|
//#endregion
|
|
@@ -241,7 +254,16 @@ declare class ParserResult implements ParserResultInterface {
|
|
|
241
254
|
}
|
|
242
255
|
//#endregion
|
|
243
256
|
//#region src/parser.d.ts
|
|
244
|
-
declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions
|
|
257
|
+
declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions,
|
|
258
|
+
/**
|
|
259
|
+
* How to place a module specifier inside the project.
|
|
260
|
+
*
|
|
261
|
+
* Supplied by the caller because it owns the resolution cache, and because the cheap
|
|
262
|
+
* implementation is a filesystem lookup rather than a symbol-table walk. Absent, a
|
|
263
|
+
* recipe declared in another module stays invisible exactly as before.
|
|
264
|
+
*/
|
|
265
|
+
|
|
266
|
+
resolveModule?: ResolveModule) => ParserResult | undefined;
|
|
245
267
|
//#endregion
|
|
246
268
|
//#region src/project.d.ts
|
|
247
269
|
interface ProjectOptions extends ProjectOptions$1 {
|
|
@@ -305,7 +327,23 @@ declare class Project {
|
|
|
305
327
|
* `.d.ts` into the project. The graph only tracks files bamboo already scans.
|
|
306
328
|
*/
|
|
307
329
|
private moduleResolutionCache;
|
|
330
|
+
/**
|
|
331
|
+
* Everything memoized against the shape of the file tree, including the negative half.
|
|
332
|
+
*
|
|
333
|
+
* `resolveModuleName` caches failures too, so a specifier that resolved to nothing before
|
|
334
|
+
* its target existed stays unresolved for the life of the process. That silently dropped a
|
|
335
|
+
* dependency edge; now it would also leave a recipe permanently invisible to the module
|
|
336
|
+
* importing it, since resolution is what finds one.
|
|
337
|
+
*/
|
|
338
|
+
private invalidate;
|
|
308
339
|
private resolveImport;
|
|
340
|
+
/**
|
|
341
|
+
* `resolveImport` in the shape a caller can use, for a specifier read off any declaration.
|
|
342
|
+
*
|
|
343
|
+
* Shares the module resolution cache above, so a barrel resolved while tracking
|
|
344
|
+
* dependencies is not resolved again while looking for recipes.
|
|
345
|
+
*/
|
|
346
|
+
private resolveModule;
|
|
309
347
|
private trackDependencies;
|
|
310
348
|
/**
|
|
311
349
|
* Every file that transitively imports `filePath`, so a watcher can re-parse the
|
package/dist/index.d.mts
CHANGED
|
@@ -136,6 +136,9 @@ declare class Generator extends Context {
|
|
|
136
136
|
* Get CSS for a specific theme
|
|
137
137
|
*/
|
|
138
138
|
//#endregion
|
|
139
|
+
//#region src/imported-recipes.d.ts
|
|
140
|
+
type ResolveModule = (specifier: string, from: SourceFile) => SourceFile | undefined;
|
|
141
|
+
//#endregion
|
|
139
142
|
//#region src/unresolved-styles.d.ts
|
|
140
143
|
interface UnresolvedStyle {
|
|
141
144
|
/**
|
|
@@ -162,7 +165,17 @@ interface UnresolvedStyle {
|
|
|
162
165
|
* pair of alternatives.
|
|
163
166
|
* - `too-many-combinations` — more ternary branches than it will enumerate.
|
|
164
167
|
*/
|
|
165
|
-
reason: 'unresolvable-value' | 'missing-property' | 'unenumerable-keys'
|
|
168
|
+
reason: 'unresolvable-value' | 'missing-property' | 'unenumerable-keys'
|
|
169
|
+
/**
|
|
170
|
+
* `css(recipe.raw(props), …)` — the build reads `.raw` as returning what it was passed.
|
|
171
|
+
*
|
|
172
|
+
* True of `css.raw`, which is the identity it was written for. A recipe or pattern's
|
|
173
|
+
* `.raw` takes *props* and returns *styles*, so the build composes the props instead: the
|
|
174
|
+
* recipe's own declarations never reach the stylesheet, and its variant names are handed
|
|
175
|
+
* to the encoder as if they were properties. The browser then asks for classes no rule
|
|
176
|
+
* backs, and the element renders without them.
|
|
177
|
+
*/
|
|
178
|
+
| 'unresolved-raw';
|
|
166
179
|
}
|
|
167
180
|
declare const findUnresolvedStyles: (item: ResultItem, kind: "atomic") => UnresolvedStyle[];
|
|
168
181
|
//#endregion
|
|
@@ -241,7 +254,16 @@ declare class ParserResult implements ParserResultInterface {
|
|
|
241
254
|
}
|
|
242
255
|
//#endregion
|
|
243
256
|
//#region src/parser.d.ts
|
|
244
|
-
declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions
|
|
257
|
+
declare function createParser(context: ParserOptions): (sourceFile: SourceFile | undefined, encoder?: Generator["encoder"], options?: ParserResultConfigureOptions,
|
|
258
|
+
/**
|
|
259
|
+
* How to place a module specifier inside the project.
|
|
260
|
+
*
|
|
261
|
+
* Supplied by the caller because it owns the resolution cache, and because the cheap
|
|
262
|
+
* implementation is a filesystem lookup rather than a symbol-table walk. Absent, a
|
|
263
|
+
* recipe declared in another module stays invisible exactly as before.
|
|
264
|
+
*/
|
|
265
|
+
|
|
266
|
+
resolveModule?: ResolveModule) => ParserResult | undefined;
|
|
245
267
|
//#endregion
|
|
246
268
|
//#region src/project.d.ts
|
|
247
269
|
interface ProjectOptions extends ProjectOptions$1 {
|
|
@@ -305,7 +327,23 @@ declare class Project {
|
|
|
305
327
|
* `.d.ts` into the project. The graph only tracks files bamboo already scans.
|
|
306
328
|
*/
|
|
307
329
|
private moduleResolutionCache;
|
|
330
|
+
/**
|
|
331
|
+
* Everything memoized against the shape of the file tree, including the negative half.
|
|
332
|
+
*
|
|
333
|
+
* `resolveModuleName` caches failures too, so a specifier that resolved to nothing before
|
|
334
|
+
* its target existed stays unresolved for the life of the process. That silently dropped a
|
|
335
|
+
* dependency edge; now it would also leave a recipe permanently invisible to the module
|
|
336
|
+
* importing it, since resolution is what finds one.
|
|
337
|
+
*/
|
|
338
|
+
private invalidate;
|
|
308
339
|
private resolveImport;
|
|
340
|
+
/**
|
|
341
|
+
* `resolveImport` in the shape a caller can use, for a specifier read off any declaration.
|
|
342
|
+
*
|
|
343
|
+
* Shares the module resolution cache above, so a barrel resolved while tracking
|
|
344
|
+
* dependencies is not resolved again while looking for recipes.
|
|
345
|
+
*/
|
|
346
|
+
private resolveModule;
|
|
309
347
|
private trackDependencies;
|
|
310
348
|
/**
|
|
311
349
|
* Every file that transitively imports `filePath`, so a watcher can re-parse the
|
package/dist/index.mjs
CHANGED
|
@@ -409,6 +409,11 @@ function createReportMaps() {
|
|
|
409
409
|
}
|
|
410
410
|
//#endregion
|
|
411
411
|
//#region src/get-module-specifier-value.ts
|
|
412
|
+
/**
|
|
413
|
+
* Both declaration kinds carry a specifier, and `export { x } from './m'` is how a barrel
|
|
414
|
+
* forwards a recipe — so this reads either. An `export { x }` with no `from` returns
|
|
415
|
+
* undefined, which is the same answer the throwing case gives.
|
|
416
|
+
*/
|
|
412
417
|
const getModuleSpecifierValue = (node) => {
|
|
413
418
|
try {
|
|
414
419
|
return node.getModuleSpecifierValue();
|
|
@@ -417,6 +422,194 @@ const getModuleSpecifierValue = (node) => {
|
|
|
417
422
|
}
|
|
418
423
|
};
|
|
419
424
|
//#endregion
|
|
425
|
+
//#region src/imported-recipes.ts
|
|
426
|
+
/**
|
|
427
|
+
* A module's exported recipes, memoized.
|
|
428
|
+
*
|
|
429
|
+
* Per *target*, so a barrel imported by two hundred files is walked once. Cleared alongside
|
|
430
|
+
* the box-node cache, which is the same invalidation this needs: both memoize a conclusion
|
|
431
|
+
* drawn from another file's contents.
|
|
432
|
+
*/
|
|
433
|
+
const exportedRecipes = /* @__PURE__ */ new Map();
|
|
434
|
+
const clearImportedRecipeCache = () => {
|
|
435
|
+
exportedRecipes.clear();
|
|
436
|
+
};
|
|
437
|
+
/** The names a file bound `cva`/`sva` to, or empty when it imports neither. */
|
|
438
|
+
const recipeFactoryAliases = (sourceFile, imports) => {
|
|
439
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
440
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
441
|
+
if (declaration.isTypeOnly()) continue;
|
|
442
|
+
const mod = getModuleSpecifierValue(declaration);
|
|
443
|
+
if (!mod) continue;
|
|
444
|
+
for (const specifier of declaration.getNamedImports()) {
|
|
445
|
+
if (specifier.isTypeOnly()) continue;
|
|
446
|
+
const name = specifier.getNameNode().getText();
|
|
447
|
+
if (name !== "cva" && name !== "sva") continue;
|
|
448
|
+
const alias = specifier.getAliasNode()?.getText() || name;
|
|
449
|
+
if (!imports.match({
|
|
450
|
+
name,
|
|
451
|
+
alias,
|
|
452
|
+
mod,
|
|
453
|
+
kind: "named"
|
|
454
|
+
})) continue;
|
|
455
|
+
aliases.add(alias);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return aliases;
|
|
459
|
+
};
|
|
460
|
+
/** Recipes this module declares itself, by the name it declared them under. */
|
|
461
|
+
const declaredRecipes = (sourceFile, imports) => {
|
|
462
|
+
const declared = /* @__PURE__ */ new Map();
|
|
463
|
+
const exported = /* @__PURE__ */ new Set();
|
|
464
|
+
const factories = recipeFactoryAliases(sourceFile, imports);
|
|
465
|
+
if (factories.size === 0) return {
|
|
466
|
+
declared,
|
|
467
|
+
exported
|
|
468
|
+
};
|
|
469
|
+
const filePath = sourceFile.getFilePath();
|
|
470
|
+
for (const statement of sourceFile.compilerNode.statements) {
|
|
471
|
+
if (!ts.isVariableStatement(statement)) continue;
|
|
472
|
+
if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
|
|
473
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
474
|
+
const initializer = declaration.initializer;
|
|
475
|
+
if (!initializer || !ts.isCallExpression(initializer)) continue;
|
|
476
|
+
const callee = initializer.expression;
|
|
477
|
+
if (!ts.isIdentifier(callee) || !ts.isIdentifier(declaration.name)) continue;
|
|
478
|
+
if (!factories.has(callee.text)) continue;
|
|
479
|
+
declared.set(declaration.name.text, {
|
|
480
|
+
filePath,
|
|
481
|
+
name: declaration.name.text
|
|
482
|
+
});
|
|
483
|
+
if (statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) exported.add(declaration.name.text);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
return {
|
|
487
|
+
declared,
|
|
488
|
+
exported
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
/**
|
|
492
|
+
* Every name this module exports that is bound to a recipe, and where it was declared.
|
|
493
|
+
*
|
|
494
|
+
* Follows `export { x } from './m'`, `export * from './m'`, and `import { x } … export { x }`,
|
|
495
|
+
* because a barrel is how these are reached in practice. The origin is carried through each
|
|
496
|
+
* hop rather than recomputed, so a consumer learns the *declaring* module however many
|
|
497
|
+
* re-exports stand between them.
|
|
498
|
+
*
|
|
499
|
+
* A star export contributes only names nothing else exports, which is what the language does:
|
|
500
|
+
* an explicit export shadows one arriving through `export *`. Resolving that the other way
|
|
501
|
+
* folded a call against a config it would never have run.
|
|
502
|
+
*/
|
|
503
|
+
const walkExports = (sourceFile, imports, resolveModule, seen) => {
|
|
504
|
+
const cached = exportedRecipes.get(sourceFile);
|
|
505
|
+
if (cached) return {
|
|
506
|
+
names: cached,
|
|
507
|
+
complete: true
|
|
508
|
+
};
|
|
509
|
+
if (seen.has(sourceFile)) return {
|
|
510
|
+
names: /* @__PURE__ */ new Map(),
|
|
511
|
+
complete: false
|
|
512
|
+
};
|
|
513
|
+
seen.add(sourceFile);
|
|
514
|
+
const names = /* @__PURE__ */ new Map();
|
|
515
|
+
/** Reachable under a local name — declarations, plus recipes this module imports. */
|
|
516
|
+
const local = /* @__PURE__ */ new Map();
|
|
517
|
+
/** Held back so an explicit export of the same name wins. */
|
|
518
|
+
const starred = /* @__PURE__ */ new Map();
|
|
519
|
+
/** Names two different star exports both carry, which resolve to neither. */
|
|
520
|
+
const ambiguous = /* @__PURE__ */ new Set();
|
|
521
|
+
let complete = true;
|
|
522
|
+
const { declared, exported } = declaredRecipes(sourceFile, imports);
|
|
523
|
+
for (const [name, origin] of declared) {
|
|
524
|
+
local.set(name, origin);
|
|
525
|
+
if (exported.has(name)) names.set(name, origin);
|
|
526
|
+
}
|
|
527
|
+
const imported = walkImports(sourceFile, imports, resolveModule, seen);
|
|
528
|
+
complete &&= imported.complete;
|
|
529
|
+
for (const [alias, origin] of imported.bindings) if (!local.has(alias)) local.set(alias, origin);
|
|
530
|
+
for (const declaration of sourceFile.getExportDeclarations()) {
|
|
531
|
+
if (declaration.isTypeOnly()) continue;
|
|
532
|
+
const specifier = getModuleSpecifierValue(declaration);
|
|
533
|
+
const target = specifier ? resolveModule(specifier, sourceFile) : void 0;
|
|
534
|
+
if (specifier && !target) continue;
|
|
535
|
+
if (declaration.getNamespaceExport()) continue;
|
|
536
|
+
if (declaration.isNamespaceExport()) {
|
|
537
|
+
if (!target) continue;
|
|
538
|
+
const walk = walkExports(target, imports, resolveModule, seen);
|
|
539
|
+
complete &&= walk.complete;
|
|
540
|
+
for (const [name, origin] of walk.names) {
|
|
541
|
+
const existing = starred.get(name);
|
|
542
|
+
if (existing && (existing.filePath !== origin.filePath || existing.name !== origin.name)) {
|
|
543
|
+
ambiguous.add(name);
|
|
544
|
+
continue;
|
|
545
|
+
}
|
|
546
|
+
starred.set(name, origin);
|
|
547
|
+
}
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
let source = local;
|
|
551
|
+
if (target) {
|
|
552
|
+
const walk = walkExports(target, imports, resolveModule, seen);
|
|
553
|
+
complete &&= walk.complete;
|
|
554
|
+
source = walk.names;
|
|
555
|
+
}
|
|
556
|
+
for (const exportSpecifier of declaration.getNamedExports()) {
|
|
557
|
+
if (exportSpecifier.isTypeOnly()) continue;
|
|
558
|
+
const name = exportSpecifier.getNameNode().getText();
|
|
559
|
+
const origin = source.get(name);
|
|
560
|
+
if (origin) names.set(exportSpecifier.getAliasNode()?.getText() || name, origin);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
for (const [name, origin] of starred) if (!names.has(name) && !ambiguous.has(name)) names.set(name, origin);
|
|
564
|
+
if (complete) exportedRecipes.set(sourceFile, names);
|
|
565
|
+
return {
|
|
566
|
+
names,
|
|
567
|
+
complete
|
|
568
|
+
};
|
|
569
|
+
};
|
|
570
|
+
/**
|
|
571
|
+
* Local names in this file bound, through an import, to a recipe declared elsewhere.
|
|
572
|
+
*
|
|
573
|
+
* Reports truncation for the same reason `walkExports` does: `export { x }` with no `from`
|
|
574
|
+
* resolves through here, so a walk cut short by a cycle can leave a name out — and caching
|
|
575
|
+
* that as the whole answer is what makes visibility depend on where the walk began.
|
|
576
|
+
*/
|
|
577
|
+
const walkImports = (sourceFile, imports, resolveModule, seen) => {
|
|
578
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
579
|
+
let complete = true;
|
|
580
|
+
for (const declaration of sourceFile.getImportDeclarations()) {
|
|
581
|
+
if (declaration.isTypeOnly()) continue;
|
|
582
|
+
const named = declaration.getNamedImports();
|
|
583
|
+
if (named.length === 0) continue;
|
|
584
|
+
const specifier = getModuleSpecifierValue(declaration);
|
|
585
|
+
if (!specifier) continue;
|
|
586
|
+
const target = resolveModule(specifier, sourceFile);
|
|
587
|
+
if (!target || target === sourceFile) continue;
|
|
588
|
+
const walk = walkExports(target, imports, resolveModule, seen);
|
|
589
|
+
complete &&= walk.complete;
|
|
590
|
+
const { names } = walk;
|
|
591
|
+
if (names.size === 0) continue;
|
|
592
|
+
for (const importSpecifier of named) {
|
|
593
|
+
if (importSpecifier.isTypeOnly()) continue;
|
|
594
|
+
const origin = names.get(importSpecifier.getNameNode().getText());
|
|
595
|
+
if (!origin) continue;
|
|
596
|
+
bindings.set(importSpecifier.getAliasNode()?.getText() || importSpecifier.getNameNode().getText(), origin);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
bindings,
|
|
601
|
+
complete
|
|
602
|
+
};
|
|
603
|
+
};
|
|
604
|
+
/**
|
|
605
|
+
* Local names in this file bound, through an import, to a recipe declared elsewhere.
|
|
606
|
+
*
|
|
607
|
+
* Only specifiers the caller's resolver can place inside the project. A recipe outside the
|
|
608
|
+
* project's `include` is never parsed, so it has no rules in the stylesheet and could not be
|
|
609
|
+
* folded against even if the binding resolved.
|
|
610
|
+
*/
|
|
611
|
+
const importedRecipeBindings = (sourceFile, imports, resolveModule) => walkImports(sourceFile, imports, resolveModule, new Set([sourceFile])).bindings;
|
|
612
|
+
//#endregion
|
|
420
613
|
//#region src/get-import-declarations.ts
|
|
421
614
|
function getImportDeclarations(context, sourceFile) {
|
|
422
615
|
const { imports, tsOptions } = context;
|
|
@@ -859,14 +1052,16 @@ const fallbackImpl = (...values) => values.some((value) => value === void 0) ? v
|
|
|
859
1052
|
const evaluateOptions = { environment: defaultEnv };
|
|
860
1053
|
function createParser(context) {
|
|
861
1054
|
const { jsx, imports, recipes } = context;
|
|
862
|
-
return function parse(sourceFile, encoder, options) {
|
|
1055
|
+
return function parse(sourceFile, encoder, options, resolveModule) {
|
|
863
1056
|
if (!sourceFile) return;
|
|
864
1057
|
const importDeclarations = getImportDeclarations(context, sourceFile);
|
|
865
1058
|
const file = imports.file(importDeclarations);
|
|
866
1059
|
const filePath = sourceFile.getFilePath();
|
|
867
1060
|
logger.debug("ast:import", !file.isEmpty() ? `Found import { ${file.toString()} } in ${filePath}` : `No import found in ${filePath}`);
|
|
868
1061
|
const parserResult = new ParserResult(context, encoder);
|
|
869
|
-
|
|
1062
|
+
const importedRecipes = resolveModule ? importedRecipeBindings(sourceFile, imports, resolveModule) : /* @__PURE__ */ new Map();
|
|
1063
|
+
if (file.isEmpty() && !jsx.isEnabled && importedRecipes.size === 0) return parserResult;
|
|
1064
|
+
for (const binding of importedRecipes.keys()) file.addLocalRecipe(binding);
|
|
870
1065
|
if (file.importsRecipeFactory()) for (const statement of sourceFile.compilerNode.statements) {
|
|
871
1066
|
if (!ts.isVariableStatement(statement)) continue;
|
|
872
1067
|
if (!(statement.declarationList.flags & ts.NodeFlags.Const)) continue;
|
|
@@ -881,6 +1076,27 @@ function createParser(context) {
|
|
|
881
1076
|
file.addLocalRecipe(declaration.name.text);
|
|
882
1077
|
}
|
|
883
1078
|
}
|
|
1079
|
+
/**
|
|
1080
|
+
* `css(recipe.raw(props), …)` loses what the recipe would have contributed.
|
|
1081
|
+
*
|
|
1082
|
+
* `.raw` on a recipe or pattern takes props and returns styles; the build reads it as the
|
|
1083
|
+
* identity `css.raw` means. Resolving it properly would mean running the recipe here, and
|
|
1084
|
+
* emitting the wrong styles is worse than emitting none — so this says so rather than
|
|
1085
|
+
* guessing.
|
|
1086
|
+
*
|
|
1087
|
+
* Defined out here rather than inside `getEvaluateOptions`, which runs per call node.
|
|
1088
|
+
*/
|
|
1089
|
+
const reportUnresolvedRaw = (base, at) => {
|
|
1090
|
+
const { line, column } = sourceFile.getLineAndColumnAtPos(at.getStart());
|
|
1091
|
+
parserResult.unresolved.push({
|
|
1092
|
+
kind: "atomic",
|
|
1093
|
+
prop: base,
|
|
1094
|
+
filePath,
|
|
1095
|
+
line,
|
|
1096
|
+
column,
|
|
1097
|
+
reason: "unresolved-raw"
|
|
1098
|
+
});
|
|
1099
|
+
};
|
|
884
1100
|
extract({
|
|
885
1101
|
ast: sourceFile,
|
|
886
1102
|
tokens: context.tokens ? {
|
|
@@ -919,8 +1135,14 @@ function createParser(context) {
|
|
|
919
1135
|
}
|
|
920
1136
|
if (!Node.isPropertyAccessExpression(propAccessExpr)) return evaluateOptions;
|
|
921
1137
|
let name = propAccessExpr.getText();
|
|
1138
|
+
const rawBase = name.endsWith(".raw") ? name.slice(0, -4) : void 0;
|
|
1139
|
+
if (rawBase && file.isLocalRecipe(rawBase)) {
|
|
1140
|
+
reportUnresolvedRaw(rawBase, node);
|
|
1141
|
+
return evaluateOptions;
|
|
1142
|
+
}
|
|
922
1143
|
if (!file.isRawFn(name)) return evaluateOptions;
|
|
923
1144
|
name = name.replace(".raw", "");
|
|
1145
|
+
if (file.isValidRecipe(name) || file.isValidPattern(name)) reportUnresolvedRaw(name, node);
|
|
924
1146
|
return { environment: Object.assign({}, defaultEnv, { extra: { [name]: { raw: (v) => v } } }) };
|
|
925
1147
|
},
|
|
926
1148
|
flags: { skipTraverseFiles: false }
|
|
@@ -936,7 +1158,8 @@ function createParser(context) {
|
|
|
936
1158
|
if (query.kind === "call-expression") parserResult.setCvaCall(alias, {
|
|
937
1159
|
name: alias,
|
|
938
1160
|
box: query.box.value[0] ?? box.fallback(query.box),
|
|
939
|
-
data: combineResult(unbox(query.box.value[0]))
|
|
1161
|
+
data: combineResult(unbox(query.box.value[0])),
|
|
1162
|
+
origin: importedRecipes.get(alias)
|
|
940
1163
|
});
|
|
941
1164
|
});
|
|
942
1165
|
return;
|
|
@@ -1008,6 +1231,17 @@ function createParser(context) {
|
|
|
1008
1231
|
}
|
|
1009
1232
|
//#endregion
|
|
1010
1233
|
//#region src/project.ts
|
|
1234
|
+
/**
|
|
1235
|
+
* Everything memoized against another file's contents.
|
|
1236
|
+
*
|
|
1237
|
+
* Both caches answer a question about a *different* module than the one being parsed — what
|
|
1238
|
+
* an identifier resolved to, and which recipes a module exports — so both go stale on exactly
|
|
1239
|
+
* the same events, and clearing one without the other leaves the pair disagreeing.
|
|
1240
|
+
*/
|
|
1241
|
+
const invalidateResolutions = () => {
|
|
1242
|
+
clearBoxNodeCache();
|
|
1243
|
+
clearImportedRecipeCache();
|
|
1244
|
+
};
|
|
1011
1245
|
const normalizeCompilerOptions = (raw) => {
|
|
1012
1246
|
if (!raw) return {};
|
|
1013
1247
|
const { options } = ts.convertCompilerOptionsFromJson(raw, process.cwd());
|
|
@@ -1091,6 +1325,18 @@ var Project = class {
|
|
|
1091
1325
|
* `.d.ts` into the project. The graph only tracks files bamboo already scans.
|
|
1092
1326
|
*/
|
|
1093
1327
|
moduleResolutionCache;
|
|
1328
|
+
/**
|
|
1329
|
+
* Everything memoized against the shape of the file tree, including the negative half.
|
|
1330
|
+
*
|
|
1331
|
+
* `resolveModuleName` caches failures too, so a specifier that resolved to nothing before
|
|
1332
|
+
* its target existed stays unresolved for the life of the process. That silently dropped a
|
|
1333
|
+
* dependency edge; now it would also leave a recipe permanently invisible to the module
|
|
1334
|
+
* importing it, since resolution is what finds one.
|
|
1335
|
+
*/
|
|
1336
|
+
invalidate = (fileTreeChanged = true) => {
|
|
1337
|
+
invalidateResolutions();
|
|
1338
|
+
if (fileTreeChanged) this.moduleResolutionCache = void 0;
|
|
1339
|
+
};
|
|
1094
1340
|
resolveImport = (decl) => {
|
|
1095
1341
|
const moduleName = decl.getModuleSpecifierValue();
|
|
1096
1342
|
if (!moduleName) return;
|
|
@@ -1099,6 +1345,16 @@ var Project = class {
|
|
|
1099
1345
|
const name = ts.resolveModuleName(moduleName, decl.getSourceFile().getFilePath(), compilerOptions, this.project.getModuleResolutionHost(), this.moduleResolutionCache).resolvedModule?.resolvedFileName;
|
|
1100
1346
|
return name ? this.project.getSourceFile(name) : void 0;
|
|
1101
1347
|
};
|
|
1348
|
+
/**
|
|
1349
|
+
* `resolveImport` in the shape a caller can use, for a specifier read off any declaration.
|
|
1350
|
+
*
|
|
1351
|
+
* Shares the module resolution cache above, so a barrel resolved while tracking
|
|
1352
|
+
* dependencies is not resolved again while looking for recipes.
|
|
1353
|
+
*/
|
|
1354
|
+
resolveModule = (specifier, from) => this.resolveImport({
|
|
1355
|
+
getModuleSpecifierValue: () => specifier,
|
|
1356
|
+
getSourceFile: () => from
|
|
1357
|
+
});
|
|
1102
1358
|
trackDependencies = (filePath, sourceFile) => {
|
|
1103
1359
|
const importer = this.normalizePath(sourceFile.getFilePath());
|
|
1104
1360
|
this.canonicalPaths.set(this.normalizePath(filePath), importer);
|
|
@@ -1145,7 +1401,7 @@ var Project = class {
|
|
|
1145
1401
|
};
|
|
1146
1402
|
createSourceFile = (filePath) => {
|
|
1147
1403
|
const { readFile } = this.options;
|
|
1148
|
-
|
|
1404
|
+
this.invalidate();
|
|
1149
1405
|
return this.project.createSourceFile(filePath, readFile(filePath), {
|
|
1150
1406
|
overwrite: true,
|
|
1151
1407
|
scriptKind: ScriptKind.TSX
|
|
@@ -1156,7 +1412,7 @@ var Project = class {
|
|
|
1156
1412
|
for (const file of files) this.createSourceFile(file);
|
|
1157
1413
|
};
|
|
1158
1414
|
addSourceFile = (filePath, content) => {
|
|
1159
|
-
|
|
1415
|
+
this.invalidate(!(filePath.includes("/") && this.project.getSourceFile(filePath)));
|
|
1160
1416
|
return this.project.createSourceFile(filePath, content, {
|
|
1161
1417
|
overwrite: true,
|
|
1162
1418
|
scriptKind: ScriptKind.TSX
|
|
@@ -1165,18 +1421,18 @@ var Project = class {
|
|
|
1165
1421
|
removeSourceFile = (filePath) => {
|
|
1166
1422
|
const sourceFile = this.project.getSourceFile(filePath);
|
|
1167
1423
|
if (sourceFile) {
|
|
1168
|
-
|
|
1424
|
+
this.invalidate();
|
|
1169
1425
|
return this.project.removeSourceFile(sourceFile);
|
|
1170
1426
|
}
|
|
1171
1427
|
return false;
|
|
1172
1428
|
};
|
|
1173
1429
|
reloadSourceFile = (filePath) => {
|
|
1174
|
-
|
|
1430
|
+
this.invalidate(false);
|
|
1175
1431
|
return this.getSourceFile(filePath)?.refreshFromFileSystemSync();
|
|
1176
1432
|
};
|
|
1177
1433
|
reloadSourceFiles = () => {
|
|
1178
1434
|
const files = this.getFiles();
|
|
1179
|
-
|
|
1435
|
+
this.invalidate();
|
|
1180
1436
|
for (const file of files) this.getSourceFile(file)?.refreshFromFileSystemSync() ?? this.project.addSourceFileAtPath(file);
|
|
1181
1437
|
};
|
|
1182
1438
|
get readFile() {
|
|
@@ -1210,7 +1466,7 @@ var Project = class {
|
|
|
1210
1466
|
}
|
|
1211
1467
|
}) ?? this.transformFile(filePath, original);
|
|
1212
1468
|
if (original !== transformed) sourceFile.replaceWithText(transformed);
|
|
1213
|
-
const result = this.parser(sourceFile, encoder, options)?.setFilePath(filePath);
|
|
1469
|
+
const result = this.parser(sourceFile, encoder, options, this.resolveModule)?.setFilePath(filePath);
|
|
1214
1470
|
hooks["parser:after"]?.({
|
|
1215
1471
|
filePath,
|
|
1216
1472
|
result
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/parser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.28.0",
|
|
4
4
|
"description": "The static parser for bamboo css",
|
|
5
5
|
"homepage": "https://bamboocss.com",
|
|
6
6
|
"license": "MIT",
|
|
@@ -34,17 +34,17 @@
|
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"ts-morph": "28.0.0",
|
|
36
36
|
"ts-pattern": "5.9.0",
|
|
37
|
-
"@bamboocss/config": "^1.
|
|
38
|
-
"@bamboocss/core": "^1.
|
|
39
|
-
"@bamboocss/extractor": "1.
|
|
40
|
-
"@bamboocss/logger": "1.
|
|
41
|
-
"@bamboocss/shared": "1.
|
|
42
|
-
"@bamboocss/types": "1.
|
|
37
|
+
"@bamboocss/config": "^1.28.0",
|
|
38
|
+
"@bamboocss/core": "^1.28.0",
|
|
39
|
+
"@bamboocss/extractor": "1.28.0",
|
|
40
|
+
"@bamboocss/logger": "1.28.0",
|
|
41
|
+
"@bamboocss/shared": "1.28.0",
|
|
42
|
+
"@bamboocss/types": "1.28.0"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@bamboocss/generator": "1.
|
|
46
|
-
"@bamboocss/plugin-svelte": "1.
|
|
47
|
-
"@bamboocss/plugin-vue": "1.
|
|
45
|
+
"@bamboocss/generator": "1.28.0",
|
|
46
|
+
"@bamboocss/plugin-svelte": "1.28.0",
|
|
47
|
+
"@bamboocss/plugin-vue": "1.28.0"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsdown src/index.ts --format=esm,cjs --dts",
|