bun-plugin-stx 0.2.54 → 0.2.63

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.
@@ -591,6 +591,19 @@ function getRouterScript() {
591
591
  var containerSel=o.container;
592
592
  var debug=!!o.debug;
593
593
  function log(){if(debug&&typeof console!=='undefined'&&console.log)console.log.apply(console,arguments)}
594
+ function runViewTransition(callback){
595
+ if(!o.viewTransitions||!document.startViewTransition)return false;
596
+ try{
597
+ var transition=document.startViewTransition(callback);
598
+ var onAbort=function(err){log('[router] view transition aborted:',err&&err.message?err.message:err)};
599
+ if(transition&&transition.finished&&transition.finished.catch)transition.finished.catch(onAbort);
600
+ if(transition&&transition.updateCallbackDone&&transition.updateCallbackDone.catch)transition.updateCallbackDone.catch(onAbort);
601
+ return true;
602
+ }catch(err){
603
+ log('[router] view transition unavailable:',err&&err.message?err.message:err);
604
+ return false;
605
+ }
606
+ }
594
607
 
595
608
  // \u2500\u2500 Progress bar (0\u2192100% at top of viewport) \u2500\u2500
596
609
  // Native loading indicator baked into the router. Starts when navigation
@@ -906,12 +919,18 @@ else {
906
919
  }
907
920
  executedScriptHashes[h]=1;
908
921
  var ns=document.createElement('script');
922
+ var hasImport=code.indexOf('import ')!==-1;
923
+ if(hasImport){
924
+ ns.type='module';
925
+ }
909
926
  // Wrap in block scope to prevent const/let collisions on re-navigation.
910
927
  // Without this, navigating away and back throws "Identifier has already
911
928
  // been declared" because the previous page's top-level const/let are
912
- // still in the global scope.
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.
913
932
  var isAlreadyScoped=code.trimStart().charAt(0)==='('||code.trimStart().charAt(0)===';'||code.indexOf('window.stx.mount')>-1;
914
- ns.textContent=isAlreadyScoped?code:'{'+code+'}';
933
+ ns.textContent=(hasImport||isAlreadyScoped)?code:'{'+code+'}';
915
934
  ns.setAttribute('data-stx-page','');
916
935
  document.body.appendChild(ns);
917
936
  });
@@ -919,7 +938,7 @@ else {
919
938
  // THEN fire stx:load \u2014 now _latestSetup is set and processElement has the right scope
920
939
  window.dispatchEvent(new Event('stx:load'));
921
940
  }
922
- if(o.viewTransitions&&document.startViewTransition){document.startViewTransition(doFragSwap)}
941
+ if(runViewTransition(doFragSwap)){}
923
942
  else{currentContent.style.transition='opacity 0.12s ease-out';currentContent.style.opacity='0';setTimeout(function(){doFragSwap();currentContent.style.opacity='1';setTimeout(function(){currentContent.style.transition=''},150)},120)}
924
943
  return;
925
944
  }
@@ -1142,8 +1161,11 @@ else {
1142
1161
  // Wrap in block scope to prevent const/let collisions on re-navigation.
1143
1162
  // Top-level const/let in <script> tags are global-scoped in browsers
1144
1163
  // and throw "Identifier has already been declared" on re-execution.
1164
+ // Skip the wrap for module scripts \u2014 they get their own scope from
1165
+ // ESM, and top-level 'import' is illegal inside a block, which
1166
+ // would throw SyntaxError before the script ever runs.
1145
1167
  var alreadyScoped=text.trimStart().charAt(0)==='('||text.trimStart().charAt(0)===';'||text.indexOf('window.stx.mount')>-1;
1146
- ns.textContent=alreadyScoped?text:'{'+text+'}';
1168
+ ns.textContent=(hasImport||alreadyScoped)?text:'{'+text+'}';
1147
1169
  ns.setAttribute('data-stx-page','');
1148
1170
  document.body.appendChild(ns);
1149
1171
  });
@@ -1159,8 +1181,7 @@ else {
1159
1181
  }
1160
1182
  }
1161
1183
 
1162
- if(o.viewTransitions&&document.startViewTransition){
1163
- document.startViewTransition(doSwap);
1184
+ if(runViewTransition(doSwap)){
1164
1185
  }
1165
1186
  else {
1166
1187
  // Fallback fade for browsers without View Transitions API
package/dist/serve.d.ts CHANGED
@@ -11,9 +11,16 @@ export declare interface ServeOptions {
11
11
  partialsDir?: string
12
12
  publicDir?: string
13
13
  quiet?: boolean
14
+ autoIncrementPort?: boolean | number
14
15
  stxModule?: typeof import('@stacksjs/stx') | Promise<typeof import('@stacksjs/stx')>
16
+ i18n?: import('@stacksjs/stx').ResolvedI18n
17
+ site?: import('@stacksjs/stx').SiteConfig
15
18
  routes?: Record<string, (req: Request) => Response | Promise<Response>>
16
- onRequest?: (req: Request) => Response | Promise<Response> | null | undefined
19
+ onRequest?: (req: Request) =>
20
+ | Response
21
+ | null
22
+ | undefined
23
+ | Promise<Response | null | undefined>
17
24
  middleware?: Record<string, MiddlewareHandler>
18
25
  globalMiddleware?: string[]
19
26
  middlewareGroups?: Record<string, string[]>
package/dist/serve.js CHANGED
@@ -8736,6 +8736,18 @@ async function serve(options) {
8736
8736
  cwd: process19.cwd(),
8737
8737
  defaultConfig: defaultStxConfig
8738
8738
  });
8739
+ try {
8740
+ const stxMod = options.stxModule ? options.stxModule : await defaultStxModule;
8741
+ if (stxMod && typeof stxMod.loadStxConfig === "function") {
8742
+ const pluginLoaded = await stxMod.loadStxConfig();
8743
+ if (pluginLoaded?._pluginComponentDirs)
8744
+ stxConfig._pluginComponentDirs = pluginLoaded._pluginComponentDirs;
8745
+ if (pluginLoaded?._pluginPageDirs)
8746
+ stxConfig._pluginPageDirs = pluginLoaded._pluginPageDirs;
8747
+ }
8748
+ if (stxMod && typeof stxMod.preloadIconCollection === "function")
8749
+ await stxMod.preloadIconCollection("lucide");
8750
+ } catch {}
8739
8751
  const componentsDir = options.componentsDir ?? stxConfig.componentsDir ?? defaultStxConfig.componentsDir;
8740
8752
  const layoutsDir = options.layoutsDir ?? stxConfig.layoutsDir ?? defaultStxConfig.layoutsDir;
8741
8753
  const partialsDir = options.partialsDir ?? stxConfig.partialsDir ?? defaultStxConfig.partialsDir;
@@ -8959,7 +8971,7 @@ After installing: bun add bun-plugin-stx`);
8959
8971
  pageMiddlewarePatterns.length = 0;
8960
8972
  await Promise.all(files.map((f) => detectPageMiddleware(f)));
8961
8973
  try {
8962
- const { Router } = await import("./chunk-nznv3e0m.js");
8974
+ const { Router } = await import("./chunk-ap5bhr00.js");
8963
8975
  const pagesDirs = patterns.map((p) => p.replace(/\/$/, "")).filter(Boolean);
8964
8976
  const router = new Router(process19.cwd(), { pagesDirs });
8965
8977
  console.log(`[stx] Generated ${router.routes.length} routes \u2192 .stx/routes.ts`);
@@ -9162,7 +9174,9 @@ After installing: bun add bun-plugin-stx`);
9162
9174
  ..."defaultTitle" in stxConfig && { defaultTitle: stxConfig.defaultTitle },
9163
9175
  ..."defaultDescription" in stxConfig && { defaultDescription: stxConfig.defaultDescription },
9164
9176
  ..."defaultImage" in stxConfig && { defaultImage: stxConfig.defaultImage },
9165
- ..."seo" in stxConfig && { seo: stxConfig.seo }
9177
+ ..."seo" in stxConfig && { seo: stxConfig.seo },
9178
+ ..."_pluginComponentDirs" in stxConfig && { _pluginComponentDirs: stxConfig._pluginComponentDirs },
9179
+ ..."_pluginPageDirs" in stxConfig && { _pluginPageDirs: stxConfig._pluginPageDirs }
9166
9180
  };
9167
9181
  if (config5.ssr === false || stxConfig.ssr === false) {
9168
9182
  return generateSpaShell({
@@ -9332,7 +9346,9 @@ After installing: bun add bun-plugin-stx`);
9332
9346
  ..."defaultTitle" in stxConfig && { defaultTitle: stxConfig.defaultTitle },
9333
9347
  ..."defaultDescription" in stxConfig && { defaultDescription: stxConfig.defaultDescription },
9334
9348
  ..."defaultImage" in stxConfig && { defaultImage: stxConfig.defaultImage },
9335
- ..."seo" in stxConfig && { seo: stxConfig.seo }
9349
+ ..."seo" in stxConfig && { seo: stxConfig.seo },
9350
+ ..."_pluginComponentDirs" in stxConfig && { _pluginComponentDirs: stxConfig._pluginComponentDirs },
9351
+ ..."_pluginPageDirs" in stxConfig && { _pluginPageDirs: stxConfig._pluginPageDirs }
9336
9352
  };
9337
9353
  let output = templateContent;
9338
9354
  const dependencies = new Set;
@@ -9369,385 +9385,482 @@ ${output}`;
9369
9385
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
9370
9386
  "Access-Control-Allow-Headers": "Content-Type"
9371
9387
  };
9372
- const _server = bunServe({
9373
- port,
9374
- async fetch(req) {
9375
- const url = new URL(req.url);
9376
- let path = url.pathname;
9377
- if (req.method === "OPTIONS") {
9378
- return new Response(null, { headers: corsHeaders });
9379
- }
9380
- if (options.onRequest) {
9381
- const customResponse = await options.onRequest(req);
9382
- if (customResponse)
9383
- return customResponse;
9384
- }
9385
- const route = resolveRouteMiddleware(path);
9386
- const extraNames = extraProtectedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : `${p}/`)) ? ["auth"] : [];
9387
- const requested = [...globalMiddlewareNames, ...route.names, ...extraNames];
9388
- if (requested.length > 0) {
9389
- const expanded = expandMiddlewareNames(requested);
9390
- const cookies = parseCookies(req);
9391
- const ctx = {
9392
- path,
9393
- url,
9394
- params: route.params,
9395
- cookies,
9396
- redirect: (to, status = 302) => {
9397
- const sep = to.includes("?") ? "&" : "?";
9398
- const next = encodeURIComponent(path + (url.search || ""));
9399
- return new Response(null, {
9400
- status,
9401
- headers: { Location: `${to}${sep}next=${next}` }
9402
- });
9403
- }
9404
- };
9405
- for (const entry of expanded) {
9406
- const colon = entry.indexOf(":");
9407
- const name = colon === -1 ? entry : entry.slice(0, colon);
9408
- const args = colon === -1 ? [] : entry.slice(colon + 1).split(",");
9409
- const handler = middlewareRegistry[name];
9410
- if (!handler) {
9411
- console.warn(`[stx serve] unknown middleware "${name}" on ${path}`);
9412
- continue;
9413
- }
9414
- const result = await handler(req, ctx, ...args);
9415
- if (result instanceof Response)
9416
- return result;
9417
- }
9418
- }
9419
- if (options.routes) {
9420
- const routeHandler = options.routes[path];
9421
- if (routeHandler) {
9422
- return routeHandler(req);
9423
- }
9424
- }
9425
- if (path.startsWith("/_stx/component/")) {
9426
- const componentName = decodeURIComponent(path.slice("/_stx/component/".length));
9427
- if (componentName) {
9428
- try {
9429
- const { processDirectives, defaultConfig: defaultConfig4 } = await stxModule;
9430
- const componentTemplate = `<${componentName} />`;
9431
- const componentOpts = {
9432
- ...defaultConfig4,
9433
- ...componentsDir && { componentsDir },
9434
- ...layoutsDir && { layoutsDir },
9435
- ...partialsDir && { partialsDir },
9436
- autoShell: false
9437
- };
9438
- const html = await processDirectives(componentTemplate, {}, `${componentsDir}/${componentName}.stx`, componentOpts, new Set);
9439
- return new Response(html, {
9440
- headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
9441
- });
9442
- } catch (e) {
9443
- return new Response(`<div class="stx-async-error">${e.message}</div>`, {
9444
- status: 500,
9445
- headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
9446
- });
9447
- }
9448
- }
9449
- }
9450
- if (path.startsWith("/api/data/") && req.method === "POST") {
9451
- try {
9452
- const tableName = path.replace("/api/data/", "").split("/")[0];
9453
- if (!tableName) {
9454
- return new Response(JSON.stringify({ error: "Table name required" }), {
9455
- status: 400,
9456
- headers: { "Content-Type": "application/json", ...corsHeaders }
9457
- });
9458
- }
9459
- if (!/^[a-zA-Z_]\w*$/.test(tableName)) {
9460
- return new Response(JSON.stringify({ error: "Invalid table name" }), {
9461
- status: 400,
9462
- headers: { "Content-Type": "application/json", ...corsHeaders }
9463
- });
9388
+ const siteConfig = options.site;
9389
+ const siteStxPromise = siteConfig || options.i18n ? options.stxModule ? Promise.resolve(options.stxModule) : defaultStxModule : null;
9390
+ let i18nConfig = options.i18n ?? null;
9391
+ if (!i18nConfig && siteConfig && siteStxPromise) {
9392
+ try {
9393
+ const { resolveI18n } = await siteStxPromise;
9394
+ i18nConfig = resolveI18n(siteConfig) ?? null;
9395
+ } catch {}
9396
+ }
9397
+ function localeFromPath(pathname) {
9398
+ if (!i18nConfig)
9399
+ return { locale: "en", path: pathname };
9400
+ for (const loc of i18nConfig.locales) {
9401
+ if (pathname === `/${loc}` || pathname === `/${loc}/`)
9402
+ return { locale: loc, path: "/" };
9403
+ if (pathname.startsWith(`/${loc}/`))
9404
+ return { locale: loc, path: pathname.slice(loc.length + 1) };
9405
+ }
9406
+ return { locale: i18nConfig.defaultLocale, path: pathname };
9407
+ }
9408
+ async function applyI18nToResponse(res, locale, normalizedPath) {
9409
+ if (!siteStxPromise)
9410
+ return res;
9411
+ const contentType = res.headers.get("content-type") || "";
9412
+ if (!contentType.includes("text/html"))
9413
+ return res;
9414
+ const stx = await siteStxPromise;
9415
+ let html = await res.text();
9416
+ if (siteConfig && stx.injectThemeBootstrap)
9417
+ html = stx.injectThemeBootstrap(html, siteConfig);
9418
+ if (siteConfig && stx.injectSeo) {
9419
+ const pageMeta = (siteConfig.pages ?? {})[normalizedPath] ?? {};
9420
+ html = stx.injectSeo(html, siteConfig, pageMeta, normalizedPath);
9421
+ }
9422
+ if (i18nConfig && stx.buildLangPickerScript) {
9423
+ const pickerScript = stx.buildLangPickerScript(i18nConfig, locale);
9424
+ html = /<\/body>/i.test(html) ? html.replace(/<\/body>/i, `${pickerScript}
9425
+ </body>`) : html + pickerScript;
9426
+ }
9427
+ if (i18nConfig) {
9428
+ const groupMeta = `<meta name="stx-layout-group" content="i18n:${locale}">`;
9429
+ if (/<meta name="stx-layout-group"[^>]*>/i.test(html))
9430
+ html = html.replace(/<meta name="stx-layout-group"[^>]*>/i, groupMeta);
9431
+ else if (/<\/head>/i.test(html))
9432
+ html = html.replace(/<\/head>/i, `${groupMeta}
9433
+ </head>`);
9434
+ else
9435
+ html = `${groupMeta}
9436
+ ${html}`;
9437
+ }
9438
+ if (i18nConfig && locale !== i18nConfig.defaultLocale && siteConfig?.pages) {
9439
+ const knownPaths = new Set(Object.keys(siteConfig.pages));
9440
+ html = html.replace(/<a\b([^>]*?)\bhref="(\/[^"]*)"([^>]*)>/gi, (full, before, href, after) => {
9441
+ if (href.startsWith("//"))
9442
+ return full;
9443
+ if (href === `/${locale}` || href === `/${locale}/` || href.startsWith(`/${locale}/`))
9444
+ return full;
9445
+ const m = href.match(/^(\/[^?#]*)(.*)$/);
9446
+ if (!m)
9447
+ return full;
9448
+ const [, pathOnly, rest] = m;
9449
+ const normalized = pathOnly === "/" ? "/" : pathOnly.replace(/\/$/, "");
9450
+ if (!knownPaths.has(normalized))
9451
+ return full;
9452
+ const localized = normalized === "/" ? `/${locale}/` : `/${locale}${normalized}`;
9453
+ return `<a${before}href="${localized}${rest}"${after}>`;
9454
+ });
9455
+ }
9456
+ if (i18nConfig && stx.applyTranslations)
9457
+ html = stx.applyTranslations(html, i18nConfig, locale);
9458
+ const headers = new Headers(res.headers);
9459
+ headers.delete("content-length");
9460
+ return new Response(html, {
9461
+ status: res.status,
9462
+ statusText: res.statusText,
9463
+ headers
9464
+ });
9465
+ }
9466
+ const portAutoIncrement = options.autoIncrementPort ?? true;
9467
+ const maxPortAttempts = portAutoIncrement === false ? 1 : typeof portAutoIncrement === "number" ? Math.max(1, portAutoIncrement) : 10;
9468
+ let actualPort = port;
9469
+ let _server;
9470
+ let lastServeError;
9471
+ for (let attempt = 0;attempt < maxPortAttempts; attempt++) {
9472
+ try {
9473
+ _server = bunServe({
9474
+ port: actualPort,
9475
+ async fetch(req) {
9476
+ const url = new URL(req.url);
9477
+ let path = url.pathname;
9478
+ let i18nLocale = null;
9479
+ if (i18nConfig && !url.pathname.startsWith("/api/")) {
9480
+ const stripped = localeFromPath(url.pathname);
9481
+ i18nLocale = stripped.locale;
9482
+ if (stripped.path !== url.pathname) {
9483
+ const rewritten = new URL(req.url);
9484
+ rewritten.pathname = stripped.path;
9485
+ req = new Request(rewritten, req);
9486
+ path = stripped.path;
9487
+ }
9464
9488
  }
9465
- const body = await req.json();
9466
- const { Database } = await import("bun:sqlite");
9467
- const dbPath = nodePath.resolve(process19.cwd(), "database/stacks.sqlite");
9468
- const db = new Database(dbPath);
9469
- try {
9470
- const tableInfo = db.query(`PRAGMA table_info("${tableName}")`).all();
9471
- const validColumns = tableInfo.map((c) => c.name).filter((n) => n !== "id" && n !== "created_at" && n !== "updated_at");
9472
- const columns = [];
9473
- const placeholders = [];
9474
- const values = [];
9475
- for (const col of validColumns) {
9476
- if (body[col] !== undefined && body[col] !== "") {
9477
- columns.push(col);
9478
- placeholders.push("?");
9479
- values.push(body[col]);
9480
- }
9489
+ const _i18nResp = await (async () => {
9490
+ if (req.method === "OPTIONS") {
9491
+ return new Response(null, { headers: corsHeaders });
9481
9492
  }
9482
- const now = new Date().toISOString();
9483
- if (tableInfo.some((c) => c.name === "created_at")) {
9484
- columns.push("created_at");
9485
- placeholders.push("?");
9486
- values.push(now);
9493
+ if (options.onRequest) {
9494
+ const customResponse = await options.onRequest(req);
9495
+ if (customResponse)
9496
+ return customResponse;
9487
9497
  }
9488
- if (tableInfo.some((c) => c.name === "updated_at")) {
9489
- columns.push("updated_at");
9490
- placeholders.push("?");
9491
- values.push(now);
9498
+ const route = resolveRouteMiddleware(path);
9499
+ const extraNames = extraProtectedPrefixes.some((p) => path === p || path.startsWith(p.endsWith("/") ? p : `${p}/`)) ? ["auth"] : [];
9500
+ const requested = [...globalMiddlewareNames, ...route.names, ...extraNames];
9501
+ if (requested.length > 0) {
9502
+ const expanded = expandMiddlewareNames(requested);
9503
+ const cookies = parseCookies(req);
9504
+ const ctx = {
9505
+ path,
9506
+ url,
9507
+ params: route.params,
9508
+ cookies,
9509
+ redirect: (to, status = 302) => {
9510
+ const sep = to.includes("?") ? "&" : "?";
9511
+ const next = encodeURIComponent(path + (url.search || ""));
9512
+ return new Response(null, {
9513
+ status,
9514
+ headers: { Location: `${to}${sep}next=${next}` }
9515
+ });
9516
+ }
9517
+ };
9518
+ for (const entry of expanded) {
9519
+ const colon = entry.indexOf(":");
9520
+ const name = colon === -1 ? entry : entry.slice(0, colon);
9521
+ const args = colon === -1 ? [] : entry.slice(colon + 1).split(",");
9522
+ const handler = middlewareRegistry[name];
9523
+ if (!handler) {
9524
+ console.warn(`[stx serve] unknown middleware "${name}" on ${path}`);
9525
+ continue;
9526
+ }
9527
+ const result = await handler(req, ctx, ...args);
9528
+ if (result instanceof Response)
9529
+ return result;
9530
+ }
9492
9531
  }
9493
- if (columns.length === 0) {
9494
- return new Response(JSON.stringify({ error: "No valid fields provided" }), {
9495
- status: 400,
9496
- headers: { "Content-Type": "application/json", ...corsHeaders }
9497
- });
9532
+ if (options.routes) {
9533
+ const routeHandler = options.routes[path];
9534
+ if (routeHandler) {
9535
+ return routeHandler(req);
9536
+ }
9498
9537
  }
9499
- const query = `INSERT INTO "${tableName}" (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
9500
- const stmt = db.prepare(query);
9501
- const result = stmt.run(...values);
9502
- return new Response(JSON.stringify({
9503
- success: true,
9504
- id: result.lastInsertRowid,
9505
- message: "Record created successfully"
9506
- }), {
9507
- status: 201,
9508
- headers: { "Content-Type": "application/json", ...corsHeaders }
9509
- });
9510
- } catch (error) {
9511
- console.error("API Error:", error);
9512
- return new Response(JSON.stringify({
9513
- error: error.message || "Failed to create record"
9514
- }), {
9515
- status: 500,
9516
- headers: { "Content-Type": "application/json", ...corsHeaders }
9517
- });
9518
- } finally {
9519
- db.close();
9520
- }
9521
- } catch {}
9522
- }
9523
- if (path === "/index")
9524
- path = "/";
9525
- if (path === "/") {
9526
- const indexContent = await getRoute("/");
9527
- if (!indexContent) {
9528
- const homeContent = await getRoute("/home");
9529
- if (homeContent) {
9530
- return new Response(null, {
9531
- status: 302,
9532
- headers: {
9533
- Location: "/home",
9534
- ...corsHeaders
9538
+ if (path.startsWith("/_stx/component/")) {
9539
+ const componentName = decodeURIComponent(path.slice("/_stx/component/".length));
9540
+ if (componentName) {
9541
+ try {
9542
+ const { processDirectives, defaultConfig: defaultConfig4 } = await stxModule;
9543
+ const componentTemplate = `<${componentName} />`;
9544
+ const componentOpts = {
9545
+ ...defaultConfig4,
9546
+ ...componentsDir && { componentsDir },
9547
+ ...layoutsDir && { layoutsDir },
9548
+ ...partialsDir && { partialsDir },
9549
+ autoShell: false
9550
+ };
9551
+ const html = await processDirectives(componentTemplate, {}, `${componentsDir}/${componentName}.stx`, componentOpts, new Set);
9552
+ return new Response(html, {
9553
+ headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
9554
+ });
9555
+ } catch (e) {
9556
+ return new Response(`<div class="stx-async-error">${e.message}</div>`, {
9557
+ status: 500,
9558
+ headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
9559
+ });
9560
+ }
9535
9561
  }
9536
- });
9537
- }
9538
- }
9539
- }
9540
- const content = await getRoute(path);
9541
- if (content) {
9542
- const isSpaNav = req.headers.get("X-STX-Router") === "true";
9543
- if (isSpaNav) {
9544
- const layoutMatch = content.match(/<!-- stx-layout: ([^ ]+) -->/);
9545
- const pageLayout = layoutMatch ? layoutMatch[1] : "default";
9546
- const pageLayoutGroup = deriveLayoutGroup(pageLayout);
9547
- let fragment = content;
9548
- const headStyles = [];
9549
- const headMatch = content.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
9550
- if (headMatch) {
9551
- const headContent = headMatch[1];
9552
- let styleMatch;
9553
- const styleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
9554
- while ((styleMatch = styleRe.exec(headContent)) !== null) {
9555
- if (styleMatch[0].includes("data-crosswind")) {
9556
- const cssContent = styleMatch[0].replace(/<style[^>]*>/, "").replace(/<\/style>/, "");
9557
- const hiddenRule = cssContent.indexOf("[hidden]");
9558
- if (hiddenRule !== -1) {
9559
- const afterPreflight = cssContent.indexOf("}", hiddenRule) + 1;
9560
- const utilities = cssContent.slice(afterPreflight).trim();
9561
- if (utilities) {
9562
- headStyles.push(`<style data-crosswind="fragment">${utilities}</style>`);
9562
+ }
9563
+ if (path.startsWith("/api/data/") && req.method === "POST") {
9564
+ try {
9565
+ const tableName = path.replace("/api/data/", "").split("/")[0];
9566
+ if (!tableName) {
9567
+ return new Response(JSON.stringify({ error: "Table name required" }), {
9568
+ status: 400,
9569
+ headers: { "Content-Type": "application/json", ...corsHeaders }
9570
+ });
9571
+ }
9572
+ if (!/^[a-zA-Z_]\w*$/.test(tableName)) {
9573
+ return new Response(JSON.stringify({ error: "Invalid table name" }), {
9574
+ status: 400,
9575
+ headers: { "Content-Type": "application/json", ...corsHeaders }
9576
+ });
9577
+ }
9578
+ const body = await req.json();
9579
+ const { Database } = await import("bun:sqlite");
9580
+ const dbPath = nodePath.resolve(process19.cwd(), "database/stacks.sqlite");
9581
+ const db = new Database(dbPath);
9582
+ try {
9583
+ const tableInfo = db.query(`PRAGMA table_info("${tableName}")`).all();
9584
+ const validColumns = tableInfo.map((c) => c.name).filter((n) => n !== "id" && n !== "created_at" && n !== "updated_at");
9585
+ const columns = [];
9586
+ const placeholders = [];
9587
+ const values = [];
9588
+ for (const col of validColumns) {
9589
+ if (body[col] !== undefined && body[col] !== "") {
9590
+ columns.push(col);
9591
+ placeholders.push("?");
9592
+ values.push(body[col]);
9593
+ }
9594
+ }
9595
+ const now = new Date().toISOString();
9596
+ if (tableInfo.some((c) => c.name === "created_at")) {
9597
+ columns.push("created_at");
9598
+ placeholders.push("?");
9599
+ values.push(now);
9600
+ }
9601
+ if (tableInfo.some((c) => c.name === "updated_at")) {
9602
+ columns.push("updated_at");
9603
+ placeholders.push("?");
9604
+ values.push(now);
9563
9605
  }
9606
+ if (columns.length === 0) {
9607
+ return new Response(JSON.stringify({ error: "No valid fields provided" }), {
9608
+ status: 400,
9609
+ headers: { "Content-Type": "application/json", ...corsHeaders }
9610
+ });
9611
+ }
9612
+ const query = `INSERT INTO "${tableName}" (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
9613
+ const stmt = db.prepare(query);
9614
+ const result = stmt.run(...values);
9615
+ return new Response(JSON.stringify({
9616
+ success: true,
9617
+ id: result.lastInsertRowid,
9618
+ message: "Record created successfully"
9619
+ }), {
9620
+ status: 201,
9621
+ headers: { "Content-Type": "application/json", ...corsHeaders }
9622
+ });
9623
+ } catch (error) {
9624
+ console.error("API Error:", error);
9625
+ return new Response(JSON.stringify({
9626
+ error: error.message || "Failed to create record"
9627
+ }), {
9628
+ status: 500,
9629
+ headers: { "Content-Type": "application/json", ...corsHeaders }
9630
+ });
9631
+ } finally {
9632
+ db.close();
9564
9633
  }
9565
- continue;
9566
- }
9567
- headStyles.push(styleMatch[0]);
9634
+ } catch {}
9568
9635
  }
9569
- }
9570
- const mainOpenMatch = fragment.match(/<main\b[^>]*>/i);
9571
- const mainCloseIdx = fragment.lastIndexOf("</main>");
9572
- if (mainOpenMatch && mainCloseIdx !== -1) {
9573
- const bodyMatch = content.match(/<body\b[^>]*>/i);
9574
- if (bodyMatch) {
9575
- const bodyStart = bodyMatch.index + bodyMatch[0].length;
9576
- const mainIdx = mainOpenMatch.index;
9577
- const beforeMain = content.slice(bodyStart, mainIdx);
9578
- let bodyStyleMatch;
9579
- const bodyStyleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
9580
- while ((bodyStyleMatch = bodyStyleRe.exec(beforeMain)) !== null) {
9581
- headStyles.push(bodyStyleMatch[0]);
9636
+ if (path === "/index")
9637
+ path = "/";
9638
+ if (path === "/") {
9639
+ const indexContent = await getRoute("/");
9640
+ if (!indexContent) {
9641
+ const homeContent = await getRoute("/home");
9642
+ if (homeContent) {
9643
+ return new Response(null, {
9644
+ status: 302,
9645
+ headers: {
9646
+ Location: "/home",
9647
+ ...corsHeaders
9648
+ }
9649
+ });
9650
+ }
9582
9651
  }
9583
9652
  }
9584
- const mainStart = mainOpenMatch.index + mainOpenMatch[0].length;
9585
- fragment = fragment.slice(mainStart, mainCloseIdx).trim();
9586
- }
9587
- const pageSetupScripts = [];
9588
- const allScriptRe = /<script\b[^>]*data-stx-scoped[^>]*>[\s\S]*?<\/script>/gi;
9589
- let setupMatch;
9590
- while ((setupMatch = allScriptRe.exec(content)) !== null) {
9591
- const scriptContent = setupMatch[0];
9592
- if (scriptContent.includes("__stx_early_mounts"))
9593
- continue;
9594
- if (scriptContent.includes("data-stx-reactive") && scriptContent.includes("window.__stx_reactive"))
9595
- continue;
9596
- pageSetupScripts.push(scriptContent);
9597
- }
9598
- const bridgeInitRe = /<script\b[^>]*data-stx-reactive[^>]*>(?![\s\S]*window\.__stx_reactive)[\s\S]*?<\/script>/gi;
9599
- while ((setupMatch = bridgeInitRe.exec(content)) !== null) {
9600
- pageSetupScripts.push(setupMatch[0]);
9601
- }
9602
- fragment = fragment.replace(/<script data-stx-scoped>\s*;?\(function\(\)\s*\{[\s\S]*?<\/script>/g, "");
9603
- const clearStale = "<script data-stx-page>if(window.stx)window.stx._latestSetup=null;</script>";
9604
- fragment = `${headStyles.join(`
9653
+ const content = await getRoute(path);
9654
+ if (content) {
9655
+ const isSpaNav = req.headers.get("X-STX-Router") === "true";
9656
+ if (isSpaNav) {
9657
+ const layoutMatch = content.match(/<!-- stx-layout: ([^ ]+) -->/);
9658
+ const pageLayout = layoutMatch ? layoutMatch[1] : "default";
9659
+ const pageLayoutGroup = deriveLayoutGroup(pageLayout);
9660
+ let fragment = content;
9661
+ const headStyles = [];
9662
+ const headMatch = content.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
9663
+ if (headMatch) {
9664
+ const headContent = headMatch[1];
9665
+ let styleMatch;
9666
+ const styleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
9667
+ while ((styleMatch = styleRe.exec(headContent)) !== null) {
9668
+ if (styleMatch[0].includes("data-crosswind")) {
9669
+ const cssContent = styleMatch[0].replace(/<style[^>]*>/, "").replace(/<\/style>/, "");
9670
+ const hiddenRule = cssContent.indexOf("[hidden]");
9671
+ if (hiddenRule !== -1) {
9672
+ const afterPreflight = cssContent.indexOf("}", hiddenRule) + 1;
9673
+ const utilities = cssContent.slice(afterPreflight).trim();
9674
+ if (utilities) {
9675
+ headStyles.push(`<style data-crosswind="fragment">${utilities}</style>`);
9676
+ }
9677
+ }
9678
+ continue;
9679
+ }
9680
+ headStyles.push(styleMatch[0]);
9681
+ }
9682
+ }
9683
+ const mainOpenMatch = fragment.match(/<main\b[^>]*>/i);
9684
+ const mainCloseIdx = fragment.lastIndexOf("</main>");
9685
+ if (mainOpenMatch && mainCloseIdx !== -1) {
9686
+ const bodyMatch = content.match(/<body\b[^>]*>/i);
9687
+ if (bodyMatch) {
9688
+ const bodyStart = bodyMatch.index + bodyMatch[0].length;
9689
+ const mainIdx = mainOpenMatch.index;
9690
+ const beforeMain = content.slice(bodyStart, mainIdx);
9691
+ let bodyStyleMatch;
9692
+ const bodyStyleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
9693
+ while ((bodyStyleMatch = bodyStyleRe.exec(beforeMain)) !== null) {
9694
+ headStyles.push(bodyStyleMatch[0]);
9695
+ }
9696
+ }
9697
+ const mainStart = mainOpenMatch.index + mainOpenMatch[0].length;
9698
+ fragment = fragment.slice(mainStart, mainCloseIdx).trim();
9699
+ }
9700
+ const pageSetupScripts = [];
9701
+ const allScriptRe = /<script\b[^>]*data-stx-scoped[^>]*>[\s\S]*?<\/script>/gi;
9702
+ let setupMatch;
9703
+ while ((setupMatch = allScriptRe.exec(content)) !== null) {
9704
+ const scriptContent = setupMatch[0];
9705
+ if (scriptContent.includes("__stx_early_mounts"))
9706
+ continue;
9707
+ if (scriptContent.includes("data-stx-reactive") && scriptContent.includes("window.__stx_reactive"))
9708
+ continue;
9709
+ pageSetupScripts.push(scriptContent);
9710
+ }
9711
+ const bridgeInitRe = /<script\b[^>]*data-stx-reactive[^>]*>(?![\s\S]*window\.__stx_reactive)[\s\S]*?<\/script>/gi;
9712
+ while ((setupMatch = bridgeInitRe.exec(content)) !== null) {
9713
+ pageSetupScripts.push(setupMatch[0]);
9714
+ }
9715
+ fragment = fragment.replace(/<script data-stx-scoped>\s*;?\(function\(\)\s*\{[\s\S]*?<\/script>/g, "");
9716
+ const clearStale = "<script data-stx-page>if(window.stx)window.stx._latestSetup=null;</script>";
9717
+ fragment = `${headStyles.join(`
9605
9718
  `)}
9606
9719
  ${fragment}
9607
9720
  ${clearStale}
9608
9721
  ${pageSetupScripts.join(`
9609
9722
  `)}`;
9610
- return new Response(fragment, {
9611
- headers: {
9612
- "Content-Type": "text/html; charset=utf-8",
9613
- "X-STX-Fragment": "true",
9614
- "X-STX-Layout": pageLayout,
9615
- "X-STX-Layout-Group": pageLayoutGroup,
9616
- "Cache-Control": "no-cache",
9617
- ...corsHeaders
9618
- }
9619
- });
9620
- }
9621
- let cleaned = content;
9622
- let runtimeCount = 0;
9623
- cleaned = content.replace(/<script data-stx-scoped>\(function\(\)\{'use strict';var cloakStyle[\s\S]*?<\/script>/g, (match) => {
9624
- runtimeCount++;
9625
- return runtimeCount === 1 ? match : "";
9626
- });
9627
- return new Response(cleaned, {
9628
- headers: {
9629
- "Content-Type": "text/html; charset=utf-8",
9630
- "Cache-Control": "no-cache",
9631
- ...corsHeaders
9632
- }
9633
- });
9634
- }
9635
- const contentWithExt = await getRoute(`${path}.html`);
9636
- if (contentWithExt) {
9637
- return new Response(contentWithExt, {
9638
- headers: {
9639
- "Content-Type": "text/html; charset=utf-8",
9640
- "Cache-Control": "no-cache",
9641
- ...corsHeaders
9642
- }
9643
- });
9644
- }
9645
- if (path.startsWith("/chunk-") || path.endsWith(".js") || path.endsWith(".css")) {
9646
- try {
9647
- const buildFile = Bun.file(`.stx${path}`);
9648
- if (await buildFile.exists()) {
9649
- const ext = path.split(".").pop()?.toLowerCase();
9650
- const contentType = ext === "css" ? "text/css" : ext === "js" ? "application/javascript" : "text/plain";
9651
- return new Response(buildFile, {
9652
- headers: {
9653
- "Content-Type": contentType,
9654
- "Cache-Control": "no-cache"
9655
- }
9656
- });
9657
- }
9658
- } catch {}
9659
- }
9660
- if (path.startsWith("/assets/") || path.startsWith("/resources/assets/")) {
9661
- await ensureAssets();
9662
- const possiblePaths = [
9663
- path,
9664
- path.replace(/^\/assets\//, "/resources/assets/"),
9665
- path.replace(/^\/resources\/assets\//, "/assets/")
9666
- ];
9667
- for (const assetPath of possiblePaths) {
9668
- try {
9669
- const filePath = `.${assetPath}`;
9670
- const file = Bun.file(filePath);
9671
- if (await file.exists()) {
9672
- const ext = assetPath.split(".").pop()?.toLowerCase();
9673
- if (ext === "ts") {
9674
- const transpiler = new Bun.Transpiler({
9675
- loader: "ts"
9676
- });
9677
- const code = await file.text();
9678
- const transpiled = transpiler.transformSync(code);
9679
- return new Response(transpiled, {
9723
+ return new Response(fragment, {
9680
9724
  headers: {
9681
- "Content-Type": "application/javascript",
9682
- "Cache-Control": "no-cache"
9725
+ "Content-Type": "text/html; charset=utf-8",
9726
+ "X-STX-Fragment": "true",
9727
+ "X-STX-Layout": pageLayout,
9728
+ "X-STX-Layout-Group": pageLayoutGroup,
9729
+ "Cache-Control": "no-cache",
9730
+ ...corsHeaders
9683
9731
  }
9684
9732
  });
9685
9733
  }
9686
- return new Response(file, {
9734
+ let cleaned = content;
9735
+ let runtimeCount = 0;
9736
+ cleaned = content.replace(/<script data-stx-scoped>\(function\(\)\{'use strict';var cloakStyle[\s\S]*?<\/script>/g, (match) => {
9737
+ runtimeCount++;
9738
+ return runtimeCount === 1 ? match : "";
9739
+ });
9740
+ return new Response(cleaned, {
9687
9741
  headers: {
9688
- "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9689
- "Cache-Control": "public, max-age=31536000"
9742
+ "Content-Type": "text/html; charset=utf-8",
9743
+ "Cache-Control": "no-cache",
9744
+ ...corsHeaders
9690
9745
  }
9691
9746
  });
9692
9747
  }
9693
- } catch {
9694
- continue;
9695
- }
9696
- }
9697
- }
9698
- if ((req.method === "GET" || req.method === "HEAD") && path !== "/") {
9699
- const publicRoot = nodePath.resolve(process19.cwd(), publicDir);
9700
- let safePathname;
9701
- try {
9702
- safePathname = decodeURIComponent(path);
9703
- } catch {
9704
- safePathname = path;
9705
- }
9706
- if (!safePathname.includes("\x00")) {
9707
- const resolvedPath = nodePath.resolve(publicRoot, `.${safePathname}`);
9708
- const isInsidePublicRoot = resolvedPath === publicRoot || resolvedPath.startsWith(`${publicRoot}${nodePath.sep}`);
9709
- if (isInsidePublicRoot) {
9710
- try {
9711
- const file = Bun.file(resolvedPath);
9712
- if (await file.exists()) {
9713
- const stat4 = await file.stat().catch(() => null);
9714
- if (stat4 && !stat4.isDirectory()) {
9715
- const ext = resolvedPath.split(".").pop()?.toLowerCase();
9716
- return new Response(file, {
9748
+ const contentWithExt = await getRoute(`${path}.html`);
9749
+ if (contentWithExt) {
9750
+ return new Response(contentWithExt, {
9751
+ headers: {
9752
+ "Content-Type": "text/html; charset=utf-8",
9753
+ "Cache-Control": "no-cache",
9754
+ ...corsHeaders
9755
+ }
9756
+ });
9757
+ }
9758
+ if (path.startsWith("/chunk-") || path.endsWith(".js") || path.endsWith(".css")) {
9759
+ try {
9760
+ const buildFile = Bun.file(`.stx${path}`);
9761
+ if (await buildFile.exists()) {
9762
+ const ext = path.split(".").pop()?.toLowerCase();
9763
+ const contentType = ext === "css" ? "text/css" : ext === "js" ? "application/javascript" : "text/plain";
9764
+ return new Response(buildFile, {
9717
9765
  headers: {
9718
- "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9719
- "Cache-Control": "public, max-age=3600"
9766
+ "Content-Type": contentType,
9767
+ "Cache-Control": "no-cache"
9720
9768
  }
9721
9769
  });
9722
9770
  }
9771
+ } catch {}
9772
+ }
9773
+ if (path.startsWith("/assets/") || path.startsWith("/resources/assets/")) {
9774
+ 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) {
9781
+ try {
9782
+ const filePath = `.${assetPath}`;
9783
+ const file = Bun.file(filePath);
9784
+ 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
+ }
9799
+ return new Response(file, {
9800
+ headers: {
9801
+ "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9802
+ "Cache-Control": "public, max-age=31536000"
9803
+ }
9804
+ });
9805
+ }
9806
+ } catch {
9807
+ continue;
9808
+ }
9723
9809
  }
9724
- } catch {}
9725
- }
9726
- }
9727
- }
9728
- if (path === "/favicon.ico") {
9729
- return new Response(null, { status: 204 });
9730
- }
9731
- const files = await discoverFiles();
9732
- const availableRoutes = [];
9733
- for (const filePath of files) {
9734
- let normalizedPath = filePath.replace(/^\.\//, "").replace(/\\/g, "/");
9735
- for (const pattern of patterns) {
9736
- const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\/$/, "");
9737
- if (normalizedPath.startsWith(`${normalizedPattern}/`)) {
9738
- normalizedPath = normalizedPath.slice(normalizedPattern.length + 1);
9739
- break;
9740
- }
9741
- }
9742
- let route2 = normalizedPath.replace(/\.(stx|md|html)$/, "");
9743
- if (route2.startsWith("pages/")) {
9744
- route2 = route2.slice(6);
9745
- }
9746
- availableRoutes.push(`<li><a href="/${route2}">/${route2}</a></li>`);
9747
- }
9748
- const routesList = availableRoutes.join(`
9810
+ }
9811
+ if ((req.method === "GET" || req.method === "HEAD") && path !== "/") {
9812
+ const publicRoot = nodePath.resolve(process19.cwd(), publicDir);
9813
+ let safePathname;
9814
+ try {
9815
+ safePathname = decodeURIComponent(path);
9816
+ } catch {
9817
+ safePathname = path;
9818
+ }
9819
+ if (!safePathname.includes("\x00")) {
9820
+ const resolvedPath = nodePath.resolve(publicRoot, `.${safePathname}`);
9821
+ const isInsidePublicRoot = resolvedPath === publicRoot || resolvedPath.startsWith(`${publicRoot}${nodePath.sep}`);
9822
+ if (isInsidePublicRoot) {
9823
+ try {
9824
+ const file = Bun.file(resolvedPath);
9825
+ if (await file.exists()) {
9826
+ const stat4 = await file.stat().catch(() => null);
9827
+ if (stat4 && !stat4.isDirectory()) {
9828
+ const ext = resolvedPath.split(".").pop()?.toLowerCase();
9829
+ return new Response(file, {
9830
+ headers: {
9831
+ "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
9832
+ "Cache-Control": "public, max-age=3600"
9833
+ }
9834
+ });
9835
+ }
9836
+ }
9837
+ } catch {}
9838
+ }
9839
+ }
9840
+ }
9841
+ if (path === "/favicon.ico") {
9842
+ return new Response(null, { status: 204 });
9843
+ }
9844
+ const files = await discoverFiles();
9845
+ const availableRoutes = [];
9846
+ for (const filePath of files) {
9847
+ let normalizedPath = filePath.replace(/^\.\//, "").replace(/\\/g, "/");
9848
+ for (const pattern of patterns) {
9849
+ const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\/$/, "");
9850
+ if (normalizedPath.startsWith(`${normalizedPattern}/`)) {
9851
+ normalizedPath = normalizedPath.slice(normalizedPattern.length + 1);
9852
+ break;
9853
+ }
9854
+ }
9855
+ let route2 = normalizedPath.replace(/\.(stx|md|html)$/, "");
9856
+ if (route2.startsWith("pages/")) {
9857
+ route2 = route2.slice(6);
9858
+ }
9859
+ availableRoutes.push(`<li><a href="/${route2}">/${route2}</a></li>`);
9860
+ }
9861
+ const routesList = availableRoutes.join(`
9749
9862
  `);
9750
- return new Response(`
9863
+ return new Response(`
9751
9864
  <!DOCTYPE html>
9752
9865
  <html>
9753
9866
  <head>
@@ -9775,11 +9888,35 @@ ${pageSetupScripts.join(`
9775
9888
  </body>
9776
9889
  </html>
9777
9890
  `, {
9778
- status: 404,
9779
- headers: { "Content-Type": "text/html; charset=utf-8" }
9891
+ status: 404,
9892
+ headers: { "Content-Type": "text/html; charset=utf-8" }
9893
+ });
9894
+ })();
9895
+ return applyI18nToResponse(_i18nResp, i18nLocale ?? (i18nConfig?.defaultLocale ?? "en"), path);
9896
+ }
9780
9897
  });
9898
+ break;
9899
+ } catch (err) {
9900
+ lastServeError = err;
9901
+ const code = err?.code;
9902
+ const message = String(err?.message ?? err ?? "");
9903
+ const portConflict = code === "EADDRINUSE" || /EADDRINUSE/i.test(message) || /port \d+ (?:is |already )?in use/i.test(message) || /Failed to start server\. Is port \d+ in use\?/i.test(message);
9904
+ if (portConflict && attempt < maxPortAttempts - 1) {
9905
+ actualPort += 1;
9906
+ continue;
9907
+ }
9908
+ throw err;
9781
9909
  }
9782
- });
9910
+ }
9911
+ if (!_server) {
9912
+ const tried = maxPortAttempts === 1 ? `port ${port}` : `ports ${port}..${port + maxPortAttempts - 1}`;
9913
+ console.error(`\x1B[31m[stx]\x1B[0m no free port available (tried ${tried}).`);
9914
+ console.error(`\x1B[2m set autoIncrementPort to a larger value, or free the port (lsof -nP -i :${port})\x1B[0m`);
9915
+ throw lastServeError ?? new Error(`stx serve: no free port in range ${port}..${port + maxPortAttempts - 1}`);
9916
+ }
9917
+ if (actualPort !== port && !options.quiet) {
9918
+ console.warn(`\x1B[33m[stx]\x1B[0m port ${port} in use \u2014 using \x1B[1m${actualPort}\x1B[0m instead`);
9919
+ }
9783
9920
  if (!options.quiet) {
9784
9921
  const elapsed = (performance.now() - startTime).toFixed(0);
9785
9922
  const routeCount = sourceFiles?.length || 0;
@@ -9787,7 +9924,7 @@ ${pageSetupScripts.join(`
9787
9924
  console.log();
9788
9925
  console.log(` \x1B[36m\x1B[1mstx\x1B[0m`);
9789
9926
  console.log();
9790
- console.log(` \x1B[32m\u279C\x1B[0m \x1B[1mLocal\x1B[0m: \x1B[36mhttp://localhost:${port}/\x1B[0m`);
9927
+ console.log(` \x1B[32m\u279C\x1B[0m \x1B[1mLocal\x1B[0m: \x1B[36mhttp://localhost:${actualPort}/\x1B[0m`);
9791
9928
  console.log(` \x1B[32m\u279C\x1B[0m \x1B[1mRoutes\x1B[0m: \x1B[2m${routeCount} files from ${patternsStr}\x1B[0m`);
9792
9929
  console.log();
9793
9930
  console.log(` \x1B[2mready in ${elapsed}ms\x1B[0m`);
@@ -9803,7 +9940,7 @@ ${pageSetupScripts.join(`
9803
9940
  const cmd = line.trim().toLowerCase();
9804
9941
  if (cmd === "o") {
9805
9942
  const openCmd = process19.platform === "darwin" ? "open" : process19.platform === "win32" ? "start" : "xdg-open";
9806
- Bun.spawn([openCmd, `http://localhost:${port}/`]);
9943
+ Bun.spawn([openCmd, `http://localhost:${actualPort}/`]);
9807
9944
  } else if (cmd === "q") {
9808
9945
  process19.exit(0);
9809
9946
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.54",
4
+ "version": "0.2.63",
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.54",
77
- "stx-router": "0.2.53"
76
+ "@stacksjs/stx": "0.2.63",
77
+ "stx-router": "0.2.57"
78
78
  }
79
79
  }