swift-rust 1.8.0 → 1.10.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.
@@ -414,22 +414,22 @@ function detectIslandExports(src) {
414
414
  return { hasDefault, named: [...named] };
415
415
  }
416
416
 
417
- // Temp sibling files holding verbatim copies of client components, so the
417
+ // Build the source of an island wrapper module for a given client component
418
+ // file. Temp sibling files hold verbatim copies of client components, so the
418
419
  // wrapper can import the *real* module under a distinct on-disk path (Bun
419
420
  // dedupes modules by resolved path, so a query-string alias would collapse the
420
- // real module into the wrapper). Cleaned up after each SSR build.
421
- const islandRawTemps = [];
422
-
423
- // Build the source of an island wrapper module for a given client component file.
424
- function islandWrapperSource(abs) {
421
+ // real module into the wrapper). `temps` is per-build: a shared global list
422
+ // let concurrent builds delete each other's temp files mid-build, which
423
+ // surfaced as transient module-not-found errors on save.
424
+ function islandWrapperSource(abs, temps) {
425
425
  const src = readFileSync(abs, "utf8");
426
426
  const { hasDefault, named } = detectIslandExports(src);
427
427
  // Write the real component to a unique sibling so imports resolve identically.
428
428
  const rawName = `.__sr_raw_${process.pid}_${Math.random().toString(36).slice(2)}__${basename(abs)}`;
429
429
  const rawPath = join(dirname(abs), rawName);
430
430
  writeFileSync(rawPath, src);
431
- islandRawTemps.push(rawPath);
432
- const rawSpec = JSON.stringify("./" + rawName);
431
+ temps.push(rawPath);
432
+ const rawSpec = JSON.stringify(rawPath);
433
433
  let out = `import { createElement as __h } from "react";
434
434
  import * as __real from ${rawSpec};
435
435
  const __src = ${JSON.stringify(abs)};
@@ -467,43 +467,152 @@ function __wrap(Comp, name){
467
467
  return out;
468
468
  }
469
469
 
470
+ // Which exported names of a 'use cache' module are *async functions* (the only
471
+ // ones safe to wrap with cache(), since cache() always returns a Promise). We
472
+ // pass everything else through untouched.
473
+ function detectAsyncExports(src) {
474
+ const code = src.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/[^\n]*/g, "$1");
475
+ const asyncNamed = new Set();
476
+ const allNamed = new Set();
477
+ let defaultAsync = false;
478
+ let hasDefault = false;
479
+ if (/\bexport\s+default\b/.test(code)) {
480
+ hasDefault = true;
481
+ if (/\bexport\s+default\s+async\b/.test(code)) defaultAsync = true;
482
+ }
483
+ for (const m of code.matchAll(/\bexport\s+(async\s+)?function\s+([A-Za-z_$][\w$]*)/g)) {
484
+ allNamed.add(m[2]);
485
+ if (m[1]) asyncNamed.add(m[2]);
486
+ }
487
+ for (const m of code.matchAll(/\bexport\s+const\s+([A-Za-z_$][\w$]*)\s*=\s*(async\b)?/g)) {
488
+ allNamed.add(m[1]);
489
+ if (m[2]) asyncNamed.add(m[1]);
490
+ }
491
+ for (const m of code.matchAll(/\bexport\s*\{([^}]*)\}/g)) {
492
+ for (const part of m[1].split(",")) {
493
+ const name = part.trim().split(/\s+as\s+/).pop()?.trim();
494
+ if (name && name !== "default") allNamed.add(name);
495
+ }
496
+ }
497
+ return { asyncNamed: [...asyncNamed], allNamed: [...allNamed], defaultAsync, hasDefault };
498
+ }
499
+
500
+ // Source of a wrapper module that memoizes a 'use cache' module's async exports.
501
+ function cacheWrapperSource(abs, temps) {
502
+ const src = readFileSync(abs, "utf8");
503
+ const { asyncNamed, allNamed, defaultAsync, hasDefault } = detectAsyncExports(src);
504
+ const rawName = `.__sr_raw_${process.pid}_${Math.random().toString(36).slice(2)}__${basename(abs)}`;
505
+ const rawPath = join(dirname(abs), rawName);
506
+ writeFileSync(rawPath, src);
507
+ temps.push(rawPath);
508
+ const tag = relative(cwd, abs);
509
+ let out = `import { cache as __cache } from "swift-rust/cache";
510
+ import * as __real from ${JSON.stringify(rawPath)};
511
+ const __opts = { tags: ${JSON.stringify([tag])} };
512
+ `;
513
+ if (hasDefault) {
514
+ out += defaultAsync
515
+ ? `export default __cache(__real.default, __opts);\n`
516
+ : `export default __real.default;\n`;
517
+ }
518
+ for (const n of allNamed) {
519
+ out += asyncNamed.includes(n)
520
+ ? `export const ${n} = __cache(__real[${JSON.stringify(n)}], __opts);\n`
521
+ : `export const ${n} = __real[${JSON.stringify(n)}];\n`;
522
+ }
523
+ return out;
524
+ }
525
+
526
+ // Bun plugin: auto-memoize modules that declare a leading 'use cache' directive.
527
+ // Built per-build so each build owns its temp-file list (see islandWrapperSource).
528
+ function makeUseCachePlugin(temps) {
529
+ return {
530
+ name: "sr-use-cache",
531
+ setup(build) {
532
+ build.onResolve({ filter: /.*/ }, (args) => {
533
+ const p = args.path;
534
+ if (!(p.startsWith(".") || p.startsWith("/") || p.startsWith("@/"))) return undefined;
535
+ // Our own raw temp copy: resolve it explicitly into the file namespace.
536
+ // (Deferring to Bun's default resolver fails from a virtual namespace.)
537
+ if (/\.__sr_raw_/.test(p)) return { path: p, namespace: "file" };
538
+ if (args.importer && hasUseDirective(args.importer, "cache")) return undefined;
539
+ const abs = resolveIslandSpecifier(p, args.importer);
540
+ if (!abs || !hasUseDirective(abs, "cache")) return undefined;
541
+ return { path: abs, namespace: "sr-cache" };
542
+ });
543
+ build.onLoad({ filter: /.*/, namespace: "sr-cache" }, (args) => ({
544
+ contents: cacheWrapperSource(args.path, temps),
545
+ loader: "js",
546
+ resolveDir: dirname(args.path),
547
+ }));
548
+ },
549
+ };
550
+ }
551
+
470
552
  // Bun plugin: wrap client components imported by *server* modules as islands.
471
- const clientIslandPlugin = {
472
- name: "sr-client-islands",
473
- setup(build) {
474
- build.onResolve({ filter: /.*/ }, (args) => {
475
- const p = args.path;
476
- if (!(p.startsWith(".") || p.startsWith("/") || p.startsWith("@/"))) return undefined;
477
- // Never wrap our own temp raw copies, and don't wrap when the importer is
478
- // itself a client module nested client components belong to that
479
- // island's bundle, not a new boundary.
480
- if (/\.__sr_raw_/.test(p)) return undefined;
481
- if (args.importer && hasUseDirective(args.importer, "client")) return undefined;
482
- const abs = resolveIslandSpecifier(p, args.importer);
483
- if (!abs || !hasUseDirective(abs, "client")) return undefined;
484
- return { path: abs, namespace: "sr-island" };
485
- });
486
- build.onLoad({ filter: /.*/, namespace: "sr-island" }, (args) => ({
487
- contents: islandWrapperSource(args.path),
488
- loader: "js",
489
- resolveDir: dirname(args.path),
490
- }));
491
- },
492
- };
553
+ function makeClientIslandPlugin(temps) {
554
+ return {
555
+ name: "sr-client-islands",
556
+ setup(build) {
557
+ build.onResolve({ filter: /.*/ }, (args) => {
558
+ const p = args.path;
559
+ if (!(p.startsWith(".") || p.startsWith("/") || p.startsWith("@/"))) return undefined;
560
+ // Never wrap our own temp raw copies, and don't wrap when the importer is
561
+ // itself a client module nested client components belong to that
562
+ // island's bundle, not a new boundary.
563
+ if (/\.__sr_raw_/.test(p)) return { path: p, namespace: "file" };
564
+ if (args.importer && hasUseDirective(args.importer, "client")) return undefined;
565
+ const abs = resolveIslandSpecifier(p, args.importer);
566
+ if (!abs || !hasUseDirective(abs, "client")) return undefined;
567
+ return { path: abs, namespace: "sr-island" };
568
+ });
569
+ build.onLoad({ filter: /.*/, namespace: "sr-island" }, (args) => ({
570
+ contents: islandWrapperSource(args.path, temps),
571
+ loader: "js",
572
+ resolveDir: dirname(args.path),
573
+ }));
574
+ },
575
+ };
576
+ }
577
+
578
+ // In-flight build dedup: concurrent requests for the same entry (every open tab
579
+ // reloads at once on an HMR broadcast) await one build instead of racing.
580
+ function withInflight(map, key, gen, factory) {
581
+ const cur = map.get(key);
582
+ if (cur && cur.gen === gen) return cur.promise;
583
+ const entry = { gen, promise: null };
584
+ entry.promise = (async () => {
585
+ try {
586
+ return await factory();
587
+ } finally {
588
+ if (map.get(key) === entry) map.delete(key);
589
+ }
590
+ })();
591
+ map.set(key, entry);
592
+ return entry.promise;
593
+ }
493
594
 
494
595
  // Cache for per-component browser island bundles (src -> { gen, code }).
495
596
  // Each bundle is self-mounting: it imports the client component, finds every
496
597
  // marker on the page for this source, and hydrates it. React is bundled in, so a
497
598
  // single <script type="module"> per source needs no import map or shared runtime.
498
599
  const componentIslandCache = new Map();
600
+ const componentIslandInflight = new Map();
499
601
  async function buildComponentIslandBundle(srcFile) {
500
602
  if (typeof Bun === "undefined" || typeof Bun.build !== "function") {
501
603
  throw new Error("client islands require the Bun runtime");
502
604
  }
503
605
  const cached = componentIslandCache.get(srcFile);
504
606
  if (cached && cached.gen === buildGeneration) return cached.code;
505
-
506
- const entryPath = join(dirname(srcFile), `.__sr_cisland_${process.pid}_${Date.now()}.js`);
607
+ return withInflight(componentIslandInflight, srcFile, buildGeneration, () =>
608
+ buildComponentIslandBundleNow(srcFile),
609
+ );
610
+ }
611
+ async function buildComponentIslandBundleNow(srcFile) {
612
+ const entryPath = join(
613
+ dirname(srcFile),
614
+ `.__sr_cisland_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2)}.js`,
615
+ );
507
616
  const entry = `import { hydrateRoot, createRoot } from "react-dom/client";
508
617
  import { createElement } from "react";
509
618
  import * as __mod from ${JSON.stringify(srcFile)};
@@ -551,6 +660,7 @@ function collectIslandSources(html) {
551
660
  }
552
661
 
553
662
  const ssrBundleCache = new Map(); // file -> { gen, mod }
663
+ const ssrInflight = new Map(); // file -> { gen, promise }
554
664
 
555
665
  async function loadModuleFresh(filePath) {
556
666
  if (typeof Bun === "undefined" || typeof Bun.build !== "function") {
@@ -558,28 +668,32 @@ async function loadModuleFresh(filePath) {
558
668
  }
559
669
  const cached = ssrBundleCache.get(filePath);
560
670
  if (cached && cached.gen === buildGeneration) return cached.mod;
671
+ return withInflight(ssrInflight, filePath, buildGeneration, () =>
672
+ buildSsrModule(filePath, buildGeneration),
673
+ );
674
+ }
561
675
 
562
- islandRawTemps.length = 0;
676
+ async function buildSsrModule(filePath, gen) {
677
+ const temps = [];
563
678
  let result;
564
679
  try {
565
680
  result = await Bun.build({
566
681
  entrypoints: [filePath],
567
682
  target: "bun",
568
- plugins: [clientIslandPlugin, externalizeDepsPlugin],
683
+ plugins: [makeUseCachePlugin(temps), makeClientIslandPlugin(temps), externalizeDepsPlugin],
569
684
  });
570
685
  } finally {
571
- for (const t of islandRawTemps) { try { unlinkSync(t); } catch {} }
572
- islandRawTemps.length = 0;
686
+ for (const t of temps) { try { unlinkSync(t); } catch {} }
573
687
  }
574
688
  if (!result.success) {
575
689
  throw new Error(result.logs.map((l) => l.message).join("\n"));
576
690
  }
577
691
  const code = await result.outputs[0].text();
578
- const tmp = join(dirname(filePath), `.__sr_ssr_${buildGeneration}_${Math.random().toString(36).slice(2)}.mjs`);
692
+ const tmp = join(dirname(filePath), `.__sr_ssr_${gen}_${Math.random().toString(36).slice(2)}.mjs`);
579
693
  writeFileSync(tmp, code);
580
694
  try {
581
695
  const mod = await import(pathToFileURL(tmp).href);
582
- ssrBundleCache.set(filePath, { gen: buildGeneration, mod });
696
+ ssrBundleCache.set(filePath, { gen, mod });
583
697
  return mod;
584
698
  } finally {
585
699
  try { unlinkSync(tmp); } catch {}
@@ -617,11 +731,15 @@ async function compileRoute(urlPath) {
617
731
  }
618
732
 
619
733
  try {
620
- await loadModule(route.file, { bust: true });
621
- for (const layout of layouts) await loadModule(layout.file, { bust: true });
622
- if (notFoundFile) await loadModule(notFoundFile, { bust: true });
623
- if (errorFile) await loadModule(errorFile, { bust: true });
624
- if (loadingFile) await loadModule(loadingFile, { bust: true });
734
+ // Generation-keyed bundles: a save bumps the generation (fresh build), an
735
+ // unchanged file is a cache hit. The old `?t=` bust-imports created new
736
+ // module instances on every request they leaked (ESM registry entries
737
+ // are permanent) and, under Bun, didn't even pick up child-module edits.
738
+ await loadModuleFresh(route.file);
739
+ for (const layout of layouts) await loadModuleFresh(layout.file);
740
+ if (notFoundFile) await loadModuleFresh(notFoundFile);
741
+ if (errorFile) await loadModuleFresh(errorFile);
742
+ if (loadingFile) await loadModuleFresh(loadingFile);
625
743
  } catch (err) {
626
744
  compileTimings.set(urlPath, performance.now() - start);
627
745
  return { ok: false, reason: "compile_error", error: err, pageFile: route.file, layoutFile: layouts[0]?.file, params: route.params, segments };
@@ -645,14 +763,33 @@ function findAppGlobalsCss() {
645
763
  return null;
646
764
  }
647
765
 
766
+ // A require() rooted at the user's project, so Node module resolution walks the
767
+ // real node_modules tree (project → monorepo root → …) and finds packages
768
+ // wherever the package manager hoisted them. Critical for production builds
769
+ // (e.g. Vercel), where hard-coded ../../node_modules guesses miss the plugin
770
+ // and Tailwind silently doesn't compile — shipping an unstyled site.
771
+ let _projectRequire;
772
+ function projectRequire() {
773
+ if (!_projectRequire) _projectRequire = createRequire(join(cwd, "package.json"));
774
+ return _projectRequire;
775
+ }
776
+
648
777
  let postcssInstance = null;
649
778
  async function getPostcss() {
650
779
  if (postcssInstance !== null) return postcssInstance;
780
+ // 1) Resolve through the project's module graph.
781
+ try {
782
+ const entry = projectRequire().resolve("postcss");
783
+ const mod = await import(pathToFileURL(entry).href);
784
+ postcssInstance = { available: true, default: mod.default ?? mod, mod };
785
+ return postcssInstance;
786
+ } catch {}
787
+ // 2) Hard-coded fallbacks, then a bare import.
651
788
  try {
652
789
  const candidates = [
653
790
  join(cwd, "node_modules", "postcss"),
654
- join(cwd, "node_modules", "postcss", "lib", "postcss.mjs"),
655
- join(cwd, "node_modules", "postcss", "lib", "postcss.js"),
791
+ join(cwd, "..", "..", "node_modules", "postcss"),
792
+ join(cwd, "..", "..", "..", "node_modules", "postcss"),
656
793
  ];
657
794
  for (const candidate of candidates) {
658
795
  if (existsSync(candidate)) {
@@ -671,9 +808,19 @@ async function getPostcss() {
671
808
  }
672
809
 
673
810
  async function loadPostcssPluginByName(name) {
811
+ // 1) Node resolution from the project root — finds the plugin at any hoist level.
812
+ try {
813
+ const entryPath = projectRequire().resolve(name);
814
+ const mod = await import(pathToFileURL(entryPath).href);
815
+ const result = mod.default ?? mod;
816
+ logLine([` ${paint("dim", "css plugin loaded:")} ${paint("cyan", name)} ${paint("dim", "(resolved)")}`], 1);
817
+ return result;
818
+ } catch {}
819
+ // 2) Fallback: scan a few likely node_modules locations.
674
820
  const candidates = [
675
821
  join(cwd, "node_modules", name),
676
822
  join(cwd, "..", "..", "node_modules", name),
823
+ join(cwd, "..", "..", "..", "node_modules", name),
677
824
  ];
678
825
  for (const candidate of candidates) {
679
826
  const pkgJsonPath = join(candidate, "package.json");
@@ -696,7 +843,7 @@ async function loadPostcssPluginByName(name) {
696
843
  }
697
844
  }
698
845
  }
699
- logLine([` ${paint("dim", "css plugin not found:")} ${paint("yellow", name)}`], 1);
846
+ logLine([` ${paint("dim", "css plugin not found:")} ${paint("yellow", name)} ${paint("dim", "— is it in dependencies?")}`], 1);
700
847
  return null;
701
848
  }
702
849
 
@@ -890,13 +1037,13 @@ async function resolveMetadata(layoutFiles, pageFile, params, segments) {
890
1037
  const metas = [];
891
1038
  for (const layoutFile of layoutFiles || []) {
892
1039
  try {
893
- const mod = await loadModule(layoutFile, { bust: true });
1040
+ const mod = await loadModuleFresh(layoutFile);
894
1041
  if (mod.metadata) metas.push(mod.metadata);
895
1042
  } catch {}
896
1043
  }
897
1044
  if (pageFile) {
898
1045
  try {
899
- const mod = await loadModule(pageFile, { bust: true });
1046
+ const mod = await loadModuleFresh(pageFile);
900
1047
  if (mod.generateMetadata) {
901
1048
  const m = await mod.generateMetadata({ params: params || {}, searchParams: {} });
902
1049
  if (m) metas.push(m);
@@ -1072,6 +1219,7 @@ export function hasUseDirective(file, name) {
1072
1219
  return false;
1073
1220
  }
1074
1221
 
1222
+ const islandBundleInflight = new Map();
1075
1223
  async function buildIslandBundle(pageFile) {
1076
1224
  if (typeof Bun === "undefined" || typeof Bun.build !== "function") {
1077
1225
  throw new Error("client islands require the Bun runtime");
@@ -1079,9 +1227,16 @@ async function buildIslandBundle(pageFile) {
1079
1227
  assertNoServerComponentInClientPage(pageFile);
1080
1228
  const cached = islandBundleCache.get(pageFile);
1081
1229
  if (cached && cached.gen === buildGeneration) return cached.code;
1082
-
1230
+ return withInflight(islandBundleInflight, pageFile, buildGeneration, () =>
1231
+ buildIslandBundleNow(pageFile),
1232
+ );
1233
+ }
1234
+ async function buildIslandBundleNow(pageFile) {
1083
1235
  // Temp hydration entry, written next to the page so module resolution works.
1084
- const entryPath = join(dirname(pageFile), `.__sr_island_${process.pid}_${Date.now()}.js`);
1236
+ const entryPath = join(
1237
+ dirname(pageFile),
1238
+ `.__sr_island_${process.pid}_${Date.now()}_${Math.random().toString(36).slice(2)}.js`,
1239
+ );
1085
1240
  const entry = `import { createRoot } from "react-dom/client";
1086
1241
  import { createElement } from "react";
1087
1242
  import Page from ${JSON.stringify(pageFile)};
@@ -1796,7 +1951,7 @@ async function renderRoute(urlPath, req) {
1796
1951
  if (errorFile) {
1797
1952
  try {
1798
1953
  const React = await import("react");
1799
- const errorMod = await loadModule(errorFile, { bust: true });
1954
+ const errorMod = await loadModuleFresh(errorFile);
1800
1955
  const ErrorBoundary = errorMod.default ?? errorMod.ErrorBoundary ?? errorMod.error;
1801
1956
  if (ErrorBoundary) {
1802
1957
  const html = await renderToStringCompat(React.createElement(ErrorBoundary, { error: err }));
@@ -1809,7 +1964,7 @@ async function renderRoute(urlPath, req) {
1809
1964
  if (recoveryFile) {
1810
1965
  try {
1811
1966
  const React = await import("react");
1812
- const mod = await loadModule(recoveryFile, { bust: true });
1967
+ const mod = await loadModuleFresh(recoveryFile);
1813
1968
  const Recovery = mod.default ?? mod.ErrorRecovery;
1814
1969
  if (Recovery) {
1815
1970
  const html = await renderToStringCompat(
@@ -1826,7 +1981,7 @@ async function renderRoute(urlPath, req) {
1826
1981
  if (globalErrorFile) {
1827
1982
  try {
1828
1983
  const React = await import("react");
1829
- const mod = await loadModule(globalErrorFile, { bust: true });
1984
+ const mod = await loadModuleFresh(globalErrorFile);
1830
1985
  const GlobalError = mod.default ?? mod.GlobalError;
1831
1986
  if (GlobalError) {
1832
1987
  const inner = await renderToStringCompat(
@@ -1914,14 +2069,14 @@ async function renderNotFound(segments) {
1914
2069
  try {
1915
2070
  const React = await import("react");
1916
2071
  const layouts = findLayoutsFor(segments || []);
1917
- const mod = await loadModule(notFoundFile, { bust: true });
2072
+ const mod = await loadModuleFresh(notFoundFile);
1918
2073
  const NotFound = mod.default ?? mod.NotFound ?? mod.notFound;
1919
2074
  if (!NotFound) {
1920
2075
  return { status: 404, html: null, error: null, segments };
1921
2076
  }
1922
2077
  let tree = React.createElement(NotFound);
1923
2078
  for (let i = layouts.length - 1; i >= 0; i--) {
1924
- const layoutMod = await loadModule(layouts[i].file, { bust: true });
2079
+ const layoutMod = await loadModuleFresh(layouts[i].file);
1925
2080
  const Layout = layoutMod.default ?? layoutMod.Layout ?? layoutMod.layout;
1926
2081
  if (Layout) tree = React.createElement(Layout, null, tree);
1927
2082
  }
@@ -2019,83 +2174,219 @@ function readNavigatorClient() {
2019
2174
  }
2020
2175
  }
2021
2176
 
2022
- function shouldIgnoreFile(filename) {
2023
- if (!filename) return true;
2024
- const base = filename.split(sep).pop();
2025
- if (!base) return true;
2026
- if (base.startsWith(".")) return true;
2177
+ const WATCH_IGNORE_DIRS = new Set([
2178
+ "node_modules",
2179
+ "dist",
2180
+ "build",
2181
+ "out",
2182
+ "coverage",
2183
+ "target",
2184
+ ]);
2185
+
2186
+ // Ignore anything inside a dep/build dir, any dot-prefixed path segment (which
2187
+ // also covers our own .__sr_* build temps and dirs like .git/.turbo/.next),
2188
+ // and editor scratch suffixes.
2189
+ function isIgnoredWatchPath(full) {
2190
+ const rel = relative(cwd, full);
2191
+ if (!rel || rel.startsWith("..")) return true;
2192
+ const parts = rel.split(sep);
2193
+ for (const p of parts) {
2194
+ if (!p || p.startsWith(".")) return true;
2195
+ if (WATCH_IGNORE_DIRS.has(p)) return true;
2196
+ }
2197
+ const base = parts[parts.length - 1];
2027
2198
  if (base.endsWith(".bak") || base.endsWith(".tmp") || base.endsWith("~")) return true;
2028
- if (base === "node_modules") return true;
2029
2199
  return false;
2030
2200
  }
2031
2201
 
2202
+ const SYNTAX_CHECK_LOADERS = { ".ts": "ts", ".tsx": "tsx", ".js": "js", ".jsx": "jsx", ".mjs": "js" };
2203
+
2204
+ // Quick transpile of the changed sources before telling the browser to reload.
2205
+ // Editors save in stages (truncate+write, format-on-save), so the first FS
2206
+ // event often fires mid-write; compiling that state used to navigate every tab
2207
+ // into a dead 500 page. A failed check retries once after a beat (to skate
2208
+ // over partial writes); a persistent failure is a real syntax error.
2209
+ async function findSyntaxError(files) {
2210
+ if (typeof Bun === "undefined" || typeof Bun.Transpiler !== "function") return null;
2211
+ for (const file of files) {
2212
+ const loader = SYNTAX_CHECK_LOADERS[extname(file)];
2213
+ if (!loader) continue;
2214
+ for (let attempt = 0; ; attempt++) {
2215
+ let src;
2216
+ try {
2217
+ src = readFileSync(file, "utf8");
2218
+ } catch {
2219
+ break; // deleted — nothing to check
2220
+ }
2221
+ try {
2222
+ new Bun.Transpiler({ loader }).transformSync(src);
2223
+ break;
2224
+ } catch (err) {
2225
+ if (attempt >= 1) {
2226
+ const msg = err?.message || String(err);
2227
+ return `${relative(cwd, file)}: ${msg}`;
2228
+ }
2229
+ await new Promise((r) => setTimeout(r, 150));
2230
+ }
2231
+ }
2232
+ }
2233
+ return null;
2234
+ }
2235
+
2236
+ function broadcastHmr(data) {
2237
+ const payload = JSON.stringify({ event: "change", data });
2238
+ for (const send of hmrClients) {
2239
+ try {
2240
+ send(payload);
2241
+ } catch {}
2242
+ }
2243
+ }
2244
+
2032
2245
  function setupWatcher() {
2033
2246
  if (!existsSync(APP_DIR)) return;
2034
2247
  const watchers = new Map();
2248
+ let recursiveMode = false;
2035
2249
 
2036
- const IGNORE_DIRS = new Set([
2037
- "node_modules",
2038
- "dist",
2039
- "build",
2040
- "out",
2041
- "coverage",
2042
- "target",
2043
- ".git",
2044
- ".vercel",
2045
- ".turbo",
2046
- ".swift-rust",
2047
- ".next",
2048
- ]);
2250
+ // ── Debounced batch flush ──────────────────────────────────────────────────
2251
+ // All events funnel into one pending set; a flush invalidates caches once,
2252
+ // syntax-checks the batch, and broadcasts a single reload (or error).
2253
+ const pendingFiles = new Set();
2254
+ let pendingWildcard = false; // event with no usable filename → invalidate broadly
2255
+ let flushTimer = null;
2256
+ let flushing = false;
2257
+ let lastFlushErrored = false;
2258
+
2259
+ function scheduleFlush() {
2260
+ if (flushTimer) clearTimeout(flushTimer);
2261
+ flushTimer = setTimeout(() => {
2262
+ flushTimer = null;
2263
+ void flush();
2264
+ }, 80);
2265
+ }
2266
+
2267
+ function noteChange(full) {
2268
+ pendingFiles.add(full);
2269
+ scheduleFlush();
2270
+ }
2271
+
2272
+ function noteWildcard() {
2273
+ pendingWildcard = true;
2274
+ scheduleFlush();
2275
+ }
2276
+
2277
+ async function flush() {
2278
+ if (flushing) {
2279
+ scheduleFlush();
2280
+ return;
2281
+ }
2282
+ flushing = true;
2283
+ try {
2284
+ const files = [...pendingFiles];
2285
+ pendingFiles.clear();
2286
+ const wildcard = pendingWildcard;
2287
+ pendingWildcard = false;
2288
+ if (!files.length && !wildcard) return;
2289
+
2290
+ buildGeneration++;
2291
+ for (const full of files) {
2292
+ logEvent("change", full);
2293
+ bustCache(full);
2294
+ if (full.endsWith(`${sep}layout.tsx`) || full.endsWith(`${sep}layout.ts`)) {
2295
+ fontsScannedFromLayout = false;
2296
+ GOOGLE_FONT_FAMILIES.clear();
2297
+ }
2298
+ }
2299
+ // Any source edit can introduce new utility classes (Tailwind scans the
2300
+ // source tree), so re-run the CSS pipeline on every flush — not only when
2301
+ // globals.css itself changes. mtime caching keeps subsequent requests hot.
2302
+ globalsCssCache = { file: null, mtime: 0, css: "" };
2303
+ if (wildcard) {
2304
+ fontsScannedFromLayout = false;
2305
+ GOOGLE_FONT_FAMILIES.clear();
2306
+ }
2307
+
2308
+ const syntaxError = await findSyntaxError(files);
2309
+ if (syntaxError) {
2310
+ // Overlay on the live page instead of reloading into a dead 500 —
2311
+ // page state survives, and the next clean save reloads normally.
2312
+ lastFlushErrored = true;
2313
+ logLine([` ${paint("red", "✗")} ${paint("red", syntaxError.split("\n")[0])}`]);
2314
+ broadcastHmr({ type: "error", message: syntaxError });
2315
+ return;
2316
+ }
2317
+ if (lastFlushErrored) {
2318
+ lastFlushErrored = false;
2319
+ broadcastHmr({ type: "ok" });
2320
+ }
2321
+ broadcastHmr({ type: "reload", file: relative(cwd, files[0] ?? cwd), at: Date.now() });
2322
+ logHmr(files[0] ?? cwd);
2323
+ } finally {
2324
+ flushing = false;
2325
+ if (pendingFiles.size || pendingWildcard) scheduleFlush();
2326
+ }
2327
+ }
2328
+
2329
+ function handleEvent(full) {
2330
+ try {
2331
+ if (statSync(full).isDirectory()) {
2332
+ // New directory: in fallback mode start watching it. Files may have
2333
+ // been created inside it before the watcher attached, so invalidate
2334
+ // broadly instead of silently dropping the event (the old behavior —
2335
+ // one dropped event meant stale renders until the server restarted).
2336
+ if (!recursiveMode) walk(full);
2337
+ noteWildcard();
2338
+ return;
2339
+ }
2340
+ } catch {}
2341
+ noteChange(full);
2342
+ }
2343
+
2344
+ function onWatchEvent(baseDir) {
2345
+ return (event, filename) => {
2346
+ if (!filename) {
2347
+ // macOS/FSEvents can coalesce events and drop the filename. Treat it
2348
+ // as "something changed" rather than ignoring it.
2349
+ noteWildcard();
2350
+ return;
2351
+ }
2352
+ const full = join(baseDir, filename.toString());
2353
+ if (isIgnoredWatchPath(full)) return;
2354
+ handleEvent(full);
2355
+ };
2356
+ }
2357
+
2358
+ // Fallback: one watcher per directory (non-recursive platforms).
2049
2359
  function walk(dir) {
2050
2360
  if (watchers.has(dir)) return;
2361
+ // A probe child path tells us whether the directory itself is ignored
2362
+ // (cwd resolves to rel "x", which passes).
2363
+ if (isIgnoredWatchPath(join(dir, "x"))) return;
2051
2364
  try {
2052
2365
  const entries = readdirSync(dir, { withFileTypes: true });
2053
2366
  for (const e of entries) {
2054
- if (e.isDirectory() && !e.name.startsWith(".") && !IGNORE_DIRS.has(e.name)) {
2367
+ if (e.isDirectory() && !e.name.startsWith(".") && !WATCH_IGNORE_DIRS.has(e.name)) {
2055
2368
  walk(join(dir, e.name));
2056
2369
  }
2057
2370
  }
2058
- let debounce;
2059
- const w = fsWatch(dir, (event, filename) => {
2060
- if (shouldIgnoreFile(filename)) return;
2061
- const full = join(dir, filename.toString());
2062
- if (debounce) clearTimeout(debounce);
2063
- debounce = setTimeout(() => {
2064
- try {
2065
- const s = statSync(full);
2066
- if (s.isDirectory()) {
2067
- walk(full);
2068
- return;
2069
- }
2070
- } catch {}
2071
- logEvent("change", full);
2072
- buildGeneration++;
2073
- bustCache(full);
2074
- if (full.includes(`${sep}globals.${"css"}`)) {
2075
- globalsCssCache = { file: null, mtime: 0, css: "" };
2076
- }
2077
- if (full.endsWith(`${sep}layout.tsx`) || full.endsWith(`${sep}layout.ts`)) {
2078
- fontsScannedFromLayout = false;
2079
- GOOGLE_FONT_FAMILIES.clear();
2080
- }
2081
- const payload = { type: "reload", file: relative(cwd, full), at: Date.now() };
2082
- for (const send of hmrClients) {
2083
- try {
2084
- send(JSON.stringify({ event: "change", data: payload }));
2085
- } catch {}
2086
- }
2087
- logHmr(full);
2088
- }, 30);
2089
- });
2371
+ const w = fsWatch(dir, onWatchEvent(dir));
2090
2372
  watchers.set(dir, w);
2091
2373
  } catch (err) {
2092
2374
  logLine([` ${paint("dim", "watch error:")} ${paint("red", err.message)}`], 1);
2093
2375
  }
2094
2376
  }
2377
+
2095
2378
  // Watch the WHOLE project (minus dep/build dirs) so a change to *any* source
2096
2379
  // file — pages, components, lib, hooks, content, config, wherever — triggers
2097
- // a recompile + reload. No more "edit this folder and nothing happens".
2098
- walk(cwd);
2380
+ // a recompile + reload. Prefer a single recursive watcher (macOS, Windows,
2381
+ // Linux on Node 20+/Bun): hundreds of per-directory FSEvents watchers could
2382
+ // hit OS limits and die silently, leaving saves unnoticed until a restart.
2383
+ try {
2384
+ const w = fsWatch(cwd, { recursive: true }, onWatchEvent(cwd));
2385
+ watchers.set(cwd, w);
2386
+ recursiveMode = true;
2387
+ } catch {
2388
+ walk(cwd);
2389
+ }
2099
2390
  }
2100
2391
 
2101
2392
  const networkUrls = [];
@@ -2479,6 +2770,35 @@ async function handleFetch(req) {
2479
2770
  return new Response("// island not found", { status: 404, headers: { "Content-Type": "application/javascript" } });
2480
2771
  }
2481
2772
 
2773
+ // On-demand revalidation: POST /_swift-rust/revalidate { tag?, path? }
2774
+ // Purges the shared data cache (globalThis.__SR_CACHE__) by tag or path so
2775
+ // webhooks / mutations can invalidate without a redeploy.
2776
+ if (pathname === "/_swift-rust/revalidate") {
2777
+ if (req.method !== "POST") {
2778
+ return new Response(JSON.stringify({ error: "use POST" }), {
2779
+ status: 405,
2780
+ headers: { "Content-Type": "application/json" },
2781
+ });
2782
+ }
2783
+ let body = {};
2784
+ try { body = await req.json(); } catch {}
2785
+ const tag = body.tag ?? url.searchParams.get("tag");
2786
+ const path = body.path ?? url.searchParams.get("path");
2787
+ const store = globalThis.__SR_CACHE__;
2788
+ let purged = 0;
2789
+ if (store?.store) {
2790
+ for (const [k, v] of store.store) {
2791
+ if ((tag && v.tags?.includes(tag)) || (path && k.includes(path))) {
2792
+ store.store.delete(k);
2793
+ purged++;
2794
+ }
2795
+ }
2796
+ if (tag) store.purged?.add(`tag:${tag}`);
2797
+ if (path) store.purged?.add(`path:${path}`);
2798
+ }
2799
+ return Response.json({ revalidated: true, tag: tag ?? null, path: path ?? null, purged });
2800
+ }
2801
+
2482
2802
  if (pathname.startsWith("/_swift-rust/")) {
2483
2803
  return new Response("Not found", { status: 404 });
2484
2804
  }
@@ -2749,7 +3069,7 @@ async function handleApiRoute(req, segments, method, reqStart) {
2749
3069
  }
2750
3070
  const handlerName = method.toUpperCase();
2751
3071
  try {
2752
- const mod = await loadModule(route.file, { bust: true });
3072
+ const mod = await loadModuleFresh(route.file);
2753
3073
  const handler = mod[handlerName] || mod[method.toLowerCase()];
2754
3074
  if (typeof handler !== "function") {
2755
3075
  const total = performance.now() - reqStart;
@@ -54,10 +54,22 @@
54
54
  nav.prefetch = (url) => fetchDoc(new URL(url, location.href).href).catch(() => {});
55
55
 
56
56
  // Scripts inserted via DOM cloning don't execute; clone them into fresh nodes.
57
+ // Classic scripts re-run on re-insertion, but ES module scripts are keyed by
58
+ // URL in the module map and won't re-evaluate if the same src is inserted
59
+ // again — which would leave the swapped-in island markup un-hydrated (stale
60
+ // active states, dead event handlers) until a full reload. So we cache-bust
61
+ // module src scripts with a per-navigation token to force re-execution.
62
+ let swapToken = 0;
57
63
  function runScripts(root) {
64
+ swapToken++;
58
65
  for (const old of root.querySelectorAll("script")) {
59
66
  const s = document.createElement("script");
60
67
  for (const att of old.attributes) s.setAttribute(att.name, att.value);
68
+ if (s.type === "module" && s.src) {
69
+ const u = new URL(s.src, location.href);
70
+ u.searchParams.set("__srnav", String(swapToken));
71
+ s.src = u.href;
72
+ }
61
73
  s.textContent = old.textContent;
62
74
  old.replaceWith(s);
63
75
  }
@@ -0,0 +1,37 @@
1
+ export interface CacheEntry<T = unknown> {
2
+ value: T;
3
+ /** epoch ms when the entry goes stale; 0 = never expires (until invalidated). */
4
+ expires: number;
5
+ tags: string[];
6
+ }
7
+ export interface CacheOptions {
8
+ /** Tags for targeted invalidation via revalidateTag(). */
9
+ tags?: string[];
10
+ /** Seconds before the entry is considered stale. Omit/0 = cache until invalidated. */
11
+ revalidate?: number;
12
+ /** Custom key derivation from the call arguments (defaults to fn name + JSON args). */
13
+ key?: (...args: any[]) => string;
14
+ }
15
+ /**
16
+ * Wrap an async function so its result is cached by argument key, with optional
17
+ * TTL and tags. Returns a function with the same signature.
18
+ *
19
+ * const getPosts = cache(fetchPosts, { tags: ["posts"], revalidate: 3600 });
20
+ * await getPosts(); // miss → runs fetchPosts, caches
21
+ * await getPosts(); // hit
22
+ * revalidateTag("posts"); // purge
23
+ */
24
+ export declare function cache<A extends unknown[], R>(fn: (...args: A) => Promise<R> | R, options?: CacheOptions): (...args: A) => Promise<R>;
25
+ /** Invalidate every cached entry tagged with `tag`. */
26
+ export declare function revalidateTag(tag: string): void;
27
+ /**
28
+ * Invalidate cached entries whose key references `path`. (Best-effort by
29
+ * substring — useful for "everything derived from /blog/*".) Also records the
30
+ * path so the CDN/platform can purge the rendered route.
31
+ */
32
+ export declare function revalidatePath(path: string): void;
33
+ /** Drain the set of purge signals recorded since the last call (for endpoints/webhooks). */
34
+ export declare function drainPurged(): string[];
35
+ /** Clear the entire data cache (mainly for tests / dev HMR). */
36
+ export declare function clearCache(): void;
37
+ //# sourceMappingURL=cache.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAaA,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,OAAO;IACrC,KAAK,EAAE,CAAC,CAAC;IACT,iFAAiF;IACjF,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAWD,MAAM,WAAW,YAAY;IAC3B,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,sFAAsF;IACtF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uFAAuF;IAEvF,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,GAAG,EAAE,KAAK,MAAM,CAAC;CAClC;AAED;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CAAC,CAAC,SAAS,OAAO,EAAE,EAAE,CAAC,EAC1C,EAAE,EAAE,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,GAAG,CAAC,EAClC,OAAO,GAAE,YAAiB,GACzB,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAY5B;AAED,uDAAuD;AACvD,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAK/C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAKjD;AAED,4FAA4F;AAC5F,wBAAgB,WAAW,IAAI,MAAM,EAAE,CAItC;AAED,gEAAgE;AAChE,wBAAgB,UAAU,IAAI,IAAI,CAEjC"}
package/dist/cache.js ADDED
@@ -0,0 +1,76 @@
1
+ // Data cache + on-demand revalidation.
2
+ //
3
+ // A single process-wide store (kept on globalThis so the app's `swift-rust`
4
+ // import and the dev server share ONE instance regardless of how the module was
5
+ // resolved). Entries carry an expiry (TTL) and tags; `revalidateTag` /
6
+ // `revalidatePath` purge by tag or key substring.
7
+ //
8
+ // In dev and within a warm production function instance this is a real cache.
9
+ // Across instances/CDN, invalidation also relies on the `x-vercel-cache-tags`
10
+ // response header (emitted by the render pipeline) + the platform's tag purge;
11
+ // `revalidateTag` records the purge so an on-demand endpoint/webhook can act on
12
+ // it. The in-memory store is the fast path; the CDN tags are the durable path.
13
+ const g = globalThis;
14
+ const cacheState = (g.__SR_CACHE__ ??= { store: new Map(), purged: new Set() });
15
+ /**
16
+ * Wrap an async function so its result is cached by argument key, with optional
17
+ * TTL and tags. Returns a function with the same signature.
18
+ *
19
+ * const getPosts = cache(fetchPosts, { tags: ["posts"], revalidate: 3600 });
20
+ * await getPosts(); // miss → runs fetchPosts, caches
21
+ * await getPosts(); // hit
22
+ * revalidateTag("posts"); // purge
23
+ */
24
+ export function cache(fn, options = {}) {
25
+ const tags = options.tags ?? [];
26
+ const ttlMs = options.revalidate ? options.revalidate * 1000 : 0;
27
+ const name = fn.name || "anon";
28
+ return async (...args) => {
29
+ const key = options.key ? `${name}:${options.key(...args)}` : `${name}:${safeStringify(args)}`;
30
+ const hit = cacheState.store.get(key);
31
+ if (hit && (hit.expires === 0 || hit.expires > Date.now()))
32
+ return hit.value;
33
+ const value = await fn(...args);
34
+ cacheState.store.set(key, { value, expires: ttlMs ? Date.now() + ttlMs : 0, tags });
35
+ return value;
36
+ };
37
+ }
38
+ /** Invalidate every cached entry tagged with `tag`. */
39
+ export function revalidateTag(tag) {
40
+ for (const [k, v] of cacheState.store) {
41
+ if (v.tags.includes(tag))
42
+ cacheState.store.delete(k);
43
+ }
44
+ cacheState.purged.add(`tag:${tag}`);
45
+ }
46
+ /**
47
+ * Invalidate cached entries whose key references `path`. (Best-effort by
48
+ * substring — useful for "everything derived from /blog/*".) Also records the
49
+ * path so the CDN/platform can purge the rendered route.
50
+ */
51
+ export function revalidatePath(path) {
52
+ for (const [k] of cacheState.store) {
53
+ if (k.includes(path))
54
+ cacheState.store.delete(k);
55
+ }
56
+ cacheState.purged.add(`path:${path}`);
57
+ }
58
+ /** Drain the set of purge signals recorded since the last call (for endpoints/webhooks). */
59
+ export function drainPurged() {
60
+ const out = [...cacheState.purged];
61
+ cacheState.purged.clear();
62
+ return out;
63
+ }
64
+ /** Clear the entire data cache (mainly for tests / dev HMR). */
65
+ export function clearCache() {
66
+ cacheState.store.clear();
67
+ }
68
+ function safeStringify(value) {
69
+ try {
70
+ return JSON.stringify(value);
71
+ }
72
+ catch {
73
+ return String(value);
74
+ }
75
+ }
76
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cache.js","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -74,6 +74,8 @@ export { Link } from "./link";
74
74
  export { Head, Title, Meta, Style } from "./head";
75
75
  export { notFound, redirect, permanentRedirect } from "./router";
76
76
  export { NotFoundError, RedirectError } from "./router";
77
+ export { cache, revalidateTag, revalidatePath, clearCache } from "./cache";
78
+ export type { CacheOptions, CacheEntry } from "./cache";
77
79
  export { Video, BackgroundVideo, VideoLightbox, isYouTubeUrl, isVimeoUrl, getYouTubeId, getVimeoId, detectProvider, getYouTubeEmbedUrl, getVimeoEmbedUrl, } from "./video";
78
80
  export type { VideoProps, VideoSource, VideoCaption, VideoPreload, VideoProvider, BackgroundVideoProps, VideoLightboxProps, } from "./video";
79
81
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -8,5 +8,6 @@ export { Link } from "./link";
8
8
  export { Head, Title, Meta, Style } from "./head";
9
9
  export { notFound, redirect, permanentRedirect } from "./router";
10
10
  export { NotFoundError, RedirectError } from "./router";
11
+ export { cache, revalidateTag, revalidatePath, clearCache } from "./cache";
11
12
  export { Video, BackgroundVideo, VideoLightbox, isYouTubeUrl, isVimeoUrl, getYouTubeId, getVimeoId, detectProvider, getYouTubeEmbedUrl, getVimeoEmbedUrl, } from "./video";
12
13
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA+BA,MAAM,UAAU,YAAY,CAAC,MAAuB;IAClD,OAAO,MAAM,CAAC;AAChB,CAAC;AAwBD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAW1C,OAAO,EACL,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,mBAAmB,EACnB,uBAAuB,EACvB,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,MAAM,EACN,eAAe,EACf,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,eAAe,EACf,YAAY,EACZ,YAAY,GACb,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAI7D,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EACL,KAAK,EACL,eAAe,EACf,aAAa,EACb,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA+BA,MAAM,UAAU,YAAY,CAAC,MAAuB;IAClD,OAAO,MAAM,CAAC;AAChB,CAAC;AAwBD,OAAO,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAC;AAW1C,OAAO,EACL,SAAS,EACT,QAAQ,EACR,QAAQ,EACR,mBAAmB,EACnB,uBAAuB,EACvB,aAAa,EACb,iBAAiB,EACjB,qBAAqB,EACrB,MAAM,EACN,eAAe,EACf,oBAAoB,EACpB,YAAY,EACZ,cAAc,EACd,eAAe,EACf,YAAY,EACZ,YAAY,GACb,MAAM,kBAAkB,CAAC;AAS1B,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,iBAAiB,CAAC;AAI7D,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,KAAK,EAAE,MAAM,QAAQ,CAAC;AAClD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACxD,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAE3E,OAAO,EACL,KAAK,EACL,eAAe,EACf,aAAa,EACb,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,UAAU,EACV,cAAc,EACd,kBAAkB,EAClB,gBAAgB,GACjB,MAAM,SAAS,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swift-rust",
3
- "version": "1.8.0",
3
+ "version": "1.10.2",
4
4
  "description": "The full-stack React framework powered with Rust + Bun. TSX-first, Rust rendering, 10x faster than Next.js, single binary deploy.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://swift-rust.dev",
@@ -55,6 +55,10 @@
55
55
  "types": "./dist/router.d.ts",
56
56
  "import": "./dist/router.js"
57
57
  },
58
+ "./cache": {
59
+ "types": "./dist/cache.d.ts",
60
+ "import": "./dist/cache.js"
61
+ },
58
62
  "./head": {
59
63
  "types": "./dist/head.d.ts",
60
64
  "import": "./dist/head.js"