bun-plugin-stx 0.2.735 → 0.2.739

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/README.md CHANGED
@@ -55,6 +55,11 @@ like `componentsDir`, whether configured in `stx.config.ts` or passed to
55
55
  update. These roots do not need a duplicate `watchDirs` entry. Missing roots
56
56
  are tolerated; resolution order and app overrides are unchanged.
57
57
 
58
+ Page roots in `patterns` are watched as well. Removing an app override file or
59
+ directory re-resolves the route against the remaining roots, including the
60
+ fallback page's middleware. Recreating the override restores its precedence
61
+ without restarting the development server.
62
+
58
63
  ## Usage with ESM
59
64
 
60
65
  ### 1. Configure Bun to use the plugin
@@ -0,0 +1,47 @@
1
+ /**
2
+ * A digest of everything an entry's signature does not cover. Files are
3
+ * described by path, mtime and size; a missing file is part of the answer
4
+ * too, since creating one (a new `.env.local`) can change what renders.
5
+ */
6
+ export declare function renderInputsFingerprint(inputs: RenderInputs): Promise<string>;
7
+ /** Put this process's build id where the rendering process put its own. */
8
+ export declare function restampBuildId(html: string, renderedWith: string, current: string): string;
9
+ /**
10
+ * One file per entry, read on demand. A dashboard's worth of pages is tens of
11
+ * megabytes of HTML, so nothing is loaded up front and a page nobody opens
12
+ * costs nothing.
13
+ */
14
+ export declare function createRenderCacheStore(options: {
15
+ dir: string
16
+ inputs: () => RenderInputs
17
+ buildId: () => string
18
+ }): RenderCacheStore;
19
+ export declare interface PersistableEntry {
20
+ html: string
21
+ signature: Map<string, number>
22
+ status: number
23
+ headers?: Record<string, string>
24
+ }
25
+ export declare interface RenderInputs {
26
+ roots: readonly string[]
27
+ files: readonly string[]
28
+ extra: unknown
29
+ }
30
+ /**
31
+ * Taken before a render reads anything, and handed back to `write`.
32
+ *
33
+ * The fingerprint is settled first, so it can only be older than what the
34
+ * render read, never newer. An older one no longer matches after the change
35
+ * that made it older, which costs a miss; a newer one would vouch for HTML
36
+ * rendered from files it never described.
37
+ */
38
+ export declare interface RenderTicket {
39
+ generation: number
40
+ fingerprint: string
41
+ }
42
+ export declare interface RenderCacheStore {
43
+ begin: () => Promise<RenderTicket>
44
+ read: (key: string) => Promise<PersistableEntry | undefined>
45
+ write: (key: string, entry: PersistableEntry, ticket: RenderTicket) => Promise<void>
46
+ invalidate: () => void
47
+ }
package/dist/serve.d.ts CHANGED
@@ -361,6 +361,7 @@ export declare interface ServeOptions {
361
361
  watchDirs?: string[]
362
362
  renderCache?: boolean
363
363
  renderCacheVary?: 'request' | 'source'
364
+ renderCachePersist?: boolean
364
365
  renderCacheLimit?: number
365
366
  clientAddress?: (req: Request, server: ServeServer) => string | null | undefined
366
367
  cssCacheLimit?: number
package/dist/serve.js CHANGED
@@ -9,14 +9,14 @@ import {
9
9
  // src/serve.ts
10
10
  var {serve: bunServe, Glob } = globalThis.Bun;
11
11
  import { existsSync as existsSync2, watch as fsWatch, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
12
- import nodeFs from "fs/promises";
13
- import nodePath from "path";
12
+ import nodeFs2 from "fs/promises";
13
+ import nodePath2 from "path";
14
14
  import process20 from "process";
15
15
 
16
16
  // ../../node_modules/bunfig/dist/index.js
17
17
  import { existsSync, statSync } from "fs";
18
18
  import { resolve } from "path";
19
- import process from "process";
19
+ import process2 from "process";
20
20
  import { existsSync as existsSync12, mkdirSync as mkdirSync4, readdirSync as readdirSync4, readFileSync as readFileSync2, writeFileSync as writeFileSync7 } from "fs";
21
21
  import { homedir as homedir3 } from "os";
22
22
  import { dirname as dirname4, resolve as resolve12 } from "path";
@@ -33,7 +33,7 @@ import { homedir } from "os";
33
33
  import { dirname as dirname2, resolve as resolve3 } from "path";
34
34
  import process6 from "process";
35
35
  import { join, relative, resolve as resolve2 } from "path";
36
- import process2 from "process";
36
+ import process22 from "process";
37
37
  import { existsSync as existsSync22, mkdirSync, readdirSync, writeFileSync } from "fs";
38
38
  import { dirname, resolve as resolve5 } from "path";
39
39
  import process3 from "process";
@@ -352,7 +352,7 @@ function getConfigPaths(options) {
352
352
  return patterns.flatMap((pattern) => CONFIG_FILE_EXTENSIONS.map((extension) => resolve(options.directory, `${pattern}${extension}`)));
353
353
  }
354
354
  function getLocalConfigDirectories(options = {}) {
355
- const cwd = resolve(options.cwd || process.cwd());
355
+ const cwd = resolve(options.cwd || process2.cwd());
356
356
  return Array.from(new Set([
357
357
  cwd,
358
358
  resolve(cwd, "config"),
@@ -851,12 +851,12 @@ async function loadConfig({
851
851
  return defaultConfig;
852
852
  }
853
853
  function getProjectRoot(filePath, options = {}) {
854
- let path = process2.cwd();
854
+ let path = process22.cwd();
855
855
  while (path.includes("storage"))
856
856
  path = resolve2(path, "..");
857
857
  const finalPath = resolve2(path, filePath || "");
858
858
  if (options?.relative)
859
- return relative(process2.cwd(), finalPath);
859
+ return relative(process22.cwd(), finalPath);
860
860
  return finalPath;
861
861
  }
862
862
  async function loadConfig2() {
@@ -864,7 +864,7 @@ async function loadConfig2() {
864
864
  const loadedConfig = await loadConfig({
865
865
  name: "clarity",
866
866
  defaultConfig,
867
- cwd: process2.cwd(),
867
+ cwd: process22.cwd(),
868
868
  endpoint: "",
869
869
  headers: {}
870
870
  });
@@ -5389,7 +5389,7 @@ var init_dist = __esm(async () => {
5389
5389
  };
5390
5390
  defaultConfigDir = resolve5(process3.cwd(), "config");
5391
5391
  defaultGeneratedDir = resolve5(process3.cwd(), "src/generated");
5392
- defaultLogDirectory = process2.env.CLARITY_LOG_DIR || join(getProjectRoot(), "logs");
5392
+ defaultLogDirectory = process22.env.CLARITY_LOG_DIR || join(getProjectRoot(), "logs");
5393
5393
  defaultConfig = {
5394
5394
  level: "info",
5395
5395
  defaultName: "clarity",
@@ -8881,10 +8881,39 @@ async function servedTemplatesUseImageBuiltins(root, scope) {
8881
8881
  options.additionalComponentsDirs = options.additionalComponentsDirs.map(absolute);
8882
8882
  const directories = new Map;
8883
8883
  const sources = new Map;
8884
+ const listings = new Map;
8885
+ const isFileCache = new Map;
8886
+ function listing(dir) {
8887
+ let entries = listings.get(dir);
8888
+ if (!entries) {
8889
+ entries = readdir4(dir, { withFileTypes: true }).then((list) => ({ exact: new Map(list.map((entry) => [entry.name, entry])), folded: new Set(list.map((entry) => entry.name.toLowerCase())) }), () => null);
8890
+ listings.set(dir, entries);
8891
+ }
8892
+ return entries;
8893
+ }
8894
+ function isFile(file) {
8895
+ let known = isFileCache.get(file);
8896
+ if (!known) {
8897
+ known = (async () => {
8898
+ const entries = await listing(path.dirname(file));
8899
+ if (!entries)
8900
+ return false;
8901
+ const name = path.basename(file);
8902
+ const entry = entries.exact.get(name);
8903
+ if (entry?.isFile())
8904
+ return true;
8905
+ if (entry?.isSymbolicLink() || !entry && entries.folded.has(name.toLowerCase()))
8906
+ return stat4(file).then((result) => result.isFile(), () => false);
8907
+ return false;
8908
+ })();
8909
+ isFileCache.set(file, known);
8910
+ }
8911
+ return known;
8912
+ }
8884
8913
  async function firstFile(candidates) {
8885
8914
  for (const candidate of candidates) {
8886
8915
  const file = absolute(candidate);
8887
- if (await stat4(file).then((result) => result.isFile(), () => false))
8916
+ if (await isFile(file))
8888
8917
  return file;
8889
8918
  }
8890
8919
  }
@@ -8914,12 +8943,22 @@ async function servedTemplatesUseImageBuiltins(root, scope) {
8914
8943
  }
8915
8944
  return files;
8916
8945
  }
8917
- async function component(name, file, page, imported) {
8946
+ const resolutions = new Map;
8947
+ function component(name, file, page, imported) {
8918
8948
  const pascal = name.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
8919
8949
  for (const alias of [name, name.toLowerCase(), pascal, pascalToKebab(name)]) {
8920
8950
  if (imported.has(alias))
8921
- return imported.get(alias);
8951
+ return Promise.resolve(imported.get(alias));
8952
+ }
8953
+ const key = `${name}\x00${path.dirname(page)}\x00${path.dirname(file)}`;
8954
+ let resolving = resolutions.get(key);
8955
+ if (!resolving) {
8956
+ resolving = resolveComponent(name, pascal, file, page);
8957
+ resolutions.set(key, resolving);
8922
8958
  }
8959
+ return resolving;
8960
+ }
8961
+ async function resolveComponent(name, pascal, file, page) {
8923
8962
  if (name.startsWith("./") || name.startsWith("../"))
8924
8963
  return firstFile([path.resolve(path.dirname(file), name.endsWith(".stx") ? name : `${name}.stx`)]);
8925
8964
  const names = [...new Set([name, pascal, pascalToKebab(name)].map((value) => `${value.replace(/\.stx$/, "")}.stx`))];
@@ -9023,6 +9062,94 @@ async function servedTemplatesUseImageBuiltins(root, scope) {
9023
9062
  }
9024
9063
  return false;
9025
9064
  }
9065
+
9066
+ // src/render-cache-store.ts
9067
+ import nodeFs from "fs/promises";
9068
+ import nodePath from "path";
9069
+ import { createHash } from "crypto";
9070
+ var WATCHED_EXTENSIONS = new Set([".stx", ".md", ".html", ".js", ".ts", ".tsx", ".mts", ".cts", ".json"]);
9071
+ async function renderInputsFingerprint(inputs) {
9072
+ const lines = [];
9073
+ for (const root of new Set(inputs.roots)) {
9074
+ const absolute = nodePath.resolve(root);
9075
+ const entries = await nodeFs.readdir(absolute, { recursive: true, withFileTypes: true }).catch(() => null);
9076
+ if (!entries) {
9077
+ lines.push(`root ${absolute} missing`);
9078
+ continue;
9079
+ }
9080
+ const files = entries.filter((entry) => entry.isFile() && WATCHED_EXTENSIONS.has(nodePath.extname(entry.name))).map((entry) => nodePath.join(entry.parentPath, entry.name)).filter((file) => !nodePath.relative(absolute, nodePath.dirname(file)).split(nodePath.sep).some((part) => part === "node_modules" || part.startsWith(".")));
9081
+ const stats = await Promise.all(files.map((file) => nodeFs.stat(file).catch(() => null)));
9082
+ files.forEach((file, index) => lines.push(`file ${file} ${stats[index]?.mtimeMs ?? "gone"} ${stats[index]?.size ?? 0}`));
9083
+ }
9084
+ for (const file of new Set(inputs.files)) {
9085
+ const stat = await nodeFs.stat(file).catch(() => null);
9086
+ lines.push(`input ${nodePath.resolve(file)} ${stat ? `${stat.mtimeMs} ${stat.size}` : "missing"}`);
9087
+ }
9088
+ lines.sort();
9089
+ lines.push(`extra ${JSON.stringify(inputs.extra)}`);
9090
+ return createHash("sha256").update(lines.join(`
9091
+ `)).digest("hex");
9092
+ }
9093
+ function restampBuildId(html, renderedWith, current) {
9094
+ if (renderedWith === current)
9095
+ return html;
9096
+ const before = `<meta name="stx-build" content="${renderedWith}">`;
9097
+ const at = html.indexOf(before);
9098
+ if (at === -1)
9099
+ return html;
9100
+ return `${html.slice(0, at)}<meta name="stx-build" content="${current}">${html.slice(at + before.length)}`;
9101
+ }
9102
+ function createRenderCacheStore(options) {
9103
+ let generation = 0;
9104
+ let fingerprint = null;
9105
+ const current = () => fingerprint ??= renderInputsFingerprint(options.inputs());
9106
+ const fileFor = (key) => nodePath.join(options.dir, `${createHash("sha256").update(key).digest("hex")}.json`);
9107
+ return {
9108
+ async begin() {
9109
+ const at = generation;
9110
+ return { generation: at, fingerprint: await current() };
9111
+ },
9112
+ async read(key) {
9113
+ const file = fileFor(key);
9114
+ const stored = await Bun.file(file).json().catch(() => null);
9115
+ if (!stored)
9116
+ return;
9117
+ if (stored.v !== 1 || stored.key !== key || stored.fingerprint !== await current()) {
9118
+ await nodeFs.rm(file, { force: true }).catch(() => {});
9119
+ return;
9120
+ }
9121
+ return {
9122
+ html: restampBuildId(stored.html, stored.buildId, options.buildId()),
9123
+ signature: new Map(stored.signature),
9124
+ status: stored.status,
9125
+ headers: stored.headers
9126
+ };
9127
+ },
9128
+ async write(key, entry, ticket) {
9129
+ if (ticket.generation !== generation)
9130
+ return;
9131
+ const stored = {
9132
+ v: 1,
9133
+ key,
9134
+ fingerprint: ticket.fingerprint,
9135
+ buildId: options.buildId(),
9136
+ html: entry.html,
9137
+ signature: [...entry.signature],
9138
+ status: entry.status,
9139
+ headers: entry.headers
9140
+ };
9141
+ await nodeFs.mkdir(options.dir, { recursive: true });
9142
+ const file = fileFor(key);
9143
+ const temporary = `${file}.${process.pid}.tmp`;
9144
+ await Bun.write(temporary, JSON.stringify(stored));
9145
+ await nodeFs.rename(temporary, file);
9146
+ },
9147
+ invalidate() {
9148
+ generation++;
9149
+ fingerprint = null;
9150
+ }
9151
+ };
9152
+ }
9026
9153
  // src/page-middleware.ts
9027
9154
  var DEFAULT_PRIORITY = 10;
9028
9155
  function middlewarePriority(middleware) {
@@ -9262,9 +9389,9 @@ function publicFileExists(requestPath, publicDir) {
9262
9389
  }
9263
9390
  if (decoded.includes("\x00"))
9264
9391
  return false;
9265
- const publicRoot = nodePath.resolve(process20.cwd(), publicDir);
9266
- const resolved = nodePath.resolve(publicRoot, `.${decoded}`);
9267
- const inside = resolved === publicRoot || resolved.startsWith(`${publicRoot}${nodePath.sep}`);
9392
+ const publicRoot = nodePath2.resolve(process20.cwd(), publicDir);
9393
+ const resolved = nodePath2.resolve(publicRoot, `.${decoded}`);
9394
+ const inside = resolved === publicRoot || resolved.startsWith(`${publicRoot}${nodePath2.sep}`);
9268
9395
  if (!inside)
9269
9396
  return false;
9270
9397
  try {
@@ -9418,7 +9545,7 @@ function boundedCache(limit) {
9418
9545
  }
9419
9546
  function pageExcluder(exclude, cwd = process20.cwd()) {
9420
9547
  const canonical = (entry) => {
9421
- const resolved = nodePath.resolve(cwd, entry);
9548
+ const resolved = nodePath2.resolve(cwd, entry);
9422
9549
  try {
9423
9550
  return realpathSync2(resolved);
9424
9551
  } catch {
@@ -9430,7 +9557,7 @@ function pageExcluder(exclude, cwd = process20.cwd()) {
9430
9557
  return () => false;
9431
9558
  return (file) => {
9432
9559
  const resolved = canonical(file);
9433
- return roots.some((root) => resolved === root || resolved.startsWith(`${root}${nodePath.sep}`));
9560
+ return roots.some((root) => resolved === root || resolved.startsWith(`${root}${nodePath2.sep}`));
9434
9561
  };
9435
9562
  }
9436
9563
  function resolveServeOpenUrl(port, openPath = "/") {
@@ -9665,9 +9792,9 @@ async function serve(options) {
9665
9792
  }
9666
9793
  const stxModule = options.stxModule ? Promise.resolve(options.stxModule) : defaultStxModule;
9667
9794
  const imageCacheDir = process20.env.STX_IMAGE_CACHE_DIR?.trim();
9668
- const imageDeliveryOutputDir = imageCacheDir ? nodePath.resolve(imageCacheDir, "image-delivery") : stateDir(process20.cwd(), "image-delivery");
9669
- const imagePlaceholderCachePath = imageCacheDir ? nodePath.resolve(imageCacheDir, "image-placeholders.json") : stateDir(process20.cwd(), "image-placeholders.json");
9670
- const imageDeliveryRoot = nodePath.join(imageDeliveryOutputDir, "_stx", "images");
9795
+ const imageDeliveryOutputDir = imageCacheDir ? nodePath2.resolve(imageCacheDir, "image-delivery") : stateDir(process20.cwd(), "image-delivery");
9796
+ const imagePlaceholderCachePath = imageCacheDir ? nodePath2.resolve(imageCacheDir, "image-placeholders.json") : stateDir(process20.cwd(), "image-placeholders.json");
9797
+ const imageDeliveryRoot = nodePath2.join(imageDeliveryOutputDir, "_stx", "images");
9671
9798
  let resolvedStxModule = null;
9672
9799
  Promise.resolve(stxModule).then((m) => {
9673
9800
  resolvedStxModule = m;
@@ -9694,6 +9821,28 @@ After installing: bun add bun-plugin-stx`);
9694
9821
  const pendingShellRenders = new Map;
9695
9822
  const ENABLE_HTML_CACHE = options.renderCache === true;
9696
9823
  const RENDER_CACHE_VARY = options.renderCacheVary ?? "request";
9824
+ const renderCacheStore = ENABLE_HTML_CACHE && RENDER_CACHE_VARY === "source" && options.renderCachePersist === true ? createRenderCacheStore({ dir: stateDir(process20.cwd(), "render-cache"), inputs: () => renderInputs(), buildId: getBuildId }) : null;
9825
+ async function renderTicket(reqCtx) {
9826
+ if (!renderCacheStore || reqCtx && reqCtx.method !== "GET" && reqCtx.method !== "HEAD")
9827
+ return;
9828
+ return renderCacheStore.begin().catch(() => {
9829
+ return;
9830
+ });
9831
+ }
9832
+ async function readRenderCache(key) {
9833
+ const hit = htmlCache.read(key);
9834
+ if (hit || !renderCacheStore)
9835
+ return hit;
9836
+ const kept = await renderCacheStore.read(key).catch(() => {
9837
+ return;
9838
+ });
9839
+ return kept ? htmlCache.remember(key, kept) : undefined;
9840
+ }
9841
+ function rememberRender(key, entry, ticket) {
9842
+ htmlCache.remember(key, entry);
9843
+ if (renderCacheStore && ticket)
9844
+ renderCacheStore.write(key, entry, ticket).catch(() => {});
9845
+ }
9697
9846
  const cssCssCache = boundedCache(options.cssCacheLimit ?? 512);
9698
9847
  const hmrClients = new Set;
9699
9848
  const hmrEncoder = new TextEncoder;
@@ -9791,14 +9940,22 @@ function __stxOverlay(errs){
9791
9940
  currentBuildErrors = errors;
9792
9941
  broadcastHmr({ type: "build-error", errors });
9793
9942
  }
9794
- const watchersStarted = new Set;
9943
+ const watchersStarted = new Map;
9944
+ const clearRenderCaches = () => {
9945
+ htmlCache.clear();
9946
+ renderCacheStore?.invalidate();
9947
+ cssCssCache.clear();
9948
+ resolvedStxModule?.clearDevCaches?.();
9949
+ resolvedStxModule?.clearStoreCache?.();
9950
+ resolvedStxModule?.clearComposableCache?.();
9951
+ };
9795
9952
  const startWatcher = (dir) => {
9796
9953
  if (production)
9797
9954
  return;
9798
9955
  if (watchersStarted.has(dir))
9799
9956
  return;
9800
9957
  try {
9801
- const watcher = fsWatch(dir, { recursive: true }, (_event, filename) => {
9958
+ const watcher = fsWatch(dir, { recursive: true }, (event, filename) => {
9802
9959
  if (!filename)
9803
9960
  return;
9804
9961
  const f = String(filename);
@@ -9807,22 +9964,19 @@ function __stxOverlay(errs){
9807
9964
  const isData = f.endsWith(".json");
9808
9965
  const isCss = f.endsWith(".css");
9809
9966
  const isAsset = isCss || isCode || f.endsWith(".jpg") || f.endsWith(".jpeg") || f.endsWith(".png") || f.endsWith(".gif") || f.endsWith(".svg") || f.endsWith(".webp") || f.endsWith(".avif") || f.endsWith(".woff") || f.endsWith(".woff2") || f.endsWith(".ttf") || f.endsWith(".otf");
9810
- if (!isStxLike && !isAsset && !isData)
9967
+ const topologyChanged = event === "rename";
9968
+ if (!isStxLike && !isAsset && !isData && !topologyChanged)
9811
9969
  return;
9812
- if (isStxLike || isCode || isData) {
9813
- if (isStxLike)
9970
+ if (isStxLike || isCode || isData || topologyChanged) {
9971
+ if (isStxLike || topologyChanged)
9814
9972
  sourceFiles = null;
9815
- htmlCache.clear();
9816
- cssCssCache.clear();
9817
- resolvedStxModule?.clearDevCaches?.();
9818
- resolvedStxModule?.clearStoreCache?.();
9819
- resolvedStxModule?.clearComposableCache?.();
9820
- if (layoutsDir && f.endsWith(".stx") && nodePath.resolve(f).startsWith(nodePath.resolve(layoutsDir)))
9973
+ clearRenderCaches();
9974
+ if (layoutsDir && f.endsWith(".stx") && nodePath2.resolve(f).startsWith(nodePath2.resolve(layoutsDir)))
9821
9975
  writeLayoutTypes();
9822
9976
  }
9823
- const resolved = nodePath.resolve(dir, f);
9824
- const isStore = resolved.startsWith(nodePath.resolve(storesDir)) || composableDirs.some((d) => resolved.startsWith(nodePath.resolve(d)));
9825
- const isLayout = !!layoutsDir && resolved.startsWith(nodePath.resolve(layoutsDir));
9977
+ const resolved = nodePath2.resolve(dir, f);
9978
+ const isStore = resolved.startsWith(nodePath2.resolve(storesDir)) || composableDirs.some((d) => resolved.startsWith(nodePath2.resolve(d)));
9979
+ const isLayout = !!layoutsDir && resolved.startsWith(nodePath2.resolve(layoutsDir));
9826
9980
  if (isCss && !isStxLike)
9827
9981
  broadcastHmr({ type: "css", file: f });
9828
9982
  else if (isStore)
@@ -9833,7 +9987,30 @@ function __stxOverlay(errs){
9833
9987
  broadcastHmr({ type: "reload", file: f });
9834
9988
  });
9835
9989
  watcher.on("error", () => {});
9836
- watchersStarted.add(dir);
9990
+ watchersStarted.set(dir, watcher);
9991
+ } catch {}
9992
+ };
9993
+ const pageRootWatchersStarted = new Set;
9994
+ const watchPageRoot = (dir) => {
9995
+ if (production || pageRootWatchersStarted.has(dir))
9996
+ return;
9997
+ const absolute = nodePath2.resolve(dir);
9998
+ const parent = nodePath2.dirname(absolute);
9999
+ if (parent === absolute)
10000
+ return;
10001
+ try {
10002
+ const watcher = fsWatch(parent, { recursive: false }, (event, filename) => {
10003
+ if (event !== "rename" || String(filename) !== nodePath2.basename(absolute))
10004
+ return;
10005
+ watchersStarted.get(dir)?.close();
10006
+ watchersStarted.delete(dir);
10007
+ startWatcher(dir);
10008
+ sourceFiles = null;
10009
+ clearRenderCaches();
10010
+ broadcastHmr({ type: "reload", file: dir });
10011
+ });
10012
+ watcher.on("error", () => {});
10013
+ pageRootWatchersStarted.add(dir);
9837
10014
  } catch {}
9838
10015
  };
9839
10016
  const writeLayoutTypes = async () => {
@@ -9843,9 +10020,9 @@ function __stxOverlay(errs){
9843
10020
  } catch {}
9844
10021
  };
9845
10022
  await writeLayoutTypes();
9846
- const storesDir = nodePath.resolve(stxConfig.root || ".", stxConfig.storesDir || "stores");
9847
- const composableDirs = stxConfig.composablesDir ? [nodePath.resolve(stxConfig.root || ".", stxConfig.composablesDir)] : ["composables", "functions"].map((d) => nodePath.resolve(stxConfig.root || ".", d));
9848
- for (const dir of [
10023
+ const storesDir = nodePath2.resolve(stxConfig.root || ".", stxConfig.storesDir || "stores");
10024
+ const composableDirs = stxConfig.composablesDir ? [nodePath2.resolve(stxConfig.root || ".", stxConfig.composablesDir)] : ["composables", "functions"].map((d) => nodePath2.resolve(stxConfig.root || ".", d));
10025
+ const watchedRoots = [
9849
10026
  componentsDir,
9850
10027
  ...additionalComponentsDirs ?? [],
9851
10028
  layoutsDir,
@@ -9855,10 +10032,46 @@ function __stxOverlay(errs){
9855
10032
  storesDir,
9856
10033
  ...composableDirs,
9857
10034
  ...options.watchDirs ?? []
9858
- ]) {
9859
- if (!dir)
9860
- continue;
10035
+ ].filter((dir) => !!dir);
10036
+ for (const dir of watchedRoots)
9861
10037
  startWatcher(dir);
10038
+ function renderInputs() {
10039
+ const cwd = process20.cwd();
10040
+ const configFiles = ["stx", "css"].flatMap((name) => [
10041
+ `${name}.config.ts`,
10042
+ `${name}.config.js`,
10043
+ `.config/${name}.ts`,
10044
+ `config/${name}.ts`
10045
+ ]);
10046
+ let stxEntry;
10047
+ try {
10048
+ stxEntry = Bun.resolveSync("@stacksjs/stx", import.meta.dir);
10049
+ } catch {}
10050
+ return {
10051
+ roots: [...watchedRoots, ...patterns.filter((pattern) => !pattern.includes("*"))],
10052
+ files: [
10053
+ ...["package.json", "bun.lock", "bun.lockb", "pantry.lock", ".env", ".env.local", ".env.development", ".env.production", ...configFiles].map((file) => nodePath2.join(cwd, file)),
10054
+ import.meta.path,
10055
+ ...stxEntry ? [stxEntry] : []
10056
+ ],
10057
+ extra: {
10058
+ bun: Bun.version,
10059
+ production,
10060
+ options: {
10061
+ patterns,
10062
+ root: cwd,
10063
+ componentsDir,
10064
+ additionalComponentsDirs,
10065
+ layoutsDir,
10066
+ fallbackLayoutsDir: options.fallbackLayoutsDir,
10067
+ fallbackComponentsDir: options.fallbackComponentsDir,
10068
+ partialsDir,
10069
+ publicDir,
10070
+ watchDirs: options.watchDirs,
10071
+ domains: options.domains
10072
+ }
10073
+ }
10074
+ };
9862
10075
  }
9863
10076
  const pageMiddlewareByPath = new Map;
9864
10077
  const pageMiddlewarePatterns = [];
@@ -9897,7 +10110,7 @@ function __stxOverlay(errs){
9897
10110
  }
9898
10111
  async function detectPageMiddleware(file) {
9899
10112
  try {
9900
- const src = await nodeFs.readFile(file, "utf-8");
10113
+ const src = await nodeFs2.readFile(file, "utf-8");
9901
10114
  const meta = src.match(/definePageMeta\s*\(\s*\{[\s\S]*?\}\s*\)/);
9902
10115
  if (!meta)
9903
10116
  return [];
@@ -10137,7 +10350,9 @@ function __stxOverlay(errs){
10137
10350
  const supportedExtensions = [".stx", ".md", ".html"];
10138
10351
  for (const pattern of patterns) {
10139
10352
  try {
10140
- const stat = await nodeFs.stat(pattern).catch(() => null);
10353
+ const stat = await nodeFs2.stat(pattern).catch(() => null);
10354
+ if (stat?.isDirectory() || !stat && !pattern.includes("*") && !supportedExtensions.some((ext) => pattern.endsWith(ext)))
10355
+ watchPageRoot(pattern);
10141
10356
  if (stat?.isDirectory()) {
10142
10357
  startWatcher(pattern);
10143
10358
  const excludeDirs = ["layouts", "components", "partials"];
@@ -10173,7 +10388,7 @@ function __stxOverlay(errs){
10173
10388
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
10174
10389
  const router = new Router(process20.cwd(), { pagesDirs, stateDir: stateDirName(), exclude: options.exclude });
10175
10390
  if (!options.quiet)
10176
- console.log(`[stx] Generated ${router.routes.length} routes \u2192 ${nodePath.join(stateDirName(), "routes.ts")}`);
10391
+ console.log(`[stx] Generated ${router.routes.length} routes \u2192 ${nodePath2.join(stateDirName(), "routes.ts")}`);
10177
10392
  } catch (e) {}
10178
10393
  return kept;
10179
10394
  }
@@ -10184,16 +10399,16 @@ function __stxOverlay(errs){
10184
10399
  const assetsDir = "./resources/assets";
10185
10400
  const targetAssetsDir = stateDir(process20.cwd(), "assets");
10186
10401
  try {
10187
- const assetsExist = await nodeFs.stat(assetsDir).then(() => true).catch(() => false);
10402
+ const assetsExist = await nodeFs2.stat(assetsDir).then(() => true).catch(() => false);
10188
10403
  if (assetsExist) {
10189
- await nodeFs.rm(targetAssetsDir, { recursive: true, force: true });
10190
- await nodeFs.cp(assetsDir, targetAssetsDir, { recursive: true });
10404
+ await nodeFs2.rm(targetAssetsDir, { recursive: true, force: true });
10405
+ await nodeFs2.cp(assetsDir, targetAssetsDir, { recursive: true });
10191
10406
  }
10192
10407
  } catch {}
10193
10408
  }
10194
10409
  async function templateSignatureFresh(sig) {
10195
10410
  for (const [p, expected] of sig) {
10196
- const stat = await nodeFs.stat(p).catch(() => null);
10411
+ const stat = await nodeFs2.stat(p).catch(() => null);
10197
10412
  if (!stat || stat.mtimeMs !== expected)
10198
10413
  return false;
10199
10414
  }
@@ -10202,7 +10417,7 @@ function __stxOverlay(errs){
10202
10417
  async function buildTemplateSignature(filePath, deps) {
10203
10418
  const sig = new Map;
10204
10419
  const allPaths = [filePath, ...deps];
10205
- const stats = await Promise.all(allPaths.map((p) => nodeFs.stat(p).catch(() => null)));
10420
+ const stats = await Promise.all(allPaths.map((p) => nodeFs2.stat(p).catch(() => null)));
10206
10421
  for (let i = 0;i < allPaths.length; i++) {
10207
10422
  const stat = stats[i];
10208
10423
  if (stat)
@@ -10218,10 +10433,10 @@ function __stxOverlay(errs){
10218
10433
  return cssModule;
10219
10434
  cssLoadAttempted = true;
10220
10435
  let dir = process20.cwd();
10221
- while (dir !== nodePath.dirname(dir)) {
10436
+ while (dir !== nodePath2.dirname(dir)) {
10222
10437
  for (const store of ["node_modules", "pantry"]) {
10223
10438
  for (const entry of ENGINE_PACKAGE_ENTRIES) {
10224
- const candidate = nodePath.join(dir, store, ...entry);
10439
+ const candidate = nodePath2.join(dir, store, ...entry);
10225
10440
  try {
10226
10441
  if (!await Bun.file(candidate).exists())
10227
10442
  continue;
@@ -10233,7 +10448,7 @@ function __stxOverlay(errs){
10233
10448
  } catch {}
10234
10449
  }
10235
10450
  }
10236
- dir = nodePath.dirname(dir);
10451
+ dir = nodePath2.dirname(dir);
10237
10452
  }
10238
10453
  for (const specifier of ENGINE_SPECIFIERS) {
10239
10454
  try {
@@ -10245,7 +10460,7 @@ function __stxOverlay(errs){
10245
10460
  } catch {}
10246
10461
  }
10247
10462
  try {
10248
- const localPath = nodePath.join(process20.env.HOME || "", "Code/Tools/crosswind/packages/toolkit/src/engine/index.ts");
10463
+ const localPath = nodePath2.join(process20.env.HOME || "", "Code/Tools/crosswind/packages/toolkit/src/engine/index.ts");
10249
10464
  const mod = await import(localPath);
10250
10465
  cssModule = { CSSGenerator: mod.CSSGenerator, config: mod.config };
10251
10466
  return cssModule;
@@ -10375,6 +10590,7 @@ function __stxOverlay(errs){
10375
10590
  reqCtx.responseHeaders = { ...reqCtx.responseHeaders, ...headers };
10376
10591
  }
10377
10592
  async function processTemplate(filePath, reqCtx) {
10593
+ const ticket = await renderTicket(reqCtx);
10378
10594
  const content = await Bun.file(filePath).text();
10379
10595
  if (reqCtx)
10380
10596
  reqCtx.responseStatus = extractPageResponseStatus(content) ?? 200;
@@ -10382,7 +10598,7 @@ function __stxOverlay(errs){
10382
10598
  const isMutating = !!reqCtx && reqCtx.method !== "GET" && reqCtx.method !== "HEAD";
10383
10599
  if (ENABLE_HTML_CACHE && !skipCacheHint && !isMutating) {
10384
10600
  const cacheKey = htmlCacheKey(filePath, reqCtx);
10385
- const cachedEntry = htmlCache.read(cacheKey);
10601
+ const cachedEntry = await readRenderCache(cacheKey);
10386
10602
  if (cachedEntry && await templateSignatureFresh(cachedEntry.signature)) {
10387
10603
  if (reqCtx) {
10388
10604
  reqCtx.responseStatus = cachedEntry.status;
@@ -10394,7 +10610,7 @@ function __stxOverlay(errs){
10394
10610
  const { serverScripts, templateContent } = extractServerScriptsFromTemplate(content);
10395
10611
  const context = {
10396
10612
  __filename: filePath,
10397
- __dirname: nodePath.dirname(filePath),
10613
+ __dirname: nodePath2.dirname(filePath),
10398
10614
  __stx_options: stxConfig,
10399
10615
  __stx_runtime_head: {}
10400
10616
  };
@@ -10461,7 +10677,7 @@ function __stxOverlay(errs){
10461
10677
  }
10462
10678
  if (ENABLE_HTML_CACHE && !context.__stx_skip_cache && !skipCacheHint && !isMutating && isRenderableCacheCandidate(output) && placeholdersAreReady) {
10463
10679
  const signature = await buildTemplateSignature(filePath, dependencies);
10464
- htmlCache.remember(htmlCacheKey(filePath, reqCtx), { html: output, signature, status: reqCtx?.responseStatus ?? 200, headers: reqCtx?.responseHeaders });
10680
+ rememberRender(htmlCacheKey(filePath, reqCtx), { html: output, signature, status: reqCtx?.responseStatus ?? 200, headers: reqCtx?.responseHeaders }, ticket);
10465
10681
  }
10466
10682
  return output;
10467
10683
  }
@@ -10483,7 +10699,7 @@ function __stxOverlay(errs){
10483
10699
  possibleFiles.push(`${normalizedPath}/index.md`);
10484
10700
  possibleFiles.push(`${normalizedPath}/index.html`);
10485
10701
  }
10486
- const filename = nodePath.basename(normalizedPath, nodePath.extname(normalizedPath));
10702
+ const filename = nodePath2.basename(normalizedPath, nodePath2.extname(normalizedPath));
10487
10703
  if (filename && !normalizedPath.includes("/")) {
10488
10704
  possibleFiles.push(`${filename}.stx`);
10489
10705
  possibleFiles.push(`${filename}.md`);
@@ -10565,6 +10781,7 @@ function __stxOverlay(errs){
10565
10781
  return null;
10566
10782
  }
10567
10783
  async function processTemplateDynamic(filePath, paramNames, paramValues, routePath, reqCtx) {
10784
+ const ticket = await renderTicket(reqCtx);
10568
10785
  const content = await Bun.file(filePath).text();
10569
10786
  if (reqCtx)
10570
10787
  reqCtx.responseStatus = extractPageResponseStatus(content) ?? 200;
@@ -10583,7 +10800,7 @@ function __stxOverlay(errs){
10583
10800
  if (reqCtx)
10584
10801
  reqCtx.params = paramsObj;
10585
10802
  if (cacheShell) {
10586
- const cachedEntry = htmlCache.read(filePath);
10803
+ const cachedEntry = await readRenderCache(filePath);
10587
10804
  if (cachedEntry && await templateSignatureFresh(cachedEntry.signature)) {
10588
10805
  if (reqCtx) {
10589
10806
  reqCtx.responseStatus = cachedEntry.status;
@@ -10606,7 +10823,7 @@ function __stxOverlay(errs){
10606
10823
  try {
10607
10824
  const context = {
10608
10825
  __filename: filePath,
10609
- __dirname: nodePath.dirname(filePath),
10826
+ __dirname: nodePath2.dirname(filePath),
10610
10827
  __route: routePath,
10611
10828
  __stx_runtime_head: {}
10612
10829
  };
@@ -10678,7 +10895,7 @@ ${output}`;
10678
10895
  const recordedResponse = readResponseStatus(context) !== undefined || !!readResponseHeaders(context);
10679
10896
  if (!context.__stx_skip_cache && !recordedResponse && isRenderableCacheCandidate(output) && placeholdersAreReady) {
10680
10897
  const signature = await buildTemplateSignature(filePath, dependencies);
10681
- htmlCache.remember(filePath, { html: output, signature, status: reqCtx?.responseStatus ?? 200, headers: reqCtx?.responseHeaders });
10898
+ rememberRender(filePath, { html: output, signature, status: reqCtx?.responseStatus ?? 200, headers: reqCtx?.responseHeaders }, ticket);
10682
10899
  }
10683
10900
  return fillRouteParams(output, paramsObj);
10684
10901
  } finally {
@@ -10842,7 +11059,7 @@ ${block}`;
10842
11059
  }
10843
11060
  try {
10844
11061
  const stx = await stxModule;
10845
- const publicRoot = nodePath.resolve(process20.cwd(), publicDir);
11062
+ const publicRoot = nodePath2.resolve(process20.cwd(), publicDir);
10846
11063
  const [placeholderResult, deliveryResult] = await Promise.allSettled([
10847
11064
  stx.warmImagePlaceholders(publicRoot, {
10848
11065
  cachePath: imagePlaceholderCachePath,
@@ -11077,8 +11294,8 @@ ${block}`;
11077
11294
  }
11078
11295
  if (!decodedPath || decodedPath.includes("\x00"))
11079
11296
  return new Response("Invalid image path", { status: 400 });
11080
- const resolvedPath = nodePath.resolve(imageDeliveryRoot, decodedPath);
11081
- if (!resolvedPath.startsWith(`${imageDeliveryRoot}${nodePath.sep}`))
11297
+ const resolvedPath = nodePath2.resolve(imageDeliveryRoot, decodedPath);
11298
+ if (!resolvedPath.startsWith(`${imageDeliveryRoot}${nodePath2.sep}`))
11082
11299
  return new Response("Invalid image path", { status: 400 });
11083
11300
  try {
11084
11301
  const file = Bun.file(resolvedPath);
@@ -11140,6 +11357,7 @@ ${block}`;
11140
11357
  if (hookResult && typeof hookResult === "object")
11141
11358
  Object.assign(reqCtx, hookResult);
11142
11359
  }
11360
+ await discoverFiles();
11143
11361
  const route = resolveRouteMiddleware(path);
11144
11362
  const extraNames = extraProtectedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : `${p}/`)) ? ["auth"] : [];
11145
11363
  const requested = [...globalMiddlewareNames, ...route.names, ...extraNames];
@@ -11221,7 +11439,7 @@ ${block}`;
11221
11439
  }
11222
11440
  const body = await req.json();
11223
11441
  const { Database } = await import("bun:sqlite");
11224
- const dbPath = nodePath.resolve(process20.cwd(), "database/stacks.sqlite");
11442
+ const dbPath = nodePath2.resolve(process20.cwd(), "database/stacks.sqlite");
11225
11443
  const db = new Database(dbPath);
11226
11444
  try {
11227
11445
  const tableInfo = db.query(`PRAGMA table_info("${tableName}")`).all();
@@ -11514,7 +11732,7 @@ ${trailingPageScripts.join(`
11514
11732
  return new Response("Invalid asset path", { status: 400 });
11515
11733
  for (const assetPath of assetRequestPaths(assetPathname)) {
11516
11734
  try {
11517
- const filePath = nodePath.resolve(process20.cwd(), `.${assetPath}`);
11735
+ const filePath = nodePath2.resolve(process20.cwd(), `.${assetPath}`);
11518
11736
  const file = Bun.file(filePath);
11519
11737
  if (await file.exists()) {
11520
11738
  const ext = getAssetExtension(assetPath);
@@ -11533,7 +11751,7 @@ ${trailingPageScripts.join(`
11533
11751
  }
11534
11752
  }
11535
11753
  if ((req.method === "GET" || req.method === "HEAD") && path !== "/") {
11536
- const publicRoot = nodePath.resolve(process20.cwd(), publicDir);
11754
+ const publicRoot = nodePath2.resolve(process20.cwd(), publicDir);
11537
11755
  let safePathname;
11538
11756
  try {
11539
11757
  safePathname = decodeURIComponent(path);
@@ -11541,8 +11759,8 @@ ${trailingPageScripts.join(`
11541
11759
  safePathname = path;
11542
11760
  }
11543
11761
  if (!safePathname.includes("\x00")) {
11544
- const resolvedPath = nodePath.resolve(publicRoot, `.${safePathname}`);
11545
- const isInsidePublicRoot = resolvedPath === publicRoot || resolvedPath.startsWith(`${publicRoot}${nodePath.sep}`);
11762
+ const resolvedPath = nodePath2.resolve(publicRoot, `.${safePathname}`);
11763
+ const isInsidePublicRoot = resolvedPath === publicRoot || resolvedPath.startsWith(`${publicRoot}${nodePath2.sep}`);
11546
11764
  if (isInsidePublicRoot) {
11547
11765
  try {
11548
11766
  const file = Bun.file(resolvedPath);
@@ -11823,7 +12041,7 @@ async function projectUsesImageBuiltins(root, scope) {
11823
12041
  for (const entry of entries) {
11824
12042
  if (entry.name.startsWith("."))
11825
12043
  continue;
11826
- const full = nodePath.join(dir, entry.name);
12044
+ const full = nodePath2.join(dir, entry.name);
11827
12045
  if (entry.isDirectory()) {
11828
12046
  if (!TEMPLATE_SCAN_SKIP.has(entry.name))
11829
12047
  pending.push(full);
@@ -11837,7 +12055,7 @@ async function projectUsesImageBuiltins(root, scope) {
11837
12055
  } catch {
11838
12056
  continue;
11839
12057
  }
11840
- const inComponents = nodePath.relative(root, dir).split(nodePath.sep).includes("components");
12058
+ const inComponents = nodePath2.relative(root, dir).split(nodePath2.sep).includes("components");
11841
12059
  const component = inComponents ? entry.name.slice(0, -".stx".length) : undefined;
11842
12060
  if (!component && IMAGE_BUILTIN_PATTERN.test(source))
11843
12061
  return true;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.735",
4
+ "version": "0.2.739",
5
5
  "description": "A Bun plugin that allows for using Laravel Blade-like syntax.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",
@@ -74,7 +74,7 @@
74
74
  "typecheck": "bun tsc --noEmit"
75
75
  },
76
76
  "dependencies": {
77
- "@stacksjs/stx": "0.2.735",
78
- "stx-router": "0.2.735"
77
+ "@stacksjs/stx": "0.2.739",
78
+ "stx-router": "0.2.739"
79
79
  }
80
80
  }