@bamboocss/parser 1.46.0 → 1.46.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/dist/index.cjs CHANGED
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  let ts_morph = require("ts-morph");
3
3
  let _bamboocss_extractor = require("@bamboocss/extractor");
4
4
  let _bamboocss_shared = require("@bamboocss/shared");
5
+ let node_crypto = require("node:crypto");
5
6
  let _bamboocss_logger = require("@bamboocss/logger");
6
7
  let ts_pattern = require("ts-pattern");
7
8
  let _bamboocss_config_ts_path = require("@bamboocss/config/ts-path");
@@ -612,6 +613,52 @@ const walkImports = (sourceFile, imports, resolveModule, seen) => {
612
613
  */
613
614
  const importedRecipeBindings = (sourceFile, imports, resolveModule) => walkImports(sourceFile, imports, resolveModule, new Set([sourceFile])).bindings;
614
615
  //#endregion
616
+ //#region src/export-read-digest.ts
617
+ /**
618
+ * A digest of what one exported name resolves to right now, or `undefined` when the answer
619
+ * cannot be pinned down.
620
+ *
621
+ * This is the witness the read-verification protocol compares: a dependent records the digest
622
+ * of every cross-file value it folded, and an edit to the exporting file is verified by
623
+ * recomputing the same digest through the same resolution path. The two sides must therefore
624
+ * be the *same function* -- recorded at parse time, recomputed at verification time -- or
625
+ * equal values would compare unequal and every edit would fall back to a full re-fold.
626
+ *
627
+ * `undefined` is the conservative verdict, and everything unusual maps to it: a missing
628
+ * initializer, an evaluation the bare context cannot resolve (a value built through the
629
+ * parser's evaluation environment, say), a conditional value, a non-serializable result, a
630
+ * throw. An export that stopped existing is *not* unknown -- it digests to a distinct
631
+ * sentinel, so its consumers read as changed rather than unverifiable.
632
+ *
633
+ * Serialized with insertion order preserved: property order is part of a style value's
634
+ * meaning, so reordering keys must read as a change. `undefined` leaves inside the value are
635
+ * kept visible through a sentinel, since `JSON.stringify` would otherwise erase the
636
+ * difference between an unresolvable member and an absent one.
637
+ */
638
+ const digestExportValue = (sourceFile, exportedName, resolveModule, onCrossing) => {
639
+ if (!sourceFile) return "bamboo:module-missing";
640
+ const boxCtx = {
641
+ flags: { skipTraverseFiles: false },
642
+ resolveModule,
643
+ recordDependency: onCrossing
644
+ };
645
+ try {
646
+ const declaration = (0, _bamboocss_extractor.getExportedVarDeclarationWithName)(exportedName, sourceFile, [], boxCtx);
647
+ if (!declaration) return "bamboo:export-missing";
648
+ const initializer = declaration.getInitializer?.();
649
+ if (!initializer) return void 0;
650
+ const box = (0, _bamboocss_extractor.maybeBoxNode)(initializer, [], boxCtx);
651
+ if (!box || Array.isArray(box)) return void 0;
652
+ const unboxed = (0, _bamboocss_extractor.unbox)(box);
653
+ if (unboxed.conditions.length || unboxed.spreadConditions.length) return void 0;
654
+ const json = JSON.stringify(unboxed.raw, (_key, value) => value === void 0 ? "bamboo:undefined" : value);
655
+ if (json === void 0) return void 0;
656
+ return (0, node_crypto.createHash)("sha256").update(json).digest("base64");
657
+ } catch {
658
+ return;
659
+ }
660
+ };
661
+ //#endregion
615
662
  //#region src/get-import-declarations.ts
616
663
  function getImportDeclarations(context, sourceFile) {
617
664
  const { imports, tsOptions } = context;
@@ -1048,6 +1095,16 @@ var ParserResult = class {
1048
1095
  addDependency(filePath) {
1049
1096
  this.dependencies.add(filePath.replaceAll("\\", "/"));
1050
1097
  }
1098
+ /** Cross-file value reads this extraction performed, with the digests read at parse time. */
1099
+ exportReads = [];
1100
+ /** @internal Set once by the parser after extraction, digests computed on warm caches. */
1101
+ setExportReads(reads) {
1102
+ this.exportReads = reads;
1103
+ }
1104
+ /** @internal The reads a consumer can verify instead of re-folding; see the Vite plugin. */
1105
+ getExportReads() {
1106
+ return this.exportReads;
1107
+ }
1051
1108
  /**
1052
1109
  * Local source paths crossed while resolving values this extraction actually encoded.
1053
1110
  *
@@ -1173,6 +1230,8 @@ function createParser(context) {
1173
1230
  });
1174
1231
  };
1175
1232
  const recordDependency = (filePath) => parserResult.addDependency(filePath);
1233
+ const exportReadPairs = /* @__PURE__ */ new Set();
1234
+ const recordExportRead = (filePath, exportedName) => exportReadPairs.add(`${filePath.replaceAll("\\", "/")}\u0000${exportedName}`);
1176
1235
  (0, _bamboocss_extractor.extract)({
1177
1236
  ast: sourceFile,
1178
1237
  tokens: context.tokens ? {
@@ -1223,6 +1282,7 @@ function createParser(context) {
1223
1282
  },
1224
1283
  flags: { skipTraverseFiles: false },
1225
1284
  recordDependency,
1285
+ recordExportRead,
1226
1286
  resolveModule
1227
1287
  }).forEach((result, alias) => {
1228
1288
  const name = file.getName(file.normalizeFnName(alias));
@@ -1315,6 +1375,19 @@ function createParser(context) {
1315
1375
  });
1316
1376
  });
1317
1377
  parserResult.deadCalls = file.getDeadCalls();
1378
+ if (exportReadPairs.size) {
1379
+ const project = sourceFile.getProject();
1380
+ parserResult.setExportReads([...exportReadPairs].map((pair) => {
1381
+ const at = pair.indexOf("\0");
1382
+ const file = pair.slice(0, at);
1383
+ const name = pair.slice(at + 1);
1384
+ return {
1385
+ file,
1386
+ name,
1387
+ digest: digestExportValue(project.getSourceFile(file), name, resolveModule)
1388
+ };
1389
+ }));
1390
+ }
1318
1391
  return parserResult;
1319
1392
  };
1320
1393
  }
@@ -1680,8 +1753,28 @@ var Project = class {
1680
1753
  * dependency edge; now it would also leave a recipe permanently invisible to the module
1681
1754
  * importing it, since resolution is what finds one.
1682
1755
  */
1683
- invalidate = (fileTreeChanged = true) => {
1756
+ invalidate = (fileTreeChanged = true, changedPath) => {
1684
1757
  this.#assertNotLoading();
1758
+ /**
1759
+ * A content edit to a file the project already holds invalidates *by path* rather than
1760
+ * dropping every memoized value. Each cache entry records the module paths its
1761
+ * computation read — the same record the watch system replays into fold dependencies —
1762
+ * so marking the edited path stale rejects exactly the entries whose value could have
1763
+ * moved, while a value resolved without reading this file keeps its cache. The edited
1764
+ * file's own entries need no marking: its re-parse retires their key nodes.
1765
+ *
1766
+ * That narrowing holds only for content: a file appearing or disappearing can change
1767
+ * what a specifier resolves to without any recorded path's bytes moving — extension
1768
+ * precedence, a new local shadowing a package — so tree changes keep the full clear.
1769
+ * The recipe-origin memo records no read-set, so it is cleared on every event either
1770
+ * way; per edit that is one walk rebuilt lazily, not the per-transform storm the
1771
+ * identical-text no-op in `addSourceFile` exists to prevent.
1772
+ */
1773
+ if (!fileTreeChanged && changedPath) {
1774
+ (0, _bamboocss_extractor.invalidateDependencyPath)(this.normalizePath(changedPath));
1775
+ clearImportedRecipeCache();
1776
+ return;
1777
+ }
1685
1778
  invalidateResolutions();
1686
1779
  if (fileTreeChanged) {
1687
1780
  this.#moduleResolutionCache = void 0;
@@ -2234,7 +2327,7 @@ var Project = class {
2234
2327
  if (existing && existing.getFullText() === content) return existing;
2235
2328
  this.invalidateSourcePreparation(filePath, existing);
2236
2329
  this.removedSourcePaths.delete(this.normalizePath(filePath));
2237
- this.invalidate(!existing);
2330
+ this.invalidate(!existing, existing?.getFilePath());
2238
2331
  return this.project.createSourceFile(filePath, content, {
2239
2332
  overwrite: true,
2240
2333
  scriptKind: scriptKindFor(filePath)
@@ -2253,11 +2346,22 @@ var Project = class {
2253
2346
  }
2254
2347
  return false;
2255
2348
  };
2349
+ /**
2350
+ * The current digest of one exported value, for read verification.
2351
+ *
2352
+ * Same function and same resolver identity as the parse-time recording, which is what makes
2353
+ * the comparison meaningful; see `digestExportValue`.
2354
+ */
2355
+ digestExportRead = (filePath, exportedName, onCrossing) => {
2356
+ this.#assertNotLoading();
2357
+ this.#ensureSourceFiles();
2358
+ return digestExportValue(this.getSourceFile(filePath), exportedName, this.resolveModule, onCrossing);
2359
+ };
2256
2360
  reloadSourceFile = (filePath) => {
2257
2361
  this.#assertNotLoading();
2258
2362
  this.#ensureSourceFiles();
2259
- this.invalidate(false);
2260
2363
  const sourceFile = this.getSourceFile(filePath);
2364
+ this.invalidate(false, sourceFile?.getFilePath());
2261
2365
  if (!sourceFile) return;
2262
2366
  this.invalidateSourcePreparation(filePath, sourceFile);
2263
2367
  return sourceFile.refreshFromFileSystemSync();
package/dist/index.d.cts CHANGED
@@ -357,6 +357,20 @@ declare class ParserResult implements ParserResultInterface {
357
357
  setFilePath(filePath: string): this;
358
358
  /** @internal Called only by the extractor-facing resolver, not by import classification. */
359
359
  addDependency(filePath: string): void;
360
+ /** Cross-file value reads this extraction performed, with the digests read at parse time. */
361
+ private exportReads;
362
+ /** @internal Set once by the parser after extraction, digests computed on warm caches. */
363
+ setExportReads(reads: Array<{
364
+ file: string;
365
+ name: string;
366
+ digest: string | undefined;
367
+ }>): void;
368
+ /** @internal The reads a consumer can verify instead of re-folding; see the Vite plugin. */
369
+ getExportReads(): ReadonlyArray<{
370
+ file: string;
371
+ name: string;
372
+ digest: string | undefined;
373
+ }>;
360
374
  /**
361
375
  * Local source paths crossed while resolving values this extraction actually encoded.
362
376
  *
@@ -584,6 +598,13 @@ declare class Project {
584
598
  createSourceFiles: () => void;
585
599
  addSourceFile: (filePath: string, content: string) => SourceFile;
586
600
  removeSourceFile: (filePath: string) => boolean;
601
+ /**
602
+ * The current digest of one exported value, for read verification.
603
+ *
604
+ * Same function and same resolver identity as the parse-time recording, which is what makes
605
+ * the comparison meaningful; see `digestExportValue`.
606
+ */
607
+ digestExportRead: (filePath: string, exportedName: string, onCrossing?: (crossedPath: string) => void) => string | undefined;
587
608
  reloadSourceFile: (filePath: string) => FileSystemRefreshResult | undefined;
588
609
  reloadSourceFiles: () => void;
589
610
  get readFile(): (filePath: string) => string;
package/dist/index.d.mts CHANGED
@@ -357,6 +357,20 @@ declare class ParserResult implements ParserResultInterface {
357
357
  setFilePath(filePath: string): this;
358
358
  /** @internal Called only by the extractor-facing resolver, not by import classification. */
359
359
  addDependency(filePath: string): void;
360
+ /** Cross-file value reads this extraction performed, with the digests read at parse time. */
361
+ private exportReads;
362
+ /** @internal Set once by the parser after extraction, digests computed on warm caches. */
363
+ setExportReads(reads: Array<{
364
+ file: string;
365
+ name: string;
366
+ digest: string | undefined;
367
+ }>): void;
368
+ /** @internal The reads a consumer can verify instead of re-folding; see the Vite plugin. */
369
+ getExportReads(): ReadonlyArray<{
370
+ file: string;
371
+ name: string;
372
+ digest: string | undefined;
373
+ }>;
360
374
  /**
361
375
  * Local source paths crossed while resolving values this extraction actually encoded.
362
376
  *
@@ -584,6 +598,13 @@ declare class Project {
584
598
  createSourceFiles: () => void;
585
599
  addSourceFile: (filePath: string, content: string) => SourceFile;
586
600
  removeSourceFile: (filePath: string) => boolean;
601
+ /**
602
+ * The current digest of one exported value, for read verification.
603
+ *
604
+ * Same function and same resolver identity as the parse-time recording, which is what makes
605
+ * the comparison meaningful; see `digestExportValue`.
606
+ */
607
+ digestExportRead: (filePath: string, exportedName: string, onCrossing?: (crossedPath: string) => void) => string | undefined;
587
608
  reloadSourceFile: (filePath: string) => FileSystemRefreshResult | undefined;
588
609
  reloadSourceFiles: () => void;
589
610
  get readFile(): (filePath: string) => string;
package/dist/index.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Node, Project as Project$1, ScriptKind, ts } from "ts-morph";
2
- import { box, clearBoxNodeCache, extract, unbox, unwrapExpression } from "@bamboocss/extractor";
2
+ import { box, clearBoxNodeCache, extract, getExportedVarDeclarationWithName, invalidateDependencyPath, maybeBoxNode, unbox, unwrapExpression } from "@bamboocss/extractor";
3
3
  import { BambooError, compact, createPatternFns, getOrCreateSet } from "@bamboocss/shared";
4
+ import { createHash } from "node:crypto";
4
5
  import { logger } from "@bamboocss/logger";
5
6
  import { match } from "ts-pattern";
6
7
  import { resolveTsPathPattern } from "@bamboocss/config/ts-path";
@@ -611,6 +612,52 @@ const walkImports = (sourceFile, imports, resolveModule, seen) => {
611
612
  */
612
613
  const importedRecipeBindings = (sourceFile, imports, resolveModule) => walkImports(sourceFile, imports, resolveModule, new Set([sourceFile])).bindings;
613
614
  //#endregion
615
+ //#region src/export-read-digest.ts
616
+ /**
617
+ * A digest of what one exported name resolves to right now, or `undefined` when the answer
618
+ * cannot be pinned down.
619
+ *
620
+ * This is the witness the read-verification protocol compares: a dependent records the digest
621
+ * of every cross-file value it folded, and an edit to the exporting file is verified by
622
+ * recomputing the same digest through the same resolution path. The two sides must therefore
623
+ * be the *same function* -- recorded at parse time, recomputed at verification time -- or
624
+ * equal values would compare unequal and every edit would fall back to a full re-fold.
625
+ *
626
+ * `undefined` is the conservative verdict, and everything unusual maps to it: a missing
627
+ * initializer, an evaluation the bare context cannot resolve (a value built through the
628
+ * parser's evaluation environment, say), a conditional value, a non-serializable result, a
629
+ * throw. An export that stopped existing is *not* unknown -- it digests to a distinct
630
+ * sentinel, so its consumers read as changed rather than unverifiable.
631
+ *
632
+ * Serialized with insertion order preserved: property order is part of a style value's
633
+ * meaning, so reordering keys must read as a change. `undefined` leaves inside the value are
634
+ * kept visible through a sentinel, since `JSON.stringify` would otherwise erase the
635
+ * difference between an unresolvable member and an absent one.
636
+ */
637
+ const digestExportValue = (sourceFile, exportedName, resolveModule, onCrossing) => {
638
+ if (!sourceFile) return "bamboo:module-missing";
639
+ const boxCtx = {
640
+ flags: { skipTraverseFiles: false },
641
+ resolveModule,
642
+ recordDependency: onCrossing
643
+ };
644
+ try {
645
+ const declaration = getExportedVarDeclarationWithName(exportedName, sourceFile, [], boxCtx);
646
+ if (!declaration) return "bamboo:export-missing";
647
+ const initializer = declaration.getInitializer?.();
648
+ if (!initializer) return void 0;
649
+ const box = maybeBoxNode(initializer, [], boxCtx);
650
+ if (!box || Array.isArray(box)) return void 0;
651
+ const unboxed = unbox(box);
652
+ if (unboxed.conditions.length || unboxed.spreadConditions.length) return void 0;
653
+ const json = JSON.stringify(unboxed.raw, (_key, value) => value === void 0 ? "bamboo:undefined" : value);
654
+ if (json === void 0) return void 0;
655
+ return createHash("sha256").update(json).digest("base64");
656
+ } catch {
657
+ return;
658
+ }
659
+ };
660
+ //#endregion
614
661
  //#region src/get-import-declarations.ts
615
662
  function getImportDeclarations(context, sourceFile) {
616
663
  const { imports, tsOptions } = context;
@@ -1047,6 +1094,16 @@ var ParserResult = class {
1047
1094
  addDependency(filePath) {
1048
1095
  this.dependencies.add(filePath.replaceAll("\\", "/"));
1049
1096
  }
1097
+ /** Cross-file value reads this extraction performed, with the digests read at parse time. */
1098
+ exportReads = [];
1099
+ /** @internal Set once by the parser after extraction, digests computed on warm caches. */
1100
+ setExportReads(reads) {
1101
+ this.exportReads = reads;
1102
+ }
1103
+ /** @internal The reads a consumer can verify instead of re-folding; see the Vite plugin. */
1104
+ getExportReads() {
1105
+ return this.exportReads;
1106
+ }
1050
1107
  /**
1051
1108
  * Local source paths crossed while resolving values this extraction actually encoded.
1052
1109
  *
@@ -1172,6 +1229,8 @@ function createParser(context) {
1172
1229
  });
1173
1230
  };
1174
1231
  const recordDependency = (filePath) => parserResult.addDependency(filePath);
1232
+ const exportReadPairs = /* @__PURE__ */ new Set();
1233
+ const recordExportRead = (filePath, exportedName) => exportReadPairs.add(`${filePath.replaceAll("\\", "/")}\u0000${exportedName}`);
1175
1234
  extract({
1176
1235
  ast: sourceFile,
1177
1236
  tokens: context.tokens ? {
@@ -1222,6 +1281,7 @@ function createParser(context) {
1222
1281
  },
1223
1282
  flags: { skipTraverseFiles: false },
1224
1283
  recordDependency,
1284
+ recordExportRead,
1225
1285
  resolveModule
1226
1286
  }).forEach((result, alias) => {
1227
1287
  const name = file.getName(file.normalizeFnName(alias));
@@ -1314,6 +1374,19 @@ function createParser(context) {
1314
1374
  });
1315
1375
  });
1316
1376
  parserResult.deadCalls = file.getDeadCalls();
1377
+ if (exportReadPairs.size) {
1378
+ const project = sourceFile.getProject();
1379
+ parserResult.setExportReads([...exportReadPairs].map((pair) => {
1380
+ const at = pair.indexOf("\0");
1381
+ const file = pair.slice(0, at);
1382
+ const name = pair.slice(at + 1);
1383
+ return {
1384
+ file,
1385
+ name,
1386
+ digest: digestExportValue(project.getSourceFile(file), name, resolveModule)
1387
+ };
1388
+ }));
1389
+ }
1317
1390
  return parserResult;
1318
1391
  };
1319
1392
  }
@@ -1679,8 +1752,28 @@ var Project = class {
1679
1752
  * dependency edge; now it would also leave a recipe permanently invisible to the module
1680
1753
  * importing it, since resolution is what finds one.
1681
1754
  */
1682
- invalidate = (fileTreeChanged = true) => {
1755
+ invalidate = (fileTreeChanged = true, changedPath) => {
1683
1756
  this.#assertNotLoading();
1757
+ /**
1758
+ * A content edit to a file the project already holds invalidates *by path* rather than
1759
+ * dropping every memoized value. Each cache entry records the module paths its
1760
+ * computation read — the same record the watch system replays into fold dependencies —
1761
+ * so marking the edited path stale rejects exactly the entries whose value could have
1762
+ * moved, while a value resolved without reading this file keeps its cache. The edited
1763
+ * file's own entries need no marking: its re-parse retires their key nodes.
1764
+ *
1765
+ * That narrowing holds only for content: a file appearing or disappearing can change
1766
+ * what a specifier resolves to without any recorded path's bytes moving — extension
1767
+ * precedence, a new local shadowing a package — so tree changes keep the full clear.
1768
+ * The recipe-origin memo records no read-set, so it is cleared on every event either
1769
+ * way; per edit that is one walk rebuilt lazily, not the per-transform storm the
1770
+ * identical-text no-op in `addSourceFile` exists to prevent.
1771
+ */
1772
+ if (!fileTreeChanged && changedPath) {
1773
+ invalidateDependencyPath(this.normalizePath(changedPath));
1774
+ clearImportedRecipeCache();
1775
+ return;
1776
+ }
1684
1777
  invalidateResolutions();
1685
1778
  if (fileTreeChanged) {
1686
1779
  this.#moduleResolutionCache = void 0;
@@ -2233,7 +2326,7 @@ var Project = class {
2233
2326
  if (existing && existing.getFullText() === content) return existing;
2234
2327
  this.invalidateSourcePreparation(filePath, existing);
2235
2328
  this.removedSourcePaths.delete(this.normalizePath(filePath));
2236
- this.invalidate(!existing);
2329
+ this.invalidate(!existing, existing?.getFilePath());
2237
2330
  return this.project.createSourceFile(filePath, content, {
2238
2331
  overwrite: true,
2239
2332
  scriptKind: scriptKindFor(filePath)
@@ -2252,11 +2345,22 @@ var Project = class {
2252
2345
  }
2253
2346
  return false;
2254
2347
  };
2348
+ /**
2349
+ * The current digest of one exported value, for read verification.
2350
+ *
2351
+ * Same function and same resolver identity as the parse-time recording, which is what makes
2352
+ * the comparison meaningful; see `digestExportValue`.
2353
+ */
2354
+ digestExportRead = (filePath, exportedName, onCrossing) => {
2355
+ this.#assertNotLoading();
2356
+ this.#ensureSourceFiles();
2357
+ return digestExportValue(this.getSourceFile(filePath), exportedName, this.resolveModule, onCrossing);
2358
+ };
2255
2359
  reloadSourceFile = (filePath) => {
2256
2360
  this.#assertNotLoading();
2257
2361
  this.#ensureSourceFiles();
2258
- this.invalidate(false);
2259
2362
  const sourceFile = this.getSourceFile(filePath);
2363
+ this.invalidate(false, sourceFile?.getFilePath());
2260
2364
  if (!sourceFile) return;
2261
2365
  this.invalidateSourcePreparation(filePath, sourceFile);
2262
2366
  return sourceFile.refreshFromFileSystemSync();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bamboocss/parser",
3
- "version": "1.46.0",
3
+ "version": "1.46.2",
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.46.0",
38
- "@bamboocss/core": "^1.46.0",
39
- "@bamboocss/extractor": "1.46.0",
40
- "@bamboocss/logger": "1.46.0",
41
- "@bamboocss/shared": "1.46.0",
42
- "@bamboocss/types": "1.46.0"
37
+ "@bamboocss/config": "^1.46.2",
38
+ "@bamboocss/core": "^1.46.2",
39
+ "@bamboocss/extractor": "1.46.2",
40
+ "@bamboocss/logger": "1.46.2",
41
+ "@bamboocss/shared": "1.46.2",
42
+ "@bamboocss/types": "1.46.2"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@typescript/api": "npm:typescript@7.1.0-dev.20260819.1",
46
- "@bamboocss/generator": "1.46.0",
47
- "@bamboocss/plugin-svelte": "1.46.0",
48
- "@bamboocss/plugin-vue": "1.46.0"
46
+ "@bamboocss/generator": "1.46.2",
47
+ "@bamboocss/plugin-svelte": "1.46.2",
48
+ "@bamboocss/plugin-vue": "1.46.2"
49
49
  },
50
50
  "scripts": {
51
51
  "build": "tsdown src/index.ts --format=esm,cjs --dts",