bun-plugin-stx 0.2.65 → 0.2.66

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.
@@ -913,7 +913,18 @@ else {
913
913
  // re-execute because each page has its own setup.
914
914
  var h=hashScript(code);
915
915
  var isSetup=code.indexOf('__stx_setup_')!==-1;
916
- if(!isSetup&&executedScriptHashes[h]){
916
+ // Compute isAlreadyScoped first so the dedup check can exempt
917
+ // self-contained IIFE scripts. data-stx-scoped scope IIFEs
918
+ // (the (function(){ ... })() shape) must re-execute on every
919
+ // navigation. They own their own scope and won't collide
920
+ // with prior runs, AND _cleanupContainer deleted their
921
+ // entry from window.stx._scopes on the way out. Without
922
+ // this exemption, navigating away and back leaves the page
923
+ // leaf with no scope registered: bindIf/@event never re-bind
924
+ // and all :if branches stay visible at once.
925
+ // Mirrors the full-doc swap path's runAlways escape below.
926
+ var isAlreadyScoped=code.trimStart().charAt(0)==='('||code.trimStart().charAt(0)===';'||code.indexOf('window.stx.mount')>-1;
927
+ if(!isSetup&&!isAlreadyScoped&&executedScriptHashes[h]){
917
928
  log('[router] skipping already-executed script (hash dedup)');
918
929
  return;
919
930
  }
@@ -923,13 +934,6 @@ else {
923
934
  if(hasImport){
924
935
  ns.type='module';
925
936
  }
926
- // Wrap in block scope to prevent const/let collisions on re-navigation.
927
- // Without this, navigating away and back throws "Identifier has already
928
- // been declared" because the previous page's top-level const/let are
929
- // still in the global scope. Skip the wrap for module scripts \u2014
930
- // they get their own scope from ESM, and top-level 'import' is
931
- // illegal inside a block, which would throw SyntaxError.
932
- var isAlreadyScoped=code.trimStart().charAt(0)==='('||code.trimStart().charAt(0)===';'||code.indexOf('window.stx.mount')>-1;
933
937
  ns.textContent=(hasImport||isAlreadyScoped)?code:'{'+code+'}';
934
938
  ns.setAttribute('data-stx-page','');
935
939
  document.body.appendChild(ns);
package/dist/serve.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ export declare function bundleBrowserAsset(entrypoint: string): Promise<Response>;
1
2
  /**
2
3
  * Start the STX development server
3
4
  * @param options Server options with patterns and port
package/dist/serve.js CHANGED
@@ -8730,6 +8730,53 @@ var staticContentTypes = {
8730
8730
  otf: "font/otf",
8731
8731
  eot: "application/vnd.ms-fontobject"
8732
8732
  };
8733
+ var bundledAssetExtensions = new Set(["ts", "tsx", "mts", "cts"]);
8734
+ function isBundledAssetExtension(ext) {
8735
+ return Boolean(ext && bundledAssetExtensions.has(ext));
8736
+ }
8737
+ function getAssetExtension(pathname) {
8738
+ return pathname.split(".").pop()?.toLowerCase();
8739
+ }
8740
+ function isSafeAssetPath(pathname) {
8741
+ if (pathname.includes("\x00"))
8742
+ return false;
8743
+ return !pathname.split("/").some((segment) => segment === "..");
8744
+ }
8745
+ function assetRequestPaths(pathname) {
8746
+ const paths = [
8747
+ pathname,
8748
+ pathname.replace(/^\/assets\//, "/resources/assets/"),
8749
+ pathname.replace(/^\/resources\/assets\//, "/assets/")
8750
+ ];
8751
+ return [...new Set(paths)];
8752
+ }
8753
+ async function bundleBrowserAsset(entrypoint) {
8754
+ const result = await Bun.build({
8755
+ entrypoints: [entrypoint],
8756
+ format: "esm",
8757
+ minify: false,
8758
+ packages: "bundle",
8759
+ sourcemap: "inline",
8760
+ target: "browser"
8761
+ });
8762
+ if (!result.success) {
8763
+ const message = result.logs.map((log4) => log4.message).join(`
8764
+ `) || "Unable to build TypeScript asset";
8765
+ return new Response(message, {
8766
+ status: 500,
8767
+ headers: {
8768
+ "Content-Type": "text/plain; charset=utf-8",
8769
+ "Cache-Control": "no-cache"
8770
+ }
8771
+ });
8772
+ }
8773
+ return new Response(await result.outputs[0].text(), {
8774
+ headers: {
8775
+ "Content-Type": "application/javascript",
8776
+ "Cache-Control": "no-cache"
8777
+ }
8778
+ });
8779
+ }
8733
8780
  async function serve(options) {
8734
8781
  const stxConfig = await loadConfig6({
8735
8782
  name: "stx",
@@ -8770,6 +8817,32 @@ After installing: bun add bun-plugin-stx`);
8770
8817
  const routes = new Map;
8771
8818
  let sourceFiles = null;
8772
8819
  let assetsInitialized = false;
8820
+ const htmlCache = new Map;
8821
+ const crosswindCssCache = new Map;
8822
+ const hmrClients = new Set;
8823
+ const hmrEncoder = new TextEncoder;
8824
+ function broadcastHmr(event) {
8825
+ const payload = hmrEncoder.encode(`data: ${JSON.stringify(event)}
8826
+
8827
+ `);
8828
+ for (const c of hmrClients) {
8829
+ try {
8830
+ c.enqueue(payload);
8831
+ } catch {
8832
+ hmrClients.delete(c);
8833
+ }
8834
+ }
8835
+ }
8836
+ const HMR_CLIENT_SCRIPT = `<script data-stx-hmr>(()=>{if(window.__stxHmr)return;window.__stxHmr=1;function bust(){var ls=document.querySelectorAll('link[rel="stylesheet"]');for(var i=0;i<ls.length;i++){var l=ls[i];var u=new URL(l.href,location.href);u.searchParams.set('v',Date.now().toString(36));l.href=u.toString()}}var es=new EventSource('/_stx/hmr');es.onmessage=function(e){try{var m=JSON.parse(e.data);if(m.type==='reload')location.reload();else if(m.type==='css')bust()}catch(_){}};es.onerror=function(){if(es.readyState===2){setTimeout(function(){location.reload()},400)}}})()</script>`;
8837
+ function injectHmrClient(html) {
8838
+ if (!html)
8839
+ return html;
8840
+ const closeBody = html.lastIndexOf("</body>");
8841
+ if (closeBody === -1) {
8842
+ return html + HMR_CLIENT_SCRIPT;
8843
+ }
8844
+ return html.slice(0, closeBody) + HMR_CLIENT_SCRIPT + html.slice(closeBody);
8845
+ }
8773
8846
  const watchersStarted = new Set;
8774
8847
  const startWatcher = (dir) => {
8775
8848
  if (watchersStarted.has(dir))
@@ -8779,15 +8852,34 @@ After installing: bun add bun-plugin-stx`);
8779
8852
  if (!filename)
8780
8853
  return;
8781
8854
  const f = String(filename);
8782
- if (!f.endsWith(".stx") && !f.endsWith(".md") && !f.endsWith(".html"))
8855
+ const isStxLike = f.endsWith(".stx") || f.endsWith(".md") || f.endsWith(".html");
8856
+ const isCss = f.endsWith(".css");
8857
+ const isAsset = isCss || f.endsWith(".js") || f.endsWith(".ts") || f.endsWith(".tsx") || f.endsWith(".mts") || f.endsWith(".cts") || 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");
8858
+ if (!isStxLike && !isAsset)
8783
8859
  return;
8784
- sourceFiles = null;
8785
- routes.clear();
8860
+ if (isStxLike) {
8861
+ sourceFiles = null;
8862
+ routes.clear();
8863
+ htmlCache.clear();
8864
+ crosswindCssCache.clear();
8865
+ }
8866
+ broadcastHmr(isCss && !isStxLike ? { type: "css", file: f } : { type: "reload", file: f });
8786
8867
  });
8787
8868
  watcher.on("error", () => {});
8788
8869
  watchersStarted.add(dir);
8789
8870
  } catch {}
8790
8871
  };
8872
+ for (const dir of [
8873
+ componentsDir,
8874
+ layoutsDir,
8875
+ partialsDir,
8876
+ publicDir,
8877
+ "resources/assets"
8878
+ ]) {
8879
+ if (!dir)
8880
+ continue;
8881
+ startWatcher(dir);
8882
+ }
8791
8883
  const pageMiddlewareByPath = new Map;
8792
8884
  const pageMiddlewarePatterns = [];
8793
8885
  function compileRoutePattern(urlPath) {
@@ -8971,7 +9063,7 @@ After installing: bun add bun-plugin-stx`);
8971
9063
  pageMiddlewarePatterns.length = 0;
8972
9064
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
8973
9065
  try {
8974
- const { Router } = await import("./chunk-ap5bhr00.js");
9066
+ const { Router } = await import("./chunk-wv4bcqba.js");
8975
9067
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
8976
9068
  const router = new Router(process19.cwd(), { pagesDirs });
8977
9069
  console.log(`[stx] Generated ${router.routes.length} routes \u2192 .stx/routes.ts`);
@@ -8992,7 +9084,6 @@ After installing: bun add bun-plugin-stx`);
8992
9084
  }
8993
9085
  } catch {}
8994
9086
  }
8995
- const htmlCache = new Map;
8996
9087
  async function templateSignatureFresh(sig) {
8997
9088
  for (const [p, expected] of sig) {
8998
9089
  const stat4 = await nodeFs.stat(p).catch(() => null);
@@ -9015,7 +9106,6 @@ After installing: bun add bun-plugin-stx`);
9015
9106
  let crosswindModule = null;
9016
9107
  let crosswindLoadAttempted = false;
9017
9108
  let crosswindUserConfigPromise = null;
9018
- const crosswindCssCache = new Map;
9019
9109
  async function loadCrosswind() {
9020
9110
  if (crosswindLoadAttempted)
9021
9111
  return crosswindModule;
@@ -9292,9 +9382,20 @@ After installing: bun add bun-plugin-stx`);
9292
9382
  const regexPatterns = [
9293
9383
  new RegExp(`^${routePattern}$`)
9294
9384
  ];
9385
+ const fileRouteNoIndex = fileRouteBase.replace(/\/index$/, "");
9386
+ if (fileRouteNoIndex !== fileRouteBase) {
9387
+ const noIndexPattern = fileRouteNoIndex.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9388
+ regexPatterns.push(new RegExp(`^${noIndexPattern}$`));
9389
+ }
9295
9390
  if (fileRouteBase.startsWith("pages/")) {
9296
- const prettyPattern = fileRouteBase.slice(6).replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9391
+ const prettyBase = fileRouteBase.slice(6);
9392
+ const prettyPattern = prettyBase.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9297
9393
  regexPatterns.push(new RegExp(`^${prettyPattern}$`));
9394
+ const prettyNoIndex = prettyBase.replace(/\/index$/, "");
9395
+ if (prettyNoIndex !== prettyBase) {
9396
+ const prettyNoIndexPattern = prettyNoIndex.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9397
+ regexPatterns.push(new RegExp(`^${prettyNoIndexPattern}$`));
9398
+ }
9298
9399
  }
9299
9400
  for (const regex of regexPatterns) {
9300
9401
  const match = normalizedPath.match(regex);
@@ -9472,6 +9573,7 @@ ${html}`;
9472
9573
  try {
9473
9574
  _server = bunServe({
9474
9575
  port: actualPort,
9576
+ idleTimeout: 0,
9475
9577
  async fetch(req) {
9476
9578
  const url = new URL(req.url);
9477
9579
  let path = url.pathname;
@@ -9490,6 +9592,40 @@ ${html}`;
9490
9592
  if (req.method === "OPTIONS") {
9491
9593
  return new Response(null, { headers: corsHeaders });
9492
9594
  }
9595
+ if (path === "/_stx/hmr") {
9596
+ let controller;
9597
+ let keepalive;
9598
+ const stream = new ReadableStream({
9599
+ start(c) {
9600
+ controller = c;
9601
+ hmrClients.add(c);
9602
+ c.enqueue(hmrEncoder.encode(`data: {"type":"connected"}
9603
+
9604
+ `));
9605
+ keepalive = setInterval(() => {
9606
+ try {
9607
+ c.enqueue(hmrEncoder.encode(`: keepalive
9608
+
9609
+ `));
9610
+ } catch {}
9611
+ }, 15000);
9612
+ },
9613
+ cancel() {
9614
+ if (keepalive)
9615
+ clearInterval(keepalive);
9616
+ if (controller)
9617
+ hmrClients.delete(controller);
9618
+ }
9619
+ });
9620
+ return new Response(stream, {
9621
+ headers: {
9622
+ "Content-Type": "text/event-stream",
9623
+ "Cache-Control": "no-cache, no-transform",
9624
+ Connection: "keep-alive",
9625
+ "X-Accel-Buffering": "no"
9626
+ }
9627
+ });
9628
+ }
9493
9629
  if (options.onRequest) {
9494
9630
  const customResponse = await options.onRequest(req);
9495
9631
  if (customResponse)
@@ -9737,7 +9873,7 @@ ${pageSetupScripts.join(`
9737
9873
  runtimeCount++;
9738
9874
  return runtimeCount === 1 ? match : "";
9739
9875
  });
9740
- return new Response(cleaned, {
9876
+ return new Response(injectHmrClient(cleaned), {
9741
9877
  headers: {
9742
9878
  "Content-Type": "text/html; charset=utf-8",
9743
9879
  "Cache-Control": "no-cache",
@@ -9747,7 +9883,7 @@ ${pageSetupScripts.join(`
9747
9883
  }
9748
9884
  const contentWithExt = await getRoute(`${path}.html`);
9749
9885
  if (contentWithExt) {
9750
- return new Response(contentWithExt, {
9886
+ return new Response(injectHmrClient(contentWithExt), {
9751
9887
  headers: {
9752
9888
  "Content-Type": "text/html; charset=utf-8",
9753
9889
  "Cache-Control": "no-cache",
@@ -9772,34 +9908,26 @@ ${pageSetupScripts.join(`
9772
9908
  }
9773
9909
  if (path.startsWith("/assets/") || path.startsWith("/resources/assets/")) {
9774
9910
  await ensureAssets();
9775
- const possiblePaths = [
9776
- path,
9777
- path.replace(/^\/assets\//, "/resources/assets/"),
9778
- path.replace(/^\/resources\/assets\//, "/assets/")
9779
- ];
9780
- for (const assetPath of possiblePaths) {
9911
+ let assetPathname;
9912
+ try {
9913
+ assetPathname = decodeURIComponent(path);
9914
+ } catch {
9915
+ return new Response("Invalid asset path", { status: 400 });
9916
+ }
9917
+ if (!isSafeAssetPath(assetPathname))
9918
+ return new Response("Invalid asset path", { status: 400 });
9919
+ for (const assetPath of assetRequestPaths(assetPathname)) {
9781
9920
  try {
9782
- const filePath = `.${assetPath}`;
9921
+ const filePath = nodePath.resolve(process19.cwd(), `.${assetPath}`);
9783
9922
  const file = Bun.file(filePath);
9784
9923
  if (await file.exists()) {
9785
- const ext = assetPath.split(".").pop()?.toLowerCase();
9786
- if (ext === "ts") {
9787
- const transpiler = new Bun.Transpiler({
9788
- loader: "ts"
9789
- });
9790
- const code = await file.text();
9791
- const transpiled = transpiler.transformSync(code);
9792
- return new Response(transpiled, {
9793
- headers: {
9794
- "Content-Type": "application/javascript",
9795
- "Cache-Control": "no-cache"
9796
- }
9797
- });
9798
- }
9924
+ const ext = getAssetExtension(assetPath);
9925
+ if (isBundledAssetExtension(ext))
9926
+ return bundleBrowserAsset(filePath);
9799
9927
  return new Response(file, {
9800
9928
  headers: {
9801
9929
  "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9802
- "Cache-Control": "public, max-age=31536000"
9930
+ "Cache-Control": "no-cache"
9803
9931
  }
9804
9932
  });
9805
9933
  }
@@ -9829,7 +9957,7 @@ ${pageSetupScripts.join(`
9829
9957
  return new Response(file, {
9830
9958
  headers: {
9831
9959
  "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9832
- "Cache-Control": "public, max-age=3600"
9960
+ "Cache-Control": "no-cache"
9833
9961
  }
9834
9962
  });
9835
9963
  }
@@ -9860,7 +9988,7 @@ ${pageSetupScripts.join(`
9860
9988
  }
9861
9989
  const routesList = availableRoutes.join(`
9862
9990
  `);
9863
- return new Response(`
9991
+ return new Response(injectHmrClient(`
9864
9992
  <!DOCTYPE html>
9865
9993
  <html>
9866
9994
  <head>
@@ -9887,7 +10015,7 @@ ${pageSetupScripts.join(`
9887
10015
  <ul>${routesList}</ul>
9888
10016
  </body>
9889
10017
  </html>
9890
- `, {
10018
+ `), {
9891
10019
  status: 404,
9892
10020
  headers: { "Content-Type": "text/html; charset=utf-8" }
9893
10021
  });
@@ -9963,5 +10091,6 @@ if (import.meta.main) {
9963
10091
  main().catch(console.error);
9964
10092
  }
9965
10093
  export {
9966
- serve
10094
+ serve,
10095
+ bundleBrowserAsset
9967
10096
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.65",
4
+ "version": "0.2.66",
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",
@@ -73,7 +73,7 @@
73
73
  "typecheck": "bun tsc --noEmit"
74
74
  },
75
75
  "dependencies": {
76
- "@stacksjs/stx": "0.2.64",
77
- "stx-router": "0.2.57"
76
+ "@stacksjs/stx": "0.2.66",
77
+ "stx-router": "0.2.66"
78
78
  }
79
79
  }