@bamboocss/parser 1.47.0 → 1.48.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 CHANGED
@@ -1691,6 +1691,24 @@ var Project = class {
1691
1691
  sourcePreparations = /* @__PURE__ */ new Map();
1692
1692
  /** Paths explicitly removed through this wrapper, until an add/create observes them again. */
1693
1693
  removedSourcePaths = /* @__PURE__ */ new Set();
1694
+ /**
1695
+ * Sources a bundler transform owns, rather than members of the checkout.
1696
+ *
1697
+ * A compiler which shares this Project has to park bundler-transformed text somewhere, and
1698
+ * it cannot be the file's own path — that is the canonical source every other reader
1699
+ * resolves through. It parses under a sibling path instead, and that parse resolves its
1700
+ * imports like any other, so the ledger gains an importer no watcher will ever report a
1701
+ * change for. Left in, those facts reach the incremental extraction pass: an auxiliary
1702
+ * importer would be selected as a dependent of the file it shadows and ordered against
1703
+ * inventory members that do not import it.
1704
+ *
1705
+ * Excluded from exactly the three queries that decide what a rebuild re-extracts —
1706
+ * the ledger, the dependent walk and the unresolved-importer set — and from nothing else.
1707
+ * Resolution itself is deliberately untouched: an auxiliary parse must still see the same
1708
+ * modules the real one would, and its forward edges are what a bundler registers as watch
1709
+ * files.
1710
+ */
1711
+ auxiliarySources = /* @__PURE__ */ new Set();
1694
1712
  /** File-tree changes invalidate even successful resolutions (extension precedence can move). */
1695
1713
  #fileTreeRevision = 0;
1696
1714
  resolutionWork = {
@@ -1709,6 +1727,7 @@ var Project = class {
1709
1727
  this.resolutionsByImporter = /* @__PURE__ */ new Map();
1710
1728
  this.sourcePreparations = /* @__PURE__ */ new Map();
1711
1729
  this.removedSourcePaths = /* @__PURE__ */ new Set();
1730
+ this.auxiliarySources = /* @__PURE__ */ new Set();
1712
1731
  this.resolutionWork = {
1713
1732
  moduleResolutionsAttempted: 0,
1714
1733
  sourceFilesAdded: 0,
@@ -1718,12 +1737,12 @@ var Project = class {
1718
1737
  /** Files whose imports may resolve differently after a local file appears. */
1719
1738
  getUnresolvedImporters = () => {
1720
1739
  this.#assertNotLoading();
1721
- return [...this.unresolvedImporters].sort();
1740
+ return [...this.unresolvedImporters].filter((importer) => !this.auxiliarySources.has(importer)).sort();
1722
1741
  };
1723
1742
  /** @internal Immutable resolution facts in importer/AST order. */
1724
1743
  getResolutionLedger = () => {
1725
1744
  this.#assertNotLoading();
1726
- return Object.freeze([...this.resolutionsByImporter.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).flatMap(([, entry]) => entry.facts));
1745
+ return Object.freeze([...this.resolutionsByImporter.entries()].filter(([importer]) => !this.auxiliarySources.has(importer)).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).flatMap(([, entry]) => entry.facts));
1727
1746
  };
1728
1747
  /** @internal Every distinct local target represented by the current ledger. */
1729
1748
  getResolvedSourceFiles = () => {
@@ -2052,7 +2071,7 @@ var Project = class {
2052
2071
  * downstream edges the ledger records. The transaction is published only after the AST and
2053
2072
  * its facts agree. A failed or re-entered attempt restores the input AST and can be retried.
2054
2073
  */
2055
- prepareEffectiveSource = (filePath, sourceFile) => {
2074
+ prepareEffectiveSource = (filePath, sourceFile, hookFilePath = filePath) => {
2056
2075
  this.#assertNotLoading();
2057
2076
  const sourcePath = this.normalizePath(sourceFile.getFilePath());
2058
2077
  const currentText = sourceFile.getFullText();
@@ -2087,7 +2106,7 @@ var Project = class {
2087
2106
  const options = {};
2088
2107
  try {
2089
2108
  const custom = this.options.hooks["parser:before"]?.({
2090
- filePath,
2109
+ filePath: hookFilePath,
2091
2110
  content: currentText,
2092
2111
  configure(next) {
2093
2112
  const { matchTag, matchTagMode, matchTagProp } = next;
@@ -2097,7 +2116,7 @@ var Project = class {
2097
2116
  }
2098
2117
  });
2099
2118
  assertTransaction(currentText);
2100
- const transformed = custom ?? this.transformFile(filePath, currentText);
2119
+ const transformed = custom ?? this.transformFile(hookFilePath, currentText);
2101
2120
  assertTransaction(currentText);
2102
2121
  if (currentText !== transformed) sourceFile.replaceWithText(transformed);
2103
2122
  assertTransaction(transformed);
@@ -2161,6 +2180,7 @@ var Project = class {
2161
2180
  const importers = new Set([...this.dependents.get(current) ?? [], ...this.removedDependents.get(current) ?? []]);
2162
2181
  for (const importer of [...importers].sort()) {
2163
2182
  if (importer === start || seen.has(importer)) continue;
2183
+ if (this.auxiliarySources.has(importer)) continue;
2164
2184
  seen.add(importer);
2165
2185
  queue.push(importer);
2166
2186
  }
@@ -2298,7 +2318,7 @@ var Project = class {
2298
2318
  if (error?.code !== "ENOENT") throw error;
2299
2319
  }
2300
2320
  };
2301
- addSourceFile = (filePath, content) => {
2321
+ addSourceFile = (filePath, content, options = {}) => {
2302
2322
  this.#assertNotLoading();
2303
2323
  this.#ensureSourceFiles();
2304
2324
  const existing = filePath.includes("/") ? this.project.getSourceFile(filePath) : void 0;
@@ -2324,14 +2344,25 @@ var Project = class {
2324
2344
  * may have replaced this file's text through a `parser:before` hook. Such a file no longer
2325
2345
  * matches its own source, falls through, and is overwritten exactly as before.
2326
2346
  */
2327
- if (existing && existing.getFullText() === content) return existing;
2347
+ if (existing && existing.getFullText() === content) {
2348
+ this.markAuxiliary(existing.getFilePath(), options.auxiliary);
2349
+ return existing;
2350
+ }
2328
2351
  this.invalidateSourcePreparation(filePath, existing);
2329
2352
  this.removedSourcePaths.delete(this.normalizePath(filePath));
2330
2353
  this.invalidate(!existing, existing?.getFilePath());
2331
- return this.project.createSourceFile(filePath, content, {
2354
+ const sourceFile = this.project.createSourceFile(filePath, content, {
2332
2355
  overwrite: true,
2333
2356
  scriptKind: scriptKindFor(filePath)
2334
2357
  });
2358
+ this.markAuxiliary(sourceFile.getFilePath(), options.auxiliary);
2359
+ return sourceFile;
2360
+ };
2361
+ /** Claim or release compiler ownership of one source, in the ledger's own spelling. */
2362
+ markAuxiliary = (filePath, auxiliary) => {
2363
+ const normalized = this.normalizePath(filePath);
2364
+ if (auxiliary) this.auxiliarySources.add(normalized);
2365
+ else this.auxiliarySources.delete(normalized);
2335
2366
  };
2336
2367
  removeSourceFile = (filePath) => {
2337
2368
  this.#assertNotLoading();
@@ -2342,6 +2373,7 @@ var Project = class {
2342
2373
  this.invalidateSourcePreparation(filePath, sourceFile);
2343
2374
  this.markTargetRemoved(this.normalizePath(sourceFile.getFilePath()), sourceFile);
2344
2375
  this.options.parserOptions.encoder.releaseFile(sourceFile.getFilePath());
2376
+ this.auxiliarySources.delete(this.normalizePath(sourceFile.getFilePath()));
2345
2377
  return this.project.removeSourceFile(sourceFile);
2346
2378
  }
2347
2379
  return false;
@@ -2394,23 +2426,35 @@ var Project = class {
2394
2426
  this.#assertNotLoading();
2395
2427
  return this.options.getFiles;
2396
2428
  }
2397
- parseJson = (filePath) => {
2429
+ /**
2430
+ * A dumped encoder is a parse result like any other, so it belongs to whichever encoder the
2431
+ * caller named.
2432
+ *
2433
+ * Restoring into `parserOptions.encoder` regardless is the one place a supplied encoder was
2434
+ * silently ignored, and it stops being a formality once a bundler transform and the
2435
+ * extraction pass share one Project: the transform's parses go to a private clone precisely
2436
+ * so they cannot add rules to the sheet, and a `.json` module in its graph would have
2437
+ * pinned an entire safelist into the emitted encoder from a pass that emits nothing.
2438
+ */
2439
+ parseJson = (filePath, encoder) => {
2398
2440
  this.#assertNotLoading();
2399
2441
  const { readFile, parserOptions } = this.options;
2442
+ const target = encoder ?? parserOptions.encoder;
2400
2443
  const content = readFile(filePath);
2401
- parserOptions.encoder.fromJSON(JSON.parse(content));
2402
- return new ParserResult(parserOptions).setFilePath(filePath);
2444
+ target.fromJSON(JSON.parse(content));
2445
+ return new ParserResult(parserOptions, encoder).setFilePath(filePath);
2403
2446
  };
2404
- parseSourceFile = (filePath, encoder) => {
2447
+ parseSourceFile = (filePath, encoder, options = {}) => {
2405
2448
  this.#assertNotLoading();
2406
2449
  const { hooks } = this.options;
2407
- if (filePath.endsWith(".json")) return this.parseJson(filePath);
2450
+ const hookFilePath = options.hookFilePath ?? filePath;
2451
+ if (filePath.endsWith(".json")) return this.parseJson(filePath, encoder);
2408
2452
  const sourceFile = this.project.getSourceFile(filePath);
2409
2453
  if (!sourceFile) return;
2410
- const { options } = this.prepareEffectiveSource(filePath, sourceFile);
2411
- const result = (encoder ?? this.options.parserOptions.encoder).withOwner("parse", sourceFile.getFilePath(), () => this.parser(sourceFile, encoder, options, this.resolveModule))?.setFilePath(filePath);
2454
+ const { options: parserOptions } = this.prepareEffectiveSource(filePath, sourceFile, hookFilePath);
2455
+ const result = (encoder ?? this.options.parserOptions.encoder).withOwner("parse", sourceFile.getFilePath(), () => this.parser(sourceFile, encoder, parserOptions, this.resolveModule))?.setFilePath(filePath);
2412
2456
  hooks["parser:after"]?.({
2413
- filePath,
2457
+ filePath: hookFilePath,
2414
2458
  result
2415
2459
  });
2416
2460
  return result;
package/dist/index.d.cts CHANGED
@@ -6,7 +6,7 @@ import { ResolveModule } from "@bamboocss/extractor";
6
6
  //#region ../generator/dist/index.d.cts
7
7
  //#region src/generator.d.ts
8
8
  interface SplitCssArtifact {
9
- type: 'layer' | 'recipe' | 'theme';
9
+ type: 'layer' | 'theme';
10
10
  name: string;
11
11
  file: string;
12
12
  code: string;
@@ -16,12 +16,8 @@ interface SplitCssArtifact {
16
16
  interface SplitCssResult {
17
17
  /** Layer CSS files (reset, global, tokens, utilities) */
18
18
  layers: SplitCssArtifact[];
19
- /** Recipe CSS files */
20
- recipes: SplitCssArtifact[];
21
19
  /** Theme CSS files (not auto-imported) */
22
20
  themes: SplitCssArtifact[];
23
- /** Content for recipes.css */
24
- recipesIndex: string;
25
21
  /** Content for main styles.css */
26
22
  index: string;
27
23
  }
@@ -30,7 +26,11 @@ declare class Generator extends Context {
30
26
  getArtifacts: (ids?: ArtifactId[] | undefined) => import("@bamboocss/types").Artifact[];
31
27
  appendCssOfType: (type: CssArtifactType, sheet: Stylesheet) => void;
32
28
  appendLayerParams: (sheet: Stylesheet) => void;
33
- appendBaselineCss: (sheet: Stylesheet) => void;
29
+ appendBaselineCss: (sheet: Stylesheet, {
30
+ atomizeRecipes
31
+ }?: {
32
+ atomizeRecipes?: boolean;
33
+ }) => void;
34
34
  appendParserCss: (sheet: Stylesheet) => void;
35
35
  /**
36
36
  * Drop token css variables nothing can reach. Call this only once the sheet holds the
@@ -199,23 +199,11 @@ declare class Generator extends Context {
199
199
  * Get CSS for a specific layer from the stylesheet
200
200
  */
201
201
  getLayerCss: (sheet: Stylesheet, layer: "reset" | "base" | "tokens" | "recipes" | "utilities") => string;
202
- /**
203
- * Get CSS for a specific recipe
204
- */
205
- getRecipeCss: (recipeName: string) => string;
206
- /**
207
- * Get all recipe names from the decoder
208
- */
209
- getRecipeNames: () => string[];
210
202
  /**
211
203
  * Get all split CSS artifacts for the stylesheet
212
204
  * Used when --splitting flag is enabled
213
205
  */
214
- getSplitCssArtifacts: (sheet: Stylesheet, {
215
- includeRecipes
216
- }?: {
217
- includeRecipes?: boolean;
218
- }) => SplitCssResult;
206
+ getSplitCssArtifacts: (sheet: Stylesheet) => SplitCssResult;
219
207
  getSpec: () => SpecFile[];
220
208
  getSpecOfType: <T extends SpecType>(type: T) => T extends "color-palette" | "themes" ? SpecTypeMap[T] | undefined : SpecTypeMap[T];
221
209
  } //#endregion
@@ -439,6 +427,19 @@ interface ResolutionFact {
439
427
  readonly kind: 'import' | 'export';
440
428
  readonly ordinal: number;
441
429
  }
430
+ /** @internal How one explicitly supplied source relates to the checkout it is added to. */
431
+ interface AddSourceFileOptions {
432
+ /**
433
+ * This text belongs to a bundler transform rather than to the file at this path.
434
+ *
435
+ * @see `Project.auxiliarySources`
436
+ */
437
+ auxiliary?: boolean;
438
+ }
439
+ /** @internal Logical identity for hooks when a bundler source needs a synthetic AST path. */
440
+ interface ParseSourceFileOptions {
441
+ hookFilePath?: string;
442
+ }
442
443
  /** @internal Exact semantic closure plus missing local paths which can redirect it. */
443
444
  interface ResolutionReadSet {
444
445
  readonly dependencies: readonly string[];
@@ -508,6 +509,24 @@ declare class Project {
508
509
  private sourcePreparations;
509
510
  /** Paths explicitly removed through this wrapper, until an add/create observes them again. */
510
511
  private removedSourcePaths;
512
+ /**
513
+ * Sources a bundler transform owns, rather than members of the checkout.
514
+ *
515
+ * A compiler which shares this Project has to park bundler-transformed text somewhere, and
516
+ * it cannot be the file's own path — that is the canonical source every other reader
517
+ * resolves through. It parses under a sibling path instead, and that parse resolves its
518
+ * imports like any other, so the ledger gains an importer no watcher will ever report a
519
+ * change for. Left in, those facts reach the incremental extraction pass: an auxiliary
520
+ * importer would be selected as a dependent of the file it shadows and ordered against
521
+ * inventory members that do not import it.
522
+ *
523
+ * Excluded from exactly the three queries that decide what a rebuild re-extracts —
524
+ * the ledger, the dependent walk and the unresolved-importer set — and from nothing else.
525
+ * Resolution itself is deliberately untouched: an auxiliary parse must still see the same
526
+ * modules the real one would, and its forward edges are what a bundler registers as watch
527
+ * files.
528
+ */
529
+ private auxiliarySources;
511
530
  private resolutionWork;
512
531
  private resetResolutionState;
513
532
  /** Files whose imports may resolve differently after a local file appears. */
@@ -600,7 +619,9 @@ declare class Project {
600
619
  getResolutionConfigurationFiles: (filePath: string, targets?: Iterable<string>, previous?: Iterable<string>) => readonly string[];
601
620
  createSourceFile: (filePath: string) => SourceFile;
602
621
  createSourceFiles: () => void;
603
- addSourceFile: (filePath: string, content: string) => SourceFile;
622
+ addSourceFile: (filePath: string, content: string, options?: AddSourceFileOptions) => SourceFile;
623
+ /** Claim or release compiler ownership of one source, in the ledger's own spelling. */
624
+ private markAuxiliary;
604
625
  removeSourceFile: (filePath: string) => boolean;
605
626
  /**
606
627
  * The current digest of one exported value, for read verification.
@@ -613,10 +634,20 @@ declare class Project {
613
634
  reloadSourceFiles: () => void;
614
635
  get readFile(): (filePath: string) => string;
615
636
  get getFiles(): () => string[];
616
- parseJson: (filePath: string) => ParserResult;
617
- parseSourceFile: (filePath: string, encoder?: ParserOptions["encoder"]) => ParserResult | undefined;
637
+ /**
638
+ * A dumped encoder is a parse result like any other, so it belongs to whichever encoder the
639
+ * caller named.
640
+ *
641
+ * Restoring into `parserOptions.encoder` regardless is the one place a supplied encoder was
642
+ * silently ignored, and it stops being a formality once a bundler transform and the
643
+ * extraction pass share one Project: the transform's parses go to a private clone precisely
644
+ * so they cannot add rules to the sheet, and a `.json` module in its graph would have
645
+ * pinned an entire safelist into the emitted encoder from a pass that emits nothing.
646
+ */
647
+ parseJson: (filePath: string, encoder?: ParserOptions["encoder"]) => ParserResult;
648
+ parseSourceFile: (filePath: string, encoder?: ParserOptions["encoder"], options?: ParseSourceFileOptions) => ParserResult | undefined;
618
649
  transformFile: (_filePath: string, content: string) => string;
619
650
  classify: (fileMap: Map<string, ParserResultInterface>) => import("@bamboocss/types").ClassifyReport;
620
651
  }
621
652
  //#endregion
622
- export { ParserResult, Project, ProjectOptions, ResolutionFact, ResolutionReadSet, ResolutionWork, type UnresolvedStyle, findUnresolvedStyles };
653
+ export { AddSourceFileOptions, ParseSourceFileOptions, ParserResult, Project, ProjectOptions, ResolutionFact, ResolutionReadSet, ResolutionWork, type UnresolvedStyle, findUnresolvedStyles };
package/dist/index.d.mts CHANGED
@@ -6,7 +6,7 @@ import { ArtifactId, BambooHooks, ConfigTsOptions, CssArtifactType, LoadConfigRe
6
6
  //#region ../generator/dist/index.d.cts
7
7
  //#region src/generator.d.ts
8
8
  interface SplitCssArtifact {
9
- type: 'layer' | 'recipe' | 'theme';
9
+ type: 'layer' | 'theme';
10
10
  name: string;
11
11
  file: string;
12
12
  code: string;
@@ -16,12 +16,8 @@ interface SplitCssArtifact {
16
16
  interface SplitCssResult {
17
17
  /** Layer CSS files (reset, global, tokens, utilities) */
18
18
  layers: SplitCssArtifact[];
19
- /** Recipe CSS files */
20
- recipes: SplitCssArtifact[];
21
19
  /** Theme CSS files (not auto-imported) */
22
20
  themes: SplitCssArtifact[];
23
- /** Content for recipes.css */
24
- recipesIndex: string;
25
21
  /** Content for main styles.css */
26
22
  index: string;
27
23
  }
@@ -30,7 +26,11 @@ declare class Generator extends Context {
30
26
  getArtifacts: (ids?: ArtifactId[] | undefined) => import("@bamboocss/types").Artifact[];
31
27
  appendCssOfType: (type: CssArtifactType, sheet: Stylesheet) => void;
32
28
  appendLayerParams: (sheet: Stylesheet) => void;
33
- appendBaselineCss: (sheet: Stylesheet) => void;
29
+ appendBaselineCss: (sheet: Stylesheet, {
30
+ atomizeRecipes
31
+ }?: {
32
+ atomizeRecipes?: boolean;
33
+ }) => void;
34
34
  appendParserCss: (sheet: Stylesheet) => void;
35
35
  /**
36
36
  * Drop token css variables nothing can reach. Call this only once the sheet holds the
@@ -199,23 +199,11 @@ declare class Generator extends Context {
199
199
  * Get CSS for a specific layer from the stylesheet
200
200
  */
201
201
  getLayerCss: (sheet: Stylesheet, layer: "reset" | "base" | "tokens" | "recipes" | "utilities") => string;
202
- /**
203
- * Get CSS for a specific recipe
204
- */
205
- getRecipeCss: (recipeName: string) => string;
206
- /**
207
- * Get all recipe names from the decoder
208
- */
209
- getRecipeNames: () => string[];
210
202
  /**
211
203
  * Get all split CSS artifacts for the stylesheet
212
204
  * Used when --splitting flag is enabled
213
205
  */
214
- getSplitCssArtifacts: (sheet: Stylesheet, {
215
- includeRecipes
216
- }?: {
217
- includeRecipes?: boolean;
218
- }) => SplitCssResult;
206
+ getSplitCssArtifacts: (sheet: Stylesheet) => SplitCssResult;
219
207
  getSpec: () => SpecFile[];
220
208
  getSpecOfType: <T extends SpecType>(type: T) => T extends "color-palette" | "themes" ? SpecTypeMap[T] | undefined : SpecTypeMap[T];
221
209
  } //#endregion
@@ -439,6 +427,19 @@ interface ResolutionFact {
439
427
  readonly kind: 'import' | 'export';
440
428
  readonly ordinal: number;
441
429
  }
430
+ /** @internal How one explicitly supplied source relates to the checkout it is added to. */
431
+ interface AddSourceFileOptions {
432
+ /**
433
+ * This text belongs to a bundler transform rather than to the file at this path.
434
+ *
435
+ * @see `Project.auxiliarySources`
436
+ */
437
+ auxiliary?: boolean;
438
+ }
439
+ /** @internal Logical identity for hooks when a bundler source needs a synthetic AST path. */
440
+ interface ParseSourceFileOptions {
441
+ hookFilePath?: string;
442
+ }
442
443
  /** @internal Exact semantic closure plus missing local paths which can redirect it. */
443
444
  interface ResolutionReadSet {
444
445
  readonly dependencies: readonly string[];
@@ -508,6 +509,24 @@ declare class Project {
508
509
  private sourcePreparations;
509
510
  /** Paths explicitly removed through this wrapper, until an add/create observes them again. */
510
511
  private removedSourcePaths;
512
+ /**
513
+ * Sources a bundler transform owns, rather than members of the checkout.
514
+ *
515
+ * A compiler which shares this Project has to park bundler-transformed text somewhere, and
516
+ * it cannot be the file's own path — that is the canonical source every other reader
517
+ * resolves through. It parses under a sibling path instead, and that parse resolves its
518
+ * imports like any other, so the ledger gains an importer no watcher will ever report a
519
+ * change for. Left in, those facts reach the incremental extraction pass: an auxiliary
520
+ * importer would be selected as a dependent of the file it shadows and ordered against
521
+ * inventory members that do not import it.
522
+ *
523
+ * Excluded from exactly the three queries that decide what a rebuild re-extracts —
524
+ * the ledger, the dependent walk and the unresolved-importer set — and from nothing else.
525
+ * Resolution itself is deliberately untouched: an auxiliary parse must still see the same
526
+ * modules the real one would, and its forward edges are what a bundler registers as watch
527
+ * files.
528
+ */
529
+ private auxiliarySources;
511
530
  private resolutionWork;
512
531
  private resetResolutionState;
513
532
  /** Files whose imports may resolve differently after a local file appears. */
@@ -600,7 +619,9 @@ declare class Project {
600
619
  getResolutionConfigurationFiles: (filePath: string, targets?: Iterable<string>, previous?: Iterable<string>) => readonly string[];
601
620
  createSourceFile: (filePath: string) => SourceFile;
602
621
  createSourceFiles: () => void;
603
- addSourceFile: (filePath: string, content: string) => SourceFile;
622
+ addSourceFile: (filePath: string, content: string, options?: AddSourceFileOptions) => SourceFile;
623
+ /** Claim or release compiler ownership of one source, in the ledger's own spelling. */
624
+ private markAuxiliary;
604
625
  removeSourceFile: (filePath: string) => boolean;
605
626
  /**
606
627
  * The current digest of one exported value, for read verification.
@@ -613,10 +634,20 @@ declare class Project {
613
634
  reloadSourceFiles: () => void;
614
635
  get readFile(): (filePath: string) => string;
615
636
  get getFiles(): () => string[];
616
- parseJson: (filePath: string) => ParserResult;
617
- parseSourceFile: (filePath: string, encoder?: ParserOptions["encoder"]) => ParserResult | undefined;
637
+ /**
638
+ * A dumped encoder is a parse result like any other, so it belongs to whichever encoder the
639
+ * caller named.
640
+ *
641
+ * Restoring into `parserOptions.encoder` regardless is the one place a supplied encoder was
642
+ * silently ignored, and it stops being a formality once a bundler transform and the
643
+ * extraction pass share one Project: the transform's parses go to a private clone precisely
644
+ * so they cannot add rules to the sheet, and a `.json` module in its graph would have
645
+ * pinned an entire safelist into the emitted encoder from a pass that emits nothing.
646
+ */
647
+ parseJson: (filePath: string, encoder?: ParserOptions["encoder"]) => ParserResult;
648
+ parseSourceFile: (filePath: string, encoder?: ParserOptions["encoder"], options?: ParseSourceFileOptions) => ParserResult | undefined;
618
649
  transformFile: (_filePath: string, content: string) => string;
619
650
  classify: (fileMap: Map<string, ParserResultInterface>) => import("@bamboocss/types").ClassifyReport;
620
651
  }
621
652
  //#endregion
622
- export { ParserResult, Project, ProjectOptions, ResolutionFact, ResolutionReadSet, ResolutionWork, type UnresolvedStyle, findUnresolvedStyles };
653
+ export { AddSourceFileOptions, ParseSourceFileOptions, ParserResult, Project, ProjectOptions, ResolutionFact, ResolutionReadSet, ResolutionWork, type UnresolvedStyle, findUnresolvedStyles };
package/dist/index.mjs CHANGED
@@ -1690,6 +1690,24 @@ var Project = class {
1690
1690
  sourcePreparations = /* @__PURE__ */ new Map();
1691
1691
  /** Paths explicitly removed through this wrapper, until an add/create observes them again. */
1692
1692
  removedSourcePaths = /* @__PURE__ */ new Set();
1693
+ /**
1694
+ * Sources a bundler transform owns, rather than members of the checkout.
1695
+ *
1696
+ * A compiler which shares this Project has to park bundler-transformed text somewhere, and
1697
+ * it cannot be the file's own path — that is the canonical source every other reader
1698
+ * resolves through. It parses under a sibling path instead, and that parse resolves its
1699
+ * imports like any other, so the ledger gains an importer no watcher will ever report a
1700
+ * change for. Left in, those facts reach the incremental extraction pass: an auxiliary
1701
+ * importer would be selected as a dependent of the file it shadows and ordered against
1702
+ * inventory members that do not import it.
1703
+ *
1704
+ * Excluded from exactly the three queries that decide what a rebuild re-extracts —
1705
+ * the ledger, the dependent walk and the unresolved-importer set — and from nothing else.
1706
+ * Resolution itself is deliberately untouched: an auxiliary parse must still see the same
1707
+ * modules the real one would, and its forward edges are what a bundler registers as watch
1708
+ * files.
1709
+ */
1710
+ auxiliarySources = /* @__PURE__ */ new Set();
1693
1711
  /** File-tree changes invalidate even successful resolutions (extension precedence can move). */
1694
1712
  #fileTreeRevision = 0;
1695
1713
  resolutionWork = {
@@ -1708,6 +1726,7 @@ var Project = class {
1708
1726
  this.resolutionsByImporter = /* @__PURE__ */ new Map();
1709
1727
  this.sourcePreparations = /* @__PURE__ */ new Map();
1710
1728
  this.removedSourcePaths = /* @__PURE__ */ new Set();
1729
+ this.auxiliarySources = /* @__PURE__ */ new Set();
1711
1730
  this.resolutionWork = {
1712
1731
  moduleResolutionsAttempted: 0,
1713
1732
  sourceFilesAdded: 0,
@@ -1717,12 +1736,12 @@ var Project = class {
1717
1736
  /** Files whose imports may resolve differently after a local file appears. */
1718
1737
  getUnresolvedImporters = () => {
1719
1738
  this.#assertNotLoading();
1720
- return [...this.unresolvedImporters].sort();
1739
+ return [...this.unresolvedImporters].filter((importer) => !this.auxiliarySources.has(importer)).sort();
1721
1740
  };
1722
1741
  /** @internal Immutable resolution facts in importer/AST order. */
1723
1742
  getResolutionLedger = () => {
1724
1743
  this.#assertNotLoading();
1725
- return Object.freeze([...this.resolutionsByImporter.entries()].sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).flatMap(([, entry]) => entry.facts));
1744
+ return Object.freeze([...this.resolutionsByImporter.entries()].filter(([importer]) => !this.auxiliarySources.has(importer)).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0).flatMap(([, entry]) => entry.facts));
1726
1745
  };
1727
1746
  /** @internal Every distinct local target represented by the current ledger. */
1728
1747
  getResolvedSourceFiles = () => {
@@ -2051,7 +2070,7 @@ var Project = class {
2051
2070
  * downstream edges the ledger records. The transaction is published only after the AST and
2052
2071
  * its facts agree. A failed or re-entered attempt restores the input AST and can be retried.
2053
2072
  */
2054
- prepareEffectiveSource = (filePath, sourceFile) => {
2073
+ prepareEffectiveSource = (filePath, sourceFile, hookFilePath = filePath) => {
2055
2074
  this.#assertNotLoading();
2056
2075
  const sourcePath = this.normalizePath(sourceFile.getFilePath());
2057
2076
  const currentText = sourceFile.getFullText();
@@ -2086,7 +2105,7 @@ var Project = class {
2086
2105
  const options = {};
2087
2106
  try {
2088
2107
  const custom = this.options.hooks["parser:before"]?.({
2089
- filePath,
2108
+ filePath: hookFilePath,
2090
2109
  content: currentText,
2091
2110
  configure(next) {
2092
2111
  const { matchTag, matchTagMode, matchTagProp } = next;
@@ -2096,7 +2115,7 @@ var Project = class {
2096
2115
  }
2097
2116
  });
2098
2117
  assertTransaction(currentText);
2099
- const transformed = custom ?? this.transformFile(filePath, currentText);
2118
+ const transformed = custom ?? this.transformFile(hookFilePath, currentText);
2100
2119
  assertTransaction(currentText);
2101
2120
  if (currentText !== transformed) sourceFile.replaceWithText(transformed);
2102
2121
  assertTransaction(transformed);
@@ -2160,6 +2179,7 @@ var Project = class {
2160
2179
  const importers = new Set([...this.dependents.get(current) ?? [], ...this.removedDependents.get(current) ?? []]);
2161
2180
  for (const importer of [...importers].sort()) {
2162
2181
  if (importer === start || seen.has(importer)) continue;
2182
+ if (this.auxiliarySources.has(importer)) continue;
2163
2183
  seen.add(importer);
2164
2184
  queue.push(importer);
2165
2185
  }
@@ -2297,7 +2317,7 @@ var Project = class {
2297
2317
  if (error?.code !== "ENOENT") throw error;
2298
2318
  }
2299
2319
  };
2300
- addSourceFile = (filePath, content) => {
2320
+ addSourceFile = (filePath, content, options = {}) => {
2301
2321
  this.#assertNotLoading();
2302
2322
  this.#ensureSourceFiles();
2303
2323
  const existing = filePath.includes("/") ? this.project.getSourceFile(filePath) : void 0;
@@ -2323,14 +2343,25 @@ var Project = class {
2323
2343
  * may have replaced this file's text through a `parser:before` hook. Such a file no longer
2324
2344
  * matches its own source, falls through, and is overwritten exactly as before.
2325
2345
  */
2326
- if (existing && existing.getFullText() === content) return existing;
2346
+ if (existing && existing.getFullText() === content) {
2347
+ this.markAuxiliary(existing.getFilePath(), options.auxiliary);
2348
+ return existing;
2349
+ }
2327
2350
  this.invalidateSourcePreparation(filePath, existing);
2328
2351
  this.removedSourcePaths.delete(this.normalizePath(filePath));
2329
2352
  this.invalidate(!existing, existing?.getFilePath());
2330
- return this.project.createSourceFile(filePath, content, {
2353
+ const sourceFile = this.project.createSourceFile(filePath, content, {
2331
2354
  overwrite: true,
2332
2355
  scriptKind: scriptKindFor(filePath)
2333
2356
  });
2357
+ this.markAuxiliary(sourceFile.getFilePath(), options.auxiliary);
2358
+ return sourceFile;
2359
+ };
2360
+ /** Claim or release compiler ownership of one source, in the ledger's own spelling. */
2361
+ markAuxiliary = (filePath, auxiliary) => {
2362
+ const normalized = this.normalizePath(filePath);
2363
+ if (auxiliary) this.auxiliarySources.add(normalized);
2364
+ else this.auxiliarySources.delete(normalized);
2334
2365
  };
2335
2366
  removeSourceFile = (filePath) => {
2336
2367
  this.#assertNotLoading();
@@ -2341,6 +2372,7 @@ var Project = class {
2341
2372
  this.invalidateSourcePreparation(filePath, sourceFile);
2342
2373
  this.markTargetRemoved(this.normalizePath(sourceFile.getFilePath()), sourceFile);
2343
2374
  this.options.parserOptions.encoder.releaseFile(sourceFile.getFilePath());
2375
+ this.auxiliarySources.delete(this.normalizePath(sourceFile.getFilePath()));
2344
2376
  return this.project.removeSourceFile(sourceFile);
2345
2377
  }
2346
2378
  return false;
@@ -2393,23 +2425,35 @@ var Project = class {
2393
2425
  this.#assertNotLoading();
2394
2426
  return this.options.getFiles;
2395
2427
  }
2396
- parseJson = (filePath) => {
2428
+ /**
2429
+ * A dumped encoder is a parse result like any other, so it belongs to whichever encoder the
2430
+ * caller named.
2431
+ *
2432
+ * Restoring into `parserOptions.encoder` regardless is the one place a supplied encoder was
2433
+ * silently ignored, and it stops being a formality once a bundler transform and the
2434
+ * extraction pass share one Project: the transform's parses go to a private clone precisely
2435
+ * so they cannot add rules to the sheet, and a `.json` module in its graph would have
2436
+ * pinned an entire safelist into the emitted encoder from a pass that emits nothing.
2437
+ */
2438
+ parseJson = (filePath, encoder) => {
2397
2439
  this.#assertNotLoading();
2398
2440
  const { readFile, parserOptions } = this.options;
2441
+ const target = encoder ?? parserOptions.encoder;
2399
2442
  const content = readFile(filePath);
2400
- parserOptions.encoder.fromJSON(JSON.parse(content));
2401
- return new ParserResult(parserOptions).setFilePath(filePath);
2443
+ target.fromJSON(JSON.parse(content));
2444
+ return new ParserResult(parserOptions, encoder).setFilePath(filePath);
2402
2445
  };
2403
- parseSourceFile = (filePath, encoder) => {
2446
+ parseSourceFile = (filePath, encoder, options = {}) => {
2404
2447
  this.#assertNotLoading();
2405
2448
  const { hooks } = this.options;
2406
- if (filePath.endsWith(".json")) return this.parseJson(filePath);
2449
+ const hookFilePath = options.hookFilePath ?? filePath;
2450
+ if (filePath.endsWith(".json")) return this.parseJson(filePath, encoder);
2407
2451
  const sourceFile = this.project.getSourceFile(filePath);
2408
2452
  if (!sourceFile) return;
2409
- const { options } = this.prepareEffectiveSource(filePath, sourceFile);
2410
- const result = (encoder ?? this.options.parserOptions.encoder).withOwner("parse", sourceFile.getFilePath(), () => this.parser(sourceFile, encoder, options, this.resolveModule))?.setFilePath(filePath);
2453
+ const { options: parserOptions } = this.prepareEffectiveSource(filePath, sourceFile, hookFilePath);
2454
+ const result = (encoder ?? this.options.parserOptions.encoder).withOwner("parse", sourceFile.getFilePath(), () => this.parser(sourceFile, encoder, parserOptions, this.resolveModule))?.setFilePath(filePath);
2411
2455
  hooks["parser:after"]?.({
2412
- filePath,
2456
+ filePath: hookFilePath,
2413
2457
  result
2414
2458
  });
2415
2459
  return result;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/parser",
3
- "version": "1.47.0",
3
+ "version": "1.48.0",
4
4
  "description": "The static parser for bamboo css",
5
5
  "homepage": "https://bamboocss.com",
6
6
  "license": "MIT",
@@ -34,18 +34,18 @@
34
34
  "dependencies": {
35
35
  "ts-morph": "28.0.0",
36
36
  "ts-pattern": "5.9.0",
37
- "@bamboocss/config": "^1.47.0",
38
- "@bamboocss/core": "^1.47.0",
39
- "@bamboocss/extractor": "1.47.0",
40
- "@bamboocss/logger": "1.47.0",
41
- "@bamboocss/shared": "1.47.0",
42
- "@bamboocss/types": "1.47.0"
37
+ "@bamboocss/config": "^1.48.0",
38
+ "@bamboocss/core": "^1.48.0",
39
+ "@bamboocss/extractor": "1.48.0",
40
+ "@bamboocss/logger": "1.48.0",
41
+ "@bamboocss/shared": "1.48.0",
42
+ "@bamboocss/types": "1.48.0"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@typescript/api": "npm:typescript@7.1.0-dev.20260819.1",
46
- "@bamboocss/generator": "1.47.0",
47
- "@bamboocss/plugin-svelte": "1.47.0",
48
- "@bamboocss/plugin-vue": "1.47.0"
46
+ "@bamboocss/generator": "1.48.0",
47
+ "@bamboocss/plugin-svelte": "1.48.0",
48
+ "@bamboocss/plugin-vue": "1.48.0"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsdown src/index.ts --format=esm,cjs --dts",