@bamboocss/parser 1.12.3 → 1.13.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.
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: true }
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) return this.project.removeSourceFile(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: true }
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) return this.project.removeSourceFile(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.12.3",
3
+ "version": "1.13.1",
4
4
  "description": "The static parser for bamboo css",
5
- "homepage": "https://bamboo-css.com",
5
+ "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
7
- "author": "Segun Adebayo <joseshegs@gmail.com>",
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.12.3",
38
- "@bamboocss/extractor": "1.12.3",
39
- "@bamboocss/core": "^1.12.3",
40
- "@bamboocss/shared": "1.12.3",
41
- "@bamboocss/types": "1.12.3",
42
- "@bamboocss/logger": "1.12.3"
37
+ "@bamboocss/config": "^1.13.1",
38
+ "@bamboocss/core": "^1.13.1",
39
+ "@bamboocss/extractor": "1.13.1",
40
+ "@bamboocss/logger": "1.13.1",
41
+ "@bamboocss/shared": "1.13.1",
42
+ "@bamboocss/types": "1.13.1"
43
43
  },
44
44
  "devDependencies": {
45
- "@bamboocss/plugin-svelte": "1.12.3",
46
- "@bamboocss/plugin-vue": "1.12.3",
47
- "@bamboocss/generator": "1.12.3"
45
+ "@bamboocss/generator": "1.13.1",
46
+ "@bamboocss/plugin-svelte": "1.13.1",
47
+ "@bamboocss/plugin-vue": "1.13.1"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "tsdown src/index.ts --format=esm,cjs --dts",