bun-plugin-stx 0.2.65 → 0.2.67

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,11 +8730,61 @@ 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",
8783
+ alias: ["ui"],
8736
8784
  cwd: process19.cwd(),
8737
- defaultConfig: defaultStxConfig
8785
+ defaultConfig: defaultStxConfig,
8786
+ checkEnv: false,
8787
+ verbose: false
8738
8788
  });
8739
8789
  try {
8740
8790
  const stxMod = options.stxModule ? options.stxModule : await defaultStxModule;
@@ -8770,6 +8820,32 @@ After installing: bun add bun-plugin-stx`);
8770
8820
  const routes = new Map;
8771
8821
  let sourceFiles = null;
8772
8822
  let assetsInitialized = false;
8823
+ const htmlCache = new Map;
8824
+ const crosswindCssCache = new Map;
8825
+ const hmrClients = new Set;
8826
+ const hmrEncoder = new TextEncoder;
8827
+ function broadcastHmr(event) {
8828
+ const payload = hmrEncoder.encode(`data: ${JSON.stringify(event)}
8829
+
8830
+ `);
8831
+ for (const c of hmrClients) {
8832
+ try {
8833
+ c.enqueue(payload);
8834
+ } catch {
8835
+ hmrClients.delete(c);
8836
+ }
8837
+ }
8838
+ }
8839
+ 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>`;
8840
+ function injectHmrClient(html) {
8841
+ if (!html)
8842
+ return html;
8843
+ const closeBody = html.lastIndexOf("</body>");
8844
+ if (closeBody === -1) {
8845
+ return html + HMR_CLIENT_SCRIPT;
8846
+ }
8847
+ return html.slice(0, closeBody) + HMR_CLIENT_SCRIPT + html.slice(closeBody);
8848
+ }
8773
8849
  const watchersStarted = new Set;
8774
8850
  const startWatcher = (dir) => {
8775
8851
  if (watchersStarted.has(dir))
@@ -8779,15 +8855,34 @@ After installing: bun add bun-plugin-stx`);
8779
8855
  if (!filename)
8780
8856
  return;
8781
8857
  const f = String(filename);
8782
- if (!f.endsWith(".stx") && !f.endsWith(".md") && !f.endsWith(".html"))
8858
+ const isStxLike = f.endsWith(".stx") || f.endsWith(".md") || f.endsWith(".html");
8859
+ const isCss = f.endsWith(".css");
8860
+ 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");
8861
+ if (!isStxLike && !isAsset)
8783
8862
  return;
8784
- sourceFiles = null;
8785
- routes.clear();
8863
+ if (isStxLike) {
8864
+ sourceFiles = null;
8865
+ routes.clear();
8866
+ htmlCache.clear();
8867
+ crosswindCssCache.clear();
8868
+ }
8869
+ broadcastHmr(isCss && !isStxLike ? { type: "css", file: f } : { type: "reload", file: f });
8786
8870
  });
8787
8871
  watcher.on("error", () => {});
8788
8872
  watchersStarted.add(dir);
8789
8873
  } catch {}
8790
8874
  };
8875
+ for (const dir of [
8876
+ componentsDir,
8877
+ layoutsDir,
8878
+ partialsDir,
8879
+ publicDir,
8880
+ "resources/assets"
8881
+ ]) {
8882
+ if (!dir)
8883
+ continue;
8884
+ startWatcher(dir);
8885
+ }
8791
8886
  const pageMiddlewareByPath = new Map;
8792
8887
  const pageMiddlewarePatterns = [];
8793
8888
  function compileRoutePattern(urlPath) {
@@ -8971,10 +9066,11 @@ After installing: bun add bun-plugin-stx`);
8971
9066
  pageMiddlewarePatterns.length = 0;
8972
9067
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
8973
9068
  try {
8974
- const { Router } = await import("./chunk-ap5bhr00.js");
9069
+ const { Router } = await import("./chunk-wv4bcqba.js");
8975
9070
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
8976
9071
  const router = new Router(process19.cwd(), { pagesDirs });
8977
- console.log(`[stx] Generated ${router.routes.length} routes \u2192 .stx/routes.ts`);
9072
+ if (!options.quiet)
9073
+ console.log(`[stx] Generated ${router.routes.length} routes \u2192 .stx/routes.ts`);
8978
9074
  } catch (e) {}
8979
9075
  return files;
8980
9076
  }
@@ -8992,7 +9088,6 @@ After installing: bun add bun-plugin-stx`);
8992
9088
  }
8993
9089
  } catch {}
8994
9090
  }
8995
- const htmlCache = new Map;
8996
9091
  async function templateSignatureFresh(sig) {
8997
9092
  for (const [p, expected] of sig) {
8998
9093
  const stat4 = await nodeFs.stat(p).catch(() => null);
@@ -9015,7 +9110,6 @@ After installing: bun add bun-plugin-stx`);
9015
9110
  let crosswindModule = null;
9016
9111
  let crosswindLoadAttempted = false;
9017
9112
  let crosswindUserConfigPromise = null;
9018
- const crosswindCssCache = new Map;
9019
9113
  async function loadCrosswind() {
9020
9114
  if (crosswindLoadAttempted)
9021
9115
  return crosswindModule;
@@ -9071,7 +9165,9 @@ After installing: bun add bun-plugin-stx`);
9071
9165
  crosswindUserConfigPromise = loadConfig6({
9072
9166
  name: "crosswind",
9073
9167
  cwd: process19.cwd(),
9074
- defaultConfig: {}
9168
+ defaultConfig: {},
9169
+ checkEnv: false,
9170
+ verbose: false
9075
9171
  });
9076
9172
  }
9077
9173
  const userConfig = await crosswindUserConfigPromise;
@@ -9292,9 +9388,20 @@ After installing: bun add bun-plugin-stx`);
9292
9388
  const regexPatterns = [
9293
9389
  new RegExp(`^${routePattern}$`)
9294
9390
  ];
9391
+ const fileRouteNoIndex = fileRouteBase.replace(/\/index$/, "");
9392
+ if (fileRouteNoIndex !== fileRouteBase) {
9393
+ const noIndexPattern = fileRouteNoIndex.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9394
+ regexPatterns.push(new RegExp(`^${noIndexPattern}$`));
9395
+ }
9295
9396
  if (fileRouteBase.startsWith("pages/")) {
9296
- const prettyPattern = fileRouteBase.slice(6).replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9397
+ const prettyBase = fileRouteBase.slice(6);
9398
+ const prettyPattern = prettyBase.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9297
9399
  regexPatterns.push(new RegExp(`^${prettyPattern}$`));
9400
+ const prettyNoIndex = prettyBase.replace(/\/index$/, "");
9401
+ if (prettyNoIndex !== prettyBase) {
9402
+ const prettyNoIndexPattern = prettyNoIndex.replace(/\[([^\]]+)\]/g, "([^/]+)").replace(/\//g, "\\/");
9403
+ regexPatterns.push(new RegExp(`^${prettyNoIndexPattern}$`));
9404
+ }
9298
9405
  }
9299
9406
  for (const regex of regexPatterns) {
9300
9407
  const match = normalizedPath.match(regex);
@@ -9472,6 +9579,7 @@ ${html}`;
9472
9579
  try {
9473
9580
  _server = bunServe({
9474
9581
  port: actualPort,
9582
+ idleTimeout: 0,
9475
9583
  async fetch(req) {
9476
9584
  const url = new URL(req.url);
9477
9585
  let path = url.pathname;
@@ -9490,6 +9598,40 @@ ${html}`;
9490
9598
  if (req.method === "OPTIONS") {
9491
9599
  return new Response(null, { headers: corsHeaders });
9492
9600
  }
9601
+ if (path === "/_stx/hmr") {
9602
+ let controller;
9603
+ let keepalive;
9604
+ const stream = new ReadableStream({
9605
+ start(c) {
9606
+ controller = c;
9607
+ hmrClients.add(c);
9608
+ c.enqueue(hmrEncoder.encode(`data: {"type":"connected"}
9609
+
9610
+ `));
9611
+ keepalive = setInterval(() => {
9612
+ try {
9613
+ c.enqueue(hmrEncoder.encode(`: keepalive
9614
+
9615
+ `));
9616
+ } catch {}
9617
+ }, 15000);
9618
+ },
9619
+ cancel() {
9620
+ if (keepalive)
9621
+ clearInterval(keepalive);
9622
+ if (controller)
9623
+ hmrClients.delete(controller);
9624
+ }
9625
+ });
9626
+ return new Response(stream, {
9627
+ headers: {
9628
+ "Content-Type": "text/event-stream",
9629
+ "Cache-Control": "no-cache, no-transform",
9630
+ Connection: "keep-alive",
9631
+ "X-Accel-Buffering": "no"
9632
+ }
9633
+ });
9634
+ }
9493
9635
  if (options.onRequest) {
9494
9636
  const customResponse = await options.onRequest(req);
9495
9637
  if (customResponse)
@@ -9737,7 +9879,7 @@ ${pageSetupScripts.join(`
9737
9879
  runtimeCount++;
9738
9880
  return runtimeCount === 1 ? match : "";
9739
9881
  });
9740
- return new Response(cleaned, {
9882
+ return new Response(injectHmrClient(cleaned), {
9741
9883
  headers: {
9742
9884
  "Content-Type": "text/html; charset=utf-8",
9743
9885
  "Cache-Control": "no-cache",
@@ -9747,7 +9889,7 @@ ${pageSetupScripts.join(`
9747
9889
  }
9748
9890
  const contentWithExt = await getRoute(`${path}.html`);
9749
9891
  if (contentWithExt) {
9750
- return new Response(contentWithExt, {
9892
+ return new Response(injectHmrClient(contentWithExt), {
9751
9893
  headers: {
9752
9894
  "Content-Type": "text/html; charset=utf-8",
9753
9895
  "Cache-Control": "no-cache",
@@ -9772,34 +9914,26 @@ ${pageSetupScripts.join(`
9772
9914
  }
9773
9915
  if (path.startsWith("/assets/") || path.startsWith("/resources/assets/")) {
9774
9916
  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) {
9917
+ let assetPathname;
9918
+ try {
9919
+ assetPathname = decodeURIComponent(path);
9920
+ } catch {
9921
+ return new Response("Invalid asset path", { status: 400 });
9922
+ }
9923
+ if (!isSafeAssetPath(assetPathname))
9924
+ return new Response("Invalid asset path", { status: 400 });
9925
+ for (const assetPath of assetRequestPaths(assetPathname)) {
9781
9926
  try {
9782
- const filePath = `.${assetPath}`;
9927
+ const filePath = nodePath.resolve(process19.cwd(), `.${assetPath}`);
9783
9928
  const file = Bun.file(filePath);
9784
9929
  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
- }
9930
+ const ext = getAssetExtension(assetPath);
9931
+ if (isBundledAssetExtension(ext))
9932
+ return bundleBrowserAsset(filePath);
9799
9933
  return new Response(file, {
9800
9934
  headers: {
9801
9935
  "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9802
- "Cache-Control": "public, max-age=31536000"
9936
+ "Cache-Control": "no-cache"
9803
9937
  }
9804
9938
  });
9805
9939
  }
@@ -9829,7 +9963,7 @@ ${pageSetupScripts.join(`
9829
9963
  return new Response(file, {
9830
9964
  headers: {
9831
9965
  "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9832
- "Cache-Control": "public, max-age=3600"
9966
+ "Cache-Control": "no-cache"
9833
9967
  }
9834
9968
  });
9835
9969
  }
@@ -9860,7 +9994,7 @@ ${pageSetupScripts.join(`
9860
9994
  }
9861
9995
  const routesList = availableRoutes.join(`
9862
9996
  `);
9863
- return new Response(`
9997
+ return new Response(injectHmrClient(`
9864
9998
  <!DOCTYPE html>
9865
9999
  <html>
9866
10000
  <head>
@@ -9887,7 +10021,7 @@ ${pageSetupScripts.join(`
9887
10021
  <ul>${routesList}</ul>
9888
10022
  </body>
9889
10023
  </html>
9890
- `, {
10024
+ `), {
9891
10025
  status: 404,
9892
10026
  headers: { "Content-Type": "text/html; charset=utf-8" }
9893
10027
  });
@@ -9963,5 +10097,6 @@ if (import.meta.main) {
9963
10097
  main().catch(console.error);
9964
10098
  }
9965
10099
  export {
9966
- serve
10100
+ serve,
10101
+ bundleBrowserAsset
9967
10102
  };
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.67",
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.67",
77
+ "stx-router": "0.2.67"
78
78
  }
79
79
  }