@bamboocss/parser 1.12.3 → 1.13.2
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/LICENSE.md +1 -0
- package/dist/index.cjs +108 -2
- package/dist/index.d.cts +114 -0
- package/dist/index.d.mts +114 -0
- package/dist/index.mjs +109 -3
- package/package.json +12 -12
package/LICENSE.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
3
|
Copyright (c) 2023 Segun Adebayo
|
|
4
|
+
Copyright (c) 2026 Gajus Kuizinas
|
|
4
5
|
|
|
5
6
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
|
6
7
|
documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
package/dist/index.cjs
CHANGED
|
@@ -690,7 +690,7 @@ function createParser(context) {
|
|
|
690
690
|
name = name.replace(".raw", "");
|
|
691
691
|
return { environment: Object.assign({}, defaultEnv, { extra: { [name]: { raw: (v) => v } } }) };
|
|
692
692
|
},
|
|
693
|
-
flags: { skipTraverseFiles:
|
|
693
|
+
flags: { skipTraverseFiles: false }
|
|
694
694
|
}).forEach((result, alias) => {
|
|
695
695
|
const name = file.getName(file.normalizeFnName(alias));
|
|
696
696
|
_bamboocss_logger.logger.debug(`ast:${name}`, name !== alias ? {
|
|
@@ -894,11 +894,110 @@ var Project = class {
|
|
|
894
894
|
get files() {
|
|
895
895
|
return this.options.getFiles();
|
|
896
896
|
}
|
|
897
|
+
/**
|
|
898
|
+
* Reverse dependency graph: imported file -> files importing it, both keyed on
|
|
899
|
+
* the source file's own normalized path so lookups match regardless of whether
|
|
900
|
+
* the caller passed a relative, aliased or platform-specific path.
|
|
901
|
+
*
|
|
902
|
+
* Populated while parsing. Cross-file extraction folds imported values into the
|
|
903
|
+
* importer's output, so editing a shared style file has to re-parse everyone who
|
|
904
|
+
* imports it — re-parsing only the changed file leaves consumers stale.
|
|
905
|
+
*/
|
|
906
|
+
dependents = /* @__PURE__ */ new Map();
|
|
907
|
+
/** Forward edges, so a re-parse can retract exactly the previous ones. */
|
|
908
|
+
dependencies = /* @__PURE__ */ new Map();
|
|
909
|
+
/**
|
|
910
|
+
* Path as a caller spells it -> the source file's own path.
|
|
911
|
+
*
|
|
912
|
+
* The graph is keyed on the latter, but callers pass whatever the watcher gave
|
|
913
|
+
* them. Resolving through the project covers that while the file is loaded;
|
|
914
|
+
* this keeps it resolvable afterwards too, so asking which files imported a
|
|
915
|
+
* *deleted* file still works and the unlink path does not depend on querying
|
|
916
|
+
* before removal.
|
|
917
|
+
*/
|
|
918
|
+
canonicalPaths = /* @__PURE__ */ new Map();
|
|
919
|
+
/**
|
|
920
|
+
* Files holding at least one import whose specifier resolved to nothing.
|
|
921
|
+
*
|
|
922
|
+
* A broken or not-yet-created import produces no edge, so when the file it wants
|
|
923
|
+
* finally appears there is nothing in the graph connecting them. These importers
|
|
924
|
+
* are the only candidates for that, and the set is normally empty.
|
|
925
|
+
*/
|
|
926
|
+
unresolvedImporters = /* @__PURE__ */ new Set();
|
|
927
|
+
/** Files whose imports did not all resolve when they were last parsed. */
|
|
928
|
+
getUnresolvedImporters = () => [...this.unresolvedImporters];
|
|
897
929
|
getSourceFile = (filePath) => {
|
|
898
930
|
return this.project.getSourceFile(filePath);
|
|
899
931
|
};
|
|
932
|
+
/** ts-morph reports forward slashes; normalize callers' paths to match on Windows. */
|
|
933
|
+
normalizePath = (filePath) => filePath.replaceAll("\\", "/");
|
|
934
|
+
/**
|
|
935
|
+
* Resolves a module specifier to a file already in the project.
|
|
936
|
+
*
|
|
937
|
+
* Deliberately not `decl.getModuleSpecifierSourceFile()`: that goes through the
|
|
938
|
+
* symbol table, which forces `initializeTypeChecker` on first use and costs
|
|
939
|
+
* hundreds of ms on a cold build. `ts.resolveModuleName` is purely a filesystem
|
|
940
|
+
* lookup, and a shared cache keeps repeat specifiers off the disk.
|
|
941
|
+
*
|
|
942
|
+
* Looks the result up rather than adding it, so resolving `react` cannot pull a
|
|
943
|
+
* `.d.ts` into the project. The graph only tracks files bamboo already scans.
|
|
944
|
+
*/
|
|
945
|
+
moduleResolutionCache;
|
|
946
|
+
resolveImport = (decl) => {
|
|
947
|
+
const moduleName = decl.getModuleSpecifierValue();
|
|
948
|
+
if (!moduleName) return;
|
|
949
|
+
const compilerOptions = this.project.getCompilerOptions();
|
|
950
|
+
this.moduleResolutionCache ??= ts_morph.ts.createModuleResolutionCache(this.project.getFileSystem().getCurrentDirectory(), (f) => f, compilerOptions);
|
|
951
|
+
const name = ts_morph.ts.resolveModuleName(moduleName, decl.getSourceFile().getFilePath(), compilerOptions, this.project.getModuleResolutionHost(), this.moduleResolutionCache).resolvedModule?.resolvedFileName;
|
|
952
|
+
return name ? this.project.getSourceFile(name) : void 0;
|
|
953
|
+
};
|
|
954
|
+
trackDependencies = (filePath, sourceFile) => {
|
|
955
|
+
const importer = this.normalizePath(sourceFile.getFilePath());
|
|
956
|
+
this.canonicalPaths.set(this.normalizePath(filePath), importer);
|
|
957
|
+
for (const previous of this.dependencies.get(importer) ?? []) this.dependents.get(previous)?.delete(importer);
|
|
958
|
+
const current = /* @__PURE__ */ new Set();
|
|
959
|
+
const declarations = [...sourceFile.getImportDeclarations(), ...sourceFile.getExportDeclarations()];
|
|
960
|
+
let unresolved = false;
|
|
961
|
+
for (const decl of declarations) {
|
|
962
|
+
const imported = this.resolveImport(decl);
|
|
963
|
+
if (!imported) {
|
|
964
|
+
if (decl.getModuleSpecifierValue()?.startsWith(".")) unresolved = true;
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const importedPath = this.normalizePath(imported.getFilePath());
|
|
968
|
+
if (importedPath === importer) continue;
|
|
969
|
+
const importers = this.dependents.get(importedPath) ?? /* @__PURE__ */ new Set();
|
|
970
|
+
importers.add(importer);
|
|
971
|
+
this.dependents.set(importedPath, importers);
|
|
972
|
+
current.add(importedPath);
|
|
973
|
+
}
|
|
974
|
+
this.dependencies.set(importer, current);
|
|
975
|
+
if (unresolved) this.unresolvedImporters.add(importer);
|
|
976
|
+
else this.unresolvedImporters.delete(importer);
|
|
977
|
+
};
|
|
978
|
+
/**
|
|
979
|
+
* Every file that transitively imports `filePath`, so a watcher can re-parse the
|
|
980
|
+
* consumers of an edited file. Excludes `filePath` itself.
|
|
981
|
+
*/
|
|
982
|
+
getDependents = (filePath) => {
|
|
983
|
+
const given = this.normalizePath(filePath);
|
|
984
|
+
const resolved = this.project.getSourceFile(filePath)?.getFilePath();
|
|
985
|
+
const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
|
|
986
|
+
const seen = /* @__PURE__ */ new Set();
|
|
987
|
+
const queue = [start];
|
|
988
|
+
while (queue.length) {
|
|
989
|
+
const current = queue.shift();
|
|
990
|
+
for (const importer of this.dependents.get(current) ?? []) {
|
|
991
|
+
if (importer === start || seen.has(importer)) continue;
|
|
992
|
+
seen.add(importer);
|
|
993
|
+
queue.push(importer);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return [...seen];
|
|
997
|
+
};
|
|
900
998
|
createSourceFile = (filePath) => {
|
|
901
999
|
const { readFile } = this.options;
|
|
1000
|
+
(0, _bamboocss_extractor.clearBoxNodeCache)();
|
|
902
1001
|
return this.project.createSourceFile(filePath, readFile(filePath), {
|
|
903
1002
|
overwrite: true,
|
|
904
1003
|
scriptKind: ts_morph.ScriptKind.TSX
|
|
@@ -909,6 +1008,7 @@ var Project = class {
|
|
|
909
1008
|
for (const file of files) this.createSourceFile(file);
|
|
910
1009
|
};
|
|
911
1010
|
addSourceFile = (filePath, content) => {
|
|
1011
|
+
(0, _bamboocss_extractor.clearBoxNodeCache)();
|
|
912
1012
|
return this.project.createSourceFile(filePath, content, {
|
|
913
1013
|
overwrite: true,
|
|
914
1014
|
scriptKind: ts_morph.ScriptKind.TSX
|
|
@@ -916,14 +1016,19 @@ var Project = class {
|
|
|
916
1016
|
};
|
|
917
1017
|
removeSourceFile = (filePath) => {
|
|
918
1018
|
const sourceFile = this.project.getSourceFile(filePath);
|
|
919
|
-
if (sourceFile)
|
|
1019
|
+
if (sourceFile) {
|
|
1020
|
+
(0, _bamboocss_extractor.clearBoxNodeCache)();
|
|
1021
|
+
return this.project.removeSourceFile(sourceFile);
|
|
1022
|
+
}
|
|
920
1023
|
return false;
|
|
921
1024
|
};
|
|
922
1025
|
reloadSourceFile = (filePath) => {
|
|
1026
|
+
(0, _bamboocss_extractor.clearBoxNodeCache)();
|
|
923
1027
|
return this.getSourceFile(filePath)?.refreshFromFileSystemSync();
|
|
924
1028
|
};
|
|
925
1029
|
reloadSourceFiles = () => {
|
|
926
1030
|
const files = this.getFiles();
|
|
1031
|
+
(0, _bamboocss_extractor.clearBoxNodeCache)();
|
|
927
1032
|
for (const file of files) this.getSourceFile(file)?.refreshFromFileSystemSync() ?? this.project.addSourceFileAtPath(file);
|
|
928
1033
|
};
|
|
929
1034
|
get readFile() {
|
|
@@ -943,6 +1048,7 @@ var Project = class {
|
|
|
943
1048
|
if (filePath.endsWith(".json")) return this.parseJson(filePath);
|
|
944
1049
|
const sourceFile = this.project.getSourceFile(filePath);
|
|
945
1050
|
if (!sourceFile) return;
|
|
1051
|
+
this.trackDependencies(filePath, sourceFile);
|
|
946
1052
|
const original = sourceFile.getText();
|
|
947
1053
|
const options = {};
|
|
948
1054
|
const transformed = hooks["parser:before"]?.({
|
package/dist/index.d.cts
CHANGED
|
@@ -31,6 +31,67 @@ declare class Generator extends Context {
|
|
|
31
31
|
appendLayerParams: (sheet: Stylesheet) => void;
|
|
32
32
|
appendBaselineCss: (sheet: Stylesheet) => void;
|
|
33
33
|
appendParserCss: (sheet: Stylesheet) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Drop token css variables nothing can reach. Call this only once the sheet holds the
|
|
36
|
+
* whole stylesheet — a baseline-only sheet has no utilities to reference anything, so
|
|
37
|
+
* every token would look unused.
|
|
38
|
+
*
|
|
39
|
+
* `keep` carries references this cannot see for itself; see `collectTokenReferences`.
|
|
40
|
+
*/
|
|
41
|
+
pruneTokens: (sheet: Stylesheet, keep?: Set<string>) => {
|
|
42
|
+
removed: number;
|
|
43
|
+
kept: number;
|
|
44
|
+
} | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Drop `@keyframes` nothing can reach. Same completeness requirement as
|
|
47
|
+
* `pruneTokens`: the sheet has to hold the whole stylesheet, or every keyframe looks
|
|
48
|
+
* unused for want of a utility to reference it.
|
|
49
|
+
*
|
|
50
|
+
* `keep` carries names this cannot see for itself; see `collectKeyframeReferences`.
|
|
51
|
+
*/
|
|
52
|
+
pruneKeyframes: (sheet: Stylesheet, keep?: Set<string>) => {
|
|
53
|
+
removed: number;
|
|
54
|
+
kept: number;
|
|
55
|
+
} | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Keyframes the themes name.
|
|
58
|
+
*
|
|
59
|
+
* A theme is emitted as its own artifact and injected at runtime, so its css is not in
|
|
60
|
+
* the sheet being pruned. A theme that points an animation token at a different
|
|
61
|
+
* keyframe than the base does — `--animations-enter: fade-in` in the base and
|
|
62
|
+
* `slide-up` under `dark` — would otherwise have that keyframe removed, because
|
|
63
|
+
* nothing in the pruned sheet ever names it.
|
|
64
|
+
*/
|
|
65
|
+
private getThemeKeyframeNames;
|
|
66
|
+
/**
|
|
67
|
+
* Every custom property the token system declares. Used as the allow-list of what may
|
|
68
|
+
* be removed, so custom properties from `globalCss` are never touched.
|
|
69
|
+
*/
|
|
70
|
+
private getTokenVarNames;
|
|
71
|
+
/**
|
|
72
|
+
* Everything the themes refer to.
|
|
73
|
+
*
|
|
74
|
+
* A theme is emitted as its own artifact and injected at runtime, so its css is not in
|
|
75
|
+
* the sheet being pruned and nothing there points at what it needs. A theme that maps a
|
|
76
|
+
* token onto a base colour would otherwise be left referring to a declaration that has
|
|
77
|
+
* been removed.
|
|
78
|
+
*/
|
|
79
|
+
private getThemeTokenVars;
|
|
80
|
+
/**
|
|
81
|
+
* Tokens whose javascript value is a `var()` reference rather than a literal.
|
|
82
|
+
* `token('colors.text')` hands those to the caller as a reference, so the declaration
|
|
83
|
+
* has to survive whether or not the generated css mentions it. Ordinary tokens resolve
|
|
84
|
+
* to a literal in javascript and need no such exemption.
|
|
85
|
+
*
|
|
86
|
+
* The two cases mirror `generateTokenJs`, which is what decides the value javascript
|
|
87
|
+
* actually receives:
|
|
88
|
+
*
|
|
89
|
+
* - A virtual token, or one carrying a condition, is handed its own `varRef`.
|
|
90
|
+
* - A negative token is handed `calc(var(--x) * -1)`, so it is a reference too — but to
|
|
91
|
+
* the *positive* token's declaration. Its own var is never declared, so the name has
|
|
92
|
+
* to come out of the value.
|
|
93
|
+
*/
|
|
94
|
+
private getAlwaysKeptTokenVars;
|
|
34
95
|
getParserCss: (decoder: StyleDecoder) => string;
|
|
35
96
|
getCss: (stylesheet?: Stylesheet) => string;
|
|
36
97
|
/**
|
|
@@ -119,7 +180,60 @@ declare class Project {
|
|
|
119
180
|
get parserOptions(): ParserOptions;
|
|
120
181
|
constructor(options: ProjectOptions);
|
|
121
182
|
get files(): string[];
|
|
183
|
+
/**
|
|
184
|
+
* Reverse dependency graph: imported file -> files importing it, both keyed on
|
|
185
|
+
* the source file's own normalized path so lookups match regardless of whether
|
|
186
|
+
* the caller passed a relative, aliased or platform-specific path.
|
|
187
|
+
*
|
|
188
|
+
* Populated while parsing. Cross-file extraction folds imported values into the
|
|
189
|
+
* importer's output, so editing a shared style file has to re-parse everyone who
|
|
190
|
+
* imports it — re-parsing only the changed file leaves consumers stale.
|
|
191
|
+
*/
|
|
192
|
+
private dependents;
|
|
193
|
+
/** Forward edges, so a re-parse can retract exactly the previous ones. */
|
|
194
|
+
private dependencies;
|
|
195
|
+
/**
|
|
196
|
+
* Path as a caller spells it -> the source file's own path.
|
|
197
|
+
*
|
|
198
|
+
* The graph is keyed on the latter, but callers pass whatever the watcher gave
|
|
199
|
+
* them. Resolving through the project covers that while the file is loaded;
|
|
200
|
+
* this keeps it resolvable afterwards too, so asking which files imported a
|
|
201
|
+
* *deleted* file still works and the unlink path does not depend on querying
|
|
202
|
+
* before removal.
|
|
203
|
+
*/
|
|
204
|
+
private canonicalPaths;
|
|
205
|
+
/**
|
|
206
|
+
* Files holding at least one import whose specifier resolved to nothing.
|
|
207
|
+
*
|
|
208
|
+
* A broken or not-yet-created import produces no edge, so when the file it wants
|
|
209
|
+
* finally appears there is nothing in the graph connecting them. These importers
|
|
210
|
+
* are the only candidates for that, and the set is normally empty.
|
|
211
|
+
*/
|
|
212
|
+
private unresolvedImporters;
|
|
213
|
+
/** Files whose imports did not all resolve when they were last parsed. */
|
|
214
|
+
getUnresolvedImporters: () => string[];
|
|
122
215
|
getSourceFile: (filePath: string) => SourceFile | undefined;
|
|
216
|
+
/** ts-morph reports forward slashes; normalize callers' paths to match on Windows. */
|
|
217
|
+
private normalizePath;
|
|
218
|
+
/**
|
|
219
|
+
* Resolves a module specifier to a file already in the project.
|
|
220
|
+
*
|
|
221
|
+
* Deliberately not `decl.getModuleSpecifierSourceFile()`: that goes through the
|
|
222
|
+
* symbol table, which forces `initializeTypeChecker` on first use and costs
|
|
223
|
+
* hundreds of ms on a cold build. `ts.resolveModuleName` is purely a filesystem
|
|
224
|
+
* lookup, and a shared cache keeps repeat specifiers off the disk.
|
|
225
|
+
*
|
|
226
|
+
* Looks the result up rather than adding it, so resolving `react` cannot pull a
|
|
227
|
+
* `.d.ts` into the project. The graph only tracks files bamboo already scans.
|
|
228
|
+
*/
|
|
229
|
+
private moduleResolutionCache;
|
|
230
|
+
private resolveImport;
|
|
231
|
+
private trackDependencies;
|
|
232
|
+
/**
|
|
233
|
+
* Every file that transitively imports `filePath`, so a watcher can re-parse the
|
|
234
|
+
* consumers of an edited file. Excludes `filePath` itself.
|
|
235
|
+
*/
|
|
236
|
+
getDependents: (filePath: string) => string[];
|
|
123
237
|
createSourceFile: (filePath: string) => SourceFile;
|
|
124
238
|
createSourceFiles: () => void;
|
|
125
239
|
addSourceFile: (filePath: string, content: string) => SourceFile;
|
package/dist/index.d.mts
CHANGED
|
@@ -31,6 +31,67 @@ declare class Generator extends Context {
|
|
|
31
31
|
appendLayerParams: (sheet: Stylesheet) => void;
|
|
32
32
|
appendBaselineCss: (sheet: Stylesheet) => void;
|
|
33
33
|
appendParserCss: (sheet: Stylesheet) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Drop token css variables nothing can reach. Call this only once the sheet holds the
|
|
36
|
+
* whole stylesheet — a baseline-only sheet has no utilities to reference anything, so
|
|
37
|
+
* every token would look unused.
|
|
38
|
+
*
|
|
39
|
+
* `keep` carries references this cannot see for itself; see `collectTokenReferences`.
|
|
40
|
+
*/
|
|
41
|
+
pruneTokens: (sheet: Stylesheet, keep?: Set<string>) => {
|
|
42
|
+
removed: number;
|
|
43
|
+
kept: number;
|
|
44
|
+
} | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Drop `@keyframes` nothing can reach. Same completeness requirement as
|
|
47
|
+
* `pruneTokens`: the sheet has to hold the whole stylesheet, or every keyframe looks
|
|
48
|
+
* unused for want of a utility to reference it.
|
|
49
|
+
*
|
|
50
|
+
* `keep` carries names this cannot see for itself; see `collectKeyframeReferences`.
|
|
51
|
+
*/
|
|
52
|
+
pruneKeyframes: (sheet: Stylesheet, keep?: Set<string>) => {
|
|
53
|
+
removed: number;
|
|
54
|
+
kept: number;
|
|
55
|
+
} | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Keyframes the themes name.
|
|
58
|
+
*
|
|
59
|
+
* A theme is emitted as its own artifact and injected at runtime, so its css is not in
|
|
60
|
+
* the sheet being pruned. A theme that points an animation token at a different
|
|
61
|
+
* keyframe than the base does — `--animations-enter: fade-in` in the base and
|
|
62
|
+
* `slide-up` under `dark` — would otherwise have that keyframe removed, because
|
|
63
|
+
* nothing in the pruned sheet ever names it.
|
|
64
|
+
*/
|
|
65
|
+
private getThemeKeyframeNames;
|
|
66
|
+
/**
|
|
67
|
+
* Every custom property the token system declares. Used as the allow-list of what may
|
|
68
|
+
* be removed, so custom properties from `globalCss` are never touched.
|
|
69
|
+
*/
|
|
70
|
+
private getTokenVarNames;
|
|
71
|
+
/**
|
|
72
|
+
* Everything the themes refer to.
|
|
73
|
+
*
|
|
74
|
+
* A theme is emitted as its own artifact and injected at runtime, so its css is not in
|
|
75
|
+
* the sheet being pruned and nothing there points at what it needs. A theme that maps a
|
|
76
|
+
* token onto a base colour would otherwise be left referring to a declaration that has
|
|
77
|
+
* been removed.
|
|
78
|
+
*/
|
|
79
|
+
private getThemeTokenVars;
|
|
80
|
+
/**
|
|
81
|
+
* Tokens whose javascript value is a `var()` reference rather than a literal.
|
|
82
|
+
* `token('colors.text')` hands those to the caller as a reference, so the declaration
|
|
83
|
+
* has to survive whether or not the generated css mentions it. Ordinary tokens resolve
|
|
84
|
+
* to a literal in javascript and need no such exemption.
|
|
85
|
+
*
|
|
86
|
+
* The two cases mirror `generateTokenJs`, which is what decides the value javascript
|
|
87
|
+
* actually receives:
|
|
88
|
+
*
|
|
89
|
+
* - A virtual token, or one carrying a condition, is handed its own `varRef`.
|
|
90
|
+
* - A negative token is handed `calc(var(--x) * -1)`, so it is a reference too — but to
|
|
91
|
+
* the *positive* token's declaration. Its own var is never declared, so the name has
|
|
92
|
+
* to come out of the value.
|
|
93
|
+
*/
|
|
94
|
+
private getAlwaysKeptTokenVars;
|
|
34
95
|
getParserCss: (decoder: StyleDecoder) => string;
|
|
35
96
|
getCss: (stylesheet?: Stylesheet) => string;
|
|
36
97
|
/**
|
|
@@ -119,7 +180,60 @@ declare class Project {
|
|
|
119
180
|
get parserOptions(): ParserOptions;
|
|
120
181
|
constructor(options: ProjectOptions);
|
|
121
182
|
get files(): string[];
|
|
183
|
+
/**
|
|
184
|
+
* Reverse dependency graph: imported file -> files importing it, both keyed on
|
|
185
|
+
* the source file's own normalized path so lookups match regardless of whether
|
|
186
|
+
* the caller passed a relative, aliased or platform-specific path.
|
|
187
|
+
*
|
|
188
|
+
* Populated while parsing. Cross-file extraction folds imported values into the
|
|
189
|
+
* importer's output, so editing a shared style file has to re-parse everyone who
|
|
190
|
+
* imports it — re-parsing only the changed file leaves consumers stale.
|
|
191
|
+
*/
|
|
192
|
+
private dependents;
|
|
193
|
+
/** Forward edges, so a re-parse can retract exactly the previous ones. */
|
|
194
|
+
private dependencies;
|
|
195
|
+
/**
|
|
196
|
+
* Path as a caller spells it -> the source file's own path.
|
|
197
|
+
*
|
|
198
|
+
* The graph is keyed on the latter, but callers pass whatever the watcher gave
|
|
199
|
+
* them. Resolving through the project covers that while the file is loaded;
|
|
200
|
+
* this keeps it resolvable afterwards too, so asking which files imported a
|
|
201
|
+
* *deleted* file still works and the unlink path does not depend on querying
|
|
202
|
+
* before removal.
|
|
203
|
+
*/
|
|
204
|
+
private canonicalPaths;
|
|
205
|
+
/**
|
|
206
|
+
* Files holding at least one import whose specifier resolved to nothing.
|
|
207
|
+
*
|
|
208
|
+
* A broken or not-yet-created import produces no edge, so when the file it wants
|
|
209
|
+
* finally appears there is nothing in the graph connecting them. These importers
|
|
210
|
+
* are the only candidates for that, and the set is normally empty.
|
|
211
|
+
*/
|
|
212
|
+
private unresolvedImporters;
|
|
213
|
+
/** Files whose imports did not all resolve when they were last parsed. */
|
|
214
|
+
getUnresolvedImporters: () => string[];
|
|
122
215
|
getSourceFile: (filePath: string) => SourceFile | undefined;
|
|
216
|
+
/** ts-morph reports forward slashes; normalize callers' paths to match on Windows. */
|
|
217
|
+
private normalizePath;
|
|
218
|
+
/**
|
|
219
|
+
* Resolves a module specifier to a file already in the project.
|
|
220
|
+
*
|
|
221
|
+
* Deliberately not `decl.getModuleSpecifierSourceFile()`: that goes through the
|
|
222
|
+
* symbol table, which forces `initializeTypeChecker` on first use and costs
|
|
223
|
+
* hundreds of ms on a cold build. `ts.resolveModuleName` is purely a filesystem
|
|
224
|
+
* lookup, and a shared cache keeps repeat specifiers off the disk.
|
|
225
|
+
*
|
|
226
|
+
* Looks the result up rather than adding it, so resolving `react` cannot pull a
|
|
227
|
+
* `.d.ts` into the project. The graph only tracks files bamboo already scans.
|
|
228
|
+
*/
|
|
229
|
+
private moduleResolutionCache;
|
|
230
|
+
private resolveImport;
|
|
231
|
+
private trackDependencies;
|
|
232
|
+
/**
|
|
233
|
+
* Every file that transitively imports `filePath`, so a watcher can re-parse the
|
|
234
|
+
* consumers of an edited file. Excludes `filePath` itself.
|
|
235
|
+
*/
|
|
236
|
+
getDependents: (filePath: string) => string[];
|
|
123
237
|
createSourceFile: (filePath: string) => SourceFile;
|
|
124
238
|
createSourceFiles: () => void;
|
|
125
239
|
addSourceFile: (filePath: string, content: string) => SourceFile;
|
package/dist/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Node, Project as Project$1, ScriptKind, ts } from "ts-morph";
|
|
2
|
-
import { box, extract, unbox } from "@bamboocss/extractor";
|
|
2
|
+
import { box, clearBoxNodeCache, extract, unbox } from "@bamboocss/extractor";
|
|
3
3
|
import { BambooError, astish, compact, getOrCreateSet, patternFns } from "@bamboocss/shared";
|
|
4
4
|
import { logger } from "@bamboocss/logger";
|
|
5
5
|
import { match } from "ts-pattern";
|
|
@@ -689,7 +689,7 @@ function createParser(context) {
|
|
|
689
689
|
name = name.replace(".raw", "");
|
|
690
690
|
return { environment: Object.assign({}, defaultEnv, { extra: { [name]: { raw: (v) => v } } }) };
|
|
691
691
|
},
|
|
692
|
-
flags: { skipTraverseFiles:
|
|
692
|
+
flags: { skipTraverseFiles: false }
|
|
693
693
|
}).forEach((result, alias) => {
|
|
694
694
|
const name = file.getName(file.normalizeFnName(alias));
|
|
695
695
|
logger.debug(`ast:${name}`, name !== alias ? {
|
|
@@ -893,11 +893,110 @@ var Project = class {
|
|
|
893
893
|
get files() {
|
|
894
894
|
return this.options.getFiles();
|
|
895
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* Reverse dependency graph: imported file -> files importing it, both keyed on
|
|
898
|
+
* the source file's own normalized path so lookups match regardless of whether
|
|
899
|
+
* the caller passed a relative, aliased or platform-specific path.
|
|
900
|
+
*
|
|
901
|
+
* Populated while parsing. Cross-file extraction folds imported values into the
|
|
902
|
+
* importer's output, so editing a shared style file has to re-parse everyone who
|
|
903
|
+
* imports it — re-parsing only the changed file leaves consumers stale.
|
|
904
|
+
*/
|
|
905
|
+
dependents = /* @__PURE__ */ new Map();
|
|
906
|
+
/** Forward edges, so a re-parse can retract exactly the previous ones. */
|
|
907
|
+
dependencies = /* @__PURE__ */ new Map();
|
|
908
|
+
/**
|
|
909
|
+
* Path as a caller spells it -> the source file's own path.
|
|
910
|
+
*
|
|
911
|
+
* The graph is keyed on the latter, but callers pass whatever the watcher gave
|
|
912
|
+
* them. Resolving through the project covers that while the file is loaded;
|
|
913
|
+
* this keeps it resolvable afterwards too, so asking which files imported a
|
|
914
|
+
* *deleted* file still works and the unlink path does not depend on querying
|
|
915
|
+
* before removal.
|
|
916
|
+
*/
|
|
917
|
+
canonicalPaths = /* @__PURE__ */ new Map();
|
|
918
|
+
/**
|
|
919
|
+
* Files holding at least one import whose specifier resolved to nothing.
|
|
920
|
+
*
|
|
921
|
+
* A broken or not-yet-created import produces no edge, so when the file it wants
|
|
922
|
+
* finally appears there is nothing in the graph connecting them. These importers
|
|
923
|
+
* are the only candidates for that, and the set is normally empty.
|
|
924
|
+
*/
|
|
925
|
+
unresolvedImporters = /* @__PURE__ */ new Set();
|
|
926
|
+
/** Files whose imports did not all resolve when they were last parsed. */
|
|
927
|
+
getUnresolvedImporters = () => [...this.unresolvedImporters];
|
|
896
928
|
getSourceFile = (filePath) => {
|
|
897
929
|
return this.project.getSourceFile(filePath);
|
|
898
930
|
};
|
|
931
|
+
/** ts-morph reports forward slashes; normalize callers' paths to match on Windows. */
|
|
932
|
+
normalizePath = (filePath) => filePath.replaceAll("\\", "/");
|
|
933
|
+
/**
|
|
934
|
+
* Resolves a module specifier to a file already in the project.
|
|
935
|
+
*
|
|
936
|
+
* Deliberately not `decl.getModuleSpecifierSourceFile()`: that goes through the
|
|
937
|
+
* symbol table, which forces `initializeTypeChecker` on first use and costs
|
|
938
|
+
* hundreds of ms on a cold build. `ts.resolveModuleName` is purely a filesystem
|
|
939
|
+
* lookup, and a shared cache keeps repeat specifiers off the disk.
|
|
940
|
+
*
|
|
941
|
+
* Looks the result up rather than adding it, so resolving `react` cannot pull a
|
|
942
|
+
* `.d.ts` into the project. The graph only tracks files bamboo already scans.
|
|
943
|
+
*/
|
|
944
|
+
moduleResolutionCache;
|
|
945
|
+
resolveImport = (decl) => {
|
|
946
|
+
const moduleName = decl.getModuleSpecifierValue();
|
|
947
|
+
if (!moduleName) return;
|
|
948
|
+
const compilerOptions = this.project.getCompilerOptions();
|
|
949
|
+
this.moduleResolutionCache ??= ts.createModuleResolutionCache(this.project.getFileSystem().getCurrentDirectory(), (f) => f, compilerOptions);
|
|
950
|
+
const name = ts.resolveModuleName(moduleName, decl.getSourceFile().getFilePath(), compilerOptions, this.project.getModuleResolutionHost(), this.moduleResolutionCache).resolvedModule?.resolvedFileName;
|
|
951
|
+
return name ? this.project.getSourceFile(name) : void 0;
|
|
952
|
+
};
|
|
953
|
+
trackDependencies = (filePath, sourceFile) => {
|
|
954
|
+
const importer = this.normalizePath(sourceFile.getFilePath());
|
|
955
|
+
this.canonicalPaths.set(this.normalizePath(filePath), importer);
|
|
956
|
+
for (const previous of this.dependencies.get(importer) ?? []) this.dependents.get(previous)?.delete(importer);
|
|
957
|
+
const current = /* @__PURE__ */ new Set();
|
|
958
|
+
const declarations = [...sourceFile.getImportDeclarations(), ...sourceFile.getExportDeclarations()];
|
|
959
|
+
let unresolved = false;
|
|
960
|
+
for (const decl of declarations) {
|
|
961
|
+
const imported = this.resolveImport(decl);
|
|
962
|
+
if (!imported) {
|
|
963
|
+
if (decl.getModuleSpecifierValue()?.startsWith(".")) unresolved = true;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
const importedPath = this.normalizePath(imported.getFilePath());
|
|
967
|
+
if (importedPath === importer) continue;
|
|
968
|
+
const importers = this.dependents.get(importedPath) ?? /* @__PURE__ */ new Set();
|
|
969
|
+
importers.add(importer);
|
|
970
|
+
this.dependents.set(importedPath, importers);
|
|
971
|
+
current.add(importedPath);
|
|
972
|
+
}
|
|
973
|
+
this.dependencies.set(importer, current);
|
|
974
|
+
if (unresolved) this.unresolvedImporters.add(importer);
|
|
975
|
+
else this.unresolvedImporters.delete(importer);
|
|
976
|
+
};
|
|
977
|
+
/**
|
|
978
|
+
* Every file that transitively imports `filePath`, so a watcher can re-parse the
|
|
979
|
+
* consumers of an edited file. Excludes `filePath` itself.
|
|
980
|
+
*/
|
|
981
|
+
getDependents = (filePath) => {
|
|
982
|
+
const given = this.normalizePath(filePath);
|
|
983
|
+
const resolved = this.project.getSourceFile(filePath)?.getFilePath();
|
|
984
|
+
const start = resolved ? this.normalizePath(resolved) : this.canonicalPaths.get(given) ?? given;
|
|
985
|
+
const seen = /* @__PURE__ */ new Set();
|
|
986
|
+
const queue = [start];
|
|
987
|
+
while (queue.length) {
|
|
988
|
+
const current = queue.shift();
|
|
989
|
+
for (const importer of this.dependents.get(current) ?? []) {
|
|
990
|
+
if (importer === start || seen.has(importer)) continue;
|
|
991
|
+
seen.add(importer);
|
|
992
|
+
queue.push(importer);
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
return [...seen];
|
|
996
|
+
};
|
|
899
997
|
createSourceFile = (filePath) => {
|
|
900
998
|
const { readFile } = this.options;
|
|
999
|
+
clearBoxNodeCache();
|
|
901
1000
|
return this.project.createSourceFile(filePath, readFile(filePath), {
|
|
902
1001
|
overwrite: true,
|
|
903
1002
|
scriptKind: ScriptKind.TSX
|
|
@@ -908,6 +1007,7 @@ var Project = class {
|
|
|
908
1007
|
for (const file of files) this.createSourceFile(file);
|
|
909
1008
|
};
|
|
910
1009
|
addSourceFile = (filePath, content) => {
|
|
1010
|
+
clearBoxNodeCache();
|
|
911
1011
|
return this.project.createSourceFile(filePath, content, {
|
|
912
1012
|
overwrite: true,
|
|
913
1013
|
scriptKind: ScriptKind.TSX
|
|
@@ -915,14 +1015,19 @@ var Project = class {
|
|
|
915
1015
|
};
|
|
916
1016
|
removeSourceFile = (filePath) => {
|
|
917
1017
|
const sourceFile = this.project.getSourceFile(filePath);
|
|
918
|
-
if (sourceFile)
|
|
1018
|
+
if (sourceFile) {
|
|
1019
|
+
clearBoxNodeCache();
|
|
1020
|
+
return this.project.removeSourceFile(sourceFile);
|
|
1021
|
+
}
|
|
919
1022
|
return false;
|
|
920
1023
|
};
|
|
921
1024
|
reloadSourceFile = (filePath) => {
|
|
1025
|
+
clearBoxNodeCache();
|
|
922
1026
|
return this.getSourceFile(filePath)?.refreshFromFileSystemSync();
|
|
923
1027
|
};
|
|
924
1028
|
reloadSourceFiles = () => {
|
|
925
1029
|
const files = this.getFiles();
|
|
1030
|
+
clearBoxNodeCache();
|
|
926
1031
|
for (const file of files) this.getSourceFile(file)?.refreshFromFileSystemSync() ?? this.project.addSourceFileAtPath(file);
|
|
927
1032
|
};
|
|
928
1033
|
get readFile() {
|
|
@@ -942,6 +1047,7 @@ var Project = class {
|
|
|
942
1047
|
if (filePath.endsWith(".json")) return this.parseJson(filePath);
|
|
943
1048
|
const sourceFile = this.project.getSourceFile(filePath);
|
|
944
1049
|
if (!sourceFile) return;
|
|
1050
|
+
this.trackDependencies(filePath, sourceFile);
|
|
945
1051
|
const original = sourceFile.getText();
|
|
946
1052
|
const options = {};
|
|
947
1053
|
const transformed = hooks["parser:before"]?.({
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bamboocss/parser",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.2",
|
|
4
4
|
"description": "The static parser for bamboo css",
|
|
5
|
-
"homepage": "https://
|
|
5
|
+
"homepage": "https://bamboocss.com",
|
|
6
6
|
"license": "MIT",
|
|
7
|
-
"author": "
|
|
7
|
+
"author": "Gajus Kuizinas <gajus@gajus.com>",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "git+https://github.com/bamboocss/bamboo.git",
|
|
@@ -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/
|
|
39
|
-
"@bamboocss/
|
|
40
|
-
"@bamboocss/
|
|
41
|
-
"@bamboocss/
|
|
42
|
-
"@bamboocss/
|
|
37
|
+
"@bamboocss/config": "^1.13.2",
|
|
38
|
+
"@bamboocss/core": "^1.13.2",
|
|
39
|
+
"@bamboocss/extractor": "1.13.2",
|
|
40
|
+
"@bamboocss/logger": "1.13.2",
|
|
41
|
+
"@bamboocss/shared": "1.13.2",
|
|
42
|
+
"@bamboocss/types": "1.13.2"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@bamboocss/
|
|
46
|
-
"@bamboocss/plugin-
|
|
47
|
-
"@bamboocss/
|
|
45
|
+
"@bamboocss/generator": "1.13.2",
|
|
46
|
+
"@bamboocss/plugin-svelte": "1.13.2",
|
|
47
|
+
"@bamboocss/plugin-vue": "1.13.2"
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"build": "tsdown src/index.ts --format=esm,cjs --dts",
|