bun-plugin-stx 0.2.9 → 0.2.11

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/dist/serve.js CHANGED
@@ -5064,9 +5064,39 @@ var defaultStxConfig = {
5064
5064
  partialsDir: "partials",
5065
5065
  componentsDir: "components",
5066
5066
  layoutsDir: "layouts",
5067
+ publicDir: "public",
5067
5068
  ssr: true,
5068
5069
  defaultTitle: "stx App"
5069
5070
  };
5071
+ var staticContentTypes = {
5072
+ css: "text/css",
5073
+ js: "application/javascript",
5074
+ mjs: "application/javascript",
5075
+ json: "application/json",
5076
+ html: "text/html; charset=utf-8",
5077
+ txt: "text/plain",
5078
+ xml: "application/xml",
5079
+ pdf: "application/pdf",
5080
+ png: "image/png",
5081
+ jpg: "image/jpeg",
5082
+ jpeg: "image/jpeg",
5083
+ gif: "image/gif",
5084
+ webp: "image/webp",
5085
+ avif: "image/avif",
5086
+ svg: "image/svg+xml",
5087
+ ico: "image/x-icon",
5088
+ bmp: "image/bmp",
5089
+ mp4: "video/mp4",
5090
+ webm: "video/webm",
5091
+ ogg: "audio/ogg",
5092
+ mp3: "audio/mpeg",
5093
+ wav: "audio/wav",
5094
+ woff: "font/woff",
5095
+ woff2: "font/woff2",
5096
+ ttf: "font/ttf",
5097
+ otf: "font/otf",
5098
+ eot: "application/vnd.ms-fontobject"
5099
+ };
5070
5100
  async function serve(options) {
5071
5101
  const stxConfig = await loadConfig5({
5072
5102
  name: "stx",
@@ -5076,6 +5106,7 @@ async function serve(options) {
5076
5106
  const componentsDir = options.componentsDir ?? stxConfig.componentsDir ?? defaultStxConfig.componentsDir;
5077
5107
  const layoutsDir = options.layoutsDir ?? stxConfig.layoutsDir ?? defaultStxConfig.layoutsDir;
5078
5108
  const partialsDir = options.partialsDir ?? stxConfig.partialsDir ?? defaultStxConfig.partialsDir;
5109
+ const publicDir = options.publicDir ?? stxConfig.publicDir ?? "public";
5079
5110
  const { patterns, port = 3456 } = options;
5080
5111
  if (patterns.length === 0) {
5081
5112
  console.error("Usage: serve <files...> [--port 3000]");
@@ -5103,10 +5134,17 @@ After installing: bun add bun-plugin-stx`);
5103
5134
  const fs = await import("fs/promises");
5104
5135
  const stat3 = await fs.stat(pattern).catch(() => null);
5105
5136
  if (stat3?.isDirectory()) {
5137
+ const excludeDirs = ["layouts", "components", "partials"];
5138
+ if (layoutsDir)
5139
+ excludeDirs.push(layoutsDir.replace(/^.*\//, ""));
5140
+ if (componentsDir)
5141
+ excludeDirs.push(componentsDir.replace(/^.*\//, ""));
5142
+ if (partialsDir)
5143
+ excludeDirs.push(partialsDir.replace(/^.*\//, ""));
5106
5144
  for (const ext of [".stx", ".md", ".html"]) {
5107
5145
  const glob = new Glob(`**/*${ext}`);
5108
5146
  const discovered = await Array.fromAsync(glob.scan({ cwd: pattern, followSymlinks: true }));
5109
- files.push(...discovered.map((f) => `${pattern}/${f}`.replace(/\/+/g, "/")));
5147
+ files.push(...discovered.filter((f) => !excludeDirs.some((dir) => f.startsWith(`${dir}/`))).map((f) => `${pattern}/${f}`.replace(/\/+/g, "/")));
5110
5148
  }
5111
5149
  } else if (pattern.includes("*")) {
5112
5150
  const glob = new Glob(pattern);
@@ -5121,6 +5159,12 @@ After installing: bun add bun-plugin-stx`);
5121
5159
  }
5122
5160
  }
5123
5161
  sourceFiles = files;
5162
+ try {
5163
+ const { Router } = await import("./chunk-c60m6z2p.js");
5164
+ const pagesDir = patterns[0]?.replace(/\/$/, "") || "pages";
5165
+ const router = new Router(process13.cwd(), { pagesDir });
5166
+ console.log(`[stx] Generated ${router.routes.length} routes \u2192 .stx/routes.ts`);
5167
+ } catch (e) {}
5124
5168
  return files;
5125
5169
  }
5126
5170
  async function ensureAssets() {
@@ -5145,7 +5189,7 @@ After installing: bun add bun-plugin-stx`);
5145
5189
  return crosswindModule;
5146
5190
  crosswindLoadAttempted = true;
5147
5191
  try {
5148
- const mod = await import("./chunk-fj6fej0j.js");
5192
+ const mod = await import("@cwcss/crosswind");
5149
5193
  crosswindModule = { CSSGenerator: mod.CSSGenerator, config: mod.config };
5150
5194
  return crosswindModule;
5151
5195
  } catch {
@@ -5175,13 +5219,89 @@ After installing: bun add bun-plugin-stx`);
5175
5219
  }
5176
5220
  match = classRegex.exec(htmlContent);
5177
5221
  }
5222
+ const dynRegex = /(?:x-class|:class)\s*=\s*"([^"]+)"/gi;
5223
+ let dynMatch = dynRegex.exec(htmlContent);
5224
+ while (dynMatch !== null) {
5225
+ const strLiterals = dynMatch[1].match(/'([^']+)'/g);
5226
+ if (strLiterals) {
5227
+ for (const lit of strLiterals) {
5228
+ const unquoted = lit.slice(1, -1);
5229
+ for (const cls of unquoted.split(/\s+/)) {
5230
+ if (cls.trim())
5231
+ classes.add(cls.trim());
5232
+ }
5233
+ }
5234
+ }
5235
+ dynMatch = dynRegex.exec(htmlContent);
5236
+ }
5178
5237
  if (classes.size === 0)
5179
5238
  return "";
5180
- const generator = new cw.CSSGenerator({ ...cw.config, preflight: true, minify: false });
5239
+ let userConfig = {};
5240
+ try {
5241
+ const nodePath = await import("path");
5242
+ const configPath = nodePath.join(process13.cwd(), "crosswind.config.ts");
5243
+ console.log("[crosswind] loading config from:", configPath);
5244
+ userConfig = (await import(configPath)).default || {};
5245
+ console.log("[crosswind] config loaded, keys:", Object.keys(userConfig));
5246
+ } catch (e) {
5247
+ console.warn("[crosswind] config load error:", e?.message || e);
5248
+ }
5249
+ if (userConfig.safelist) {
5250
+ for (const cls of userConfig.safelist)
5251
+ classes.add(cls);
5252
+ console.log("[crosswind] safelist:", userConfig.safelist);
5253
+ }
5254
+ if (userConfig.shortcuts) {
5255
+ console.log("[crosswind] shortcuts:", Object.keys(userConfig.shortcuts));
5256
+ }
5257
+ const generatorConfig = { ...cw.config, preflight: true, minify: false };
5258
+ if (userConfig.shortcuts) {
5259
+ generatorConfig.shortcuts = { ...generatorConfig.shortcuts || {}, ...userConfig.shortcuts };
5260
+ }
5261
+ if (userConfig.theme) {
5262
+ generatorConfig.theme = { ...generatorConfig.theme || {}, ...userConfig.theme };
5263
+ }
5264
+ const generator = new cw.CSSGenerator(generatorConfig);
5181
5265
  for (const className of classes) {
5182
5266
  generator.generate(className);
5183
5267
  }
5184
- return generator.toCSS(true, false);
5268
+ let shortcutCSS = "";
5269
+ const shortcuts = generatorConfig.shortcuts || {};
5270
+ for (const [name, classStr] of Object.entries(shortcuts)) {
5271
+ if (!classes.has(name))
5272
+ continue;
5273
+ const parts = classStr.split(/\s+/).filter(Boolean);
5274
+ for (const p of parts)
5275
+ generator.generate(p);
5276
+ }
5277
+ const baseCss = generator.toCSS(true, false);
5278
+ for (const [name, classStr] of Object.entries(shortcuts)) {
5279
+ if (!classes.has(name))
5280
+ continue;
5281
+ const parts = classStr.split(/\s+/).filter(Boolean);
5282
+ const decls = [];
5283
+ const darkDecls = [];
5284
+ for (const cls of parts) {
5285
+ const isDark = cls.startsWith("dark:");
5286
+ const actualCls = isDark ? cls.slice(5) : cls;
5287
+ const escaped = actualCls.replace(/[/:.[\]%()]/g, (c) => `\\${c}`);
5288
+ const re = new RegExp(`\\.${escaped}\\s*\\{([^}]+)\\}`);
5289
+ const m = baseCss.match(re);
5290
+ if (m) {
5291
+ if (isDark)
5292
+ darkDecls.push(m[1].trim());
5293
+ else
5294
+ decls.push(m[1].trim());
5295
+ }
5296
+ }
5297
+ if (decls.length)
5298
+ shortcutCSS += `.${name} { ${decls.join(" ")} }
5299
+ `;
5300
+ if (darkDecls.length)
5301
+ shortcutCSS += `@media (prefers-color-scheme: dark) { .dark .${name} { ${darkDecls.join(" ")} } }
5302
+ `;
5303
+ }
5304
+ return baseCss + shortcutCSS;
5185
5305
  } catch (error) {
5186
5306
  console.warn("Failed to generate Crosswind CSS:", error);
5187
5307
  return "";
@@ -5190,35 +5310,33 @@ After installing: bun add bun-plugin-stx`);
5190
5310
  async function processTemplate(filePath) {
5191
5311
  const path = await import("path");
5192
5312
  const content = await Bun.file(filePath).text();
5193
- const scriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
5194
- const clientScripts = [];
5313
+ const serverScriptRegex = /<script\b([^>]*)\bserver\b[^>]*>([\s\S]*?)<\/script>/gi;
5195
5314
  const serverScripts = [];
5196
5315
  let scriptMatch;
5197
- while ((scriptMatch = scriptRegex.exec(content)) !== null) {
5198
- const attrs = scriptMatch[1];
5199
- const scriptBody = scriptMatch[2];
5200
- const fullScript = scriptMatch[0];
5201
- const isClientScript = attrs.includes("client") || attrs.includes('type="module"') || attrs.includes("src=");
5202
- if (isClientScript) {
5203
- clientScripts.push(fullScript);
5204
- } else {
5205
- serverScripts.push(scriptBody);
5206
- }
5316
+ while ((scriptMatch = serverScriptRegex.exec(content)) !== null) {
5317
+ serverScripts.push(scriptMatch[2]);
5207
5318
  }
5208
- let templateContent = content.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
5319
+ let templateContent = content.replace(/<script\b[^>]*\bserver\b[^>]*>[\s\S]*?<\/script>/gi, "");
5209
5320
  const context = {
5210
5321
  __filename: filePath,
5211
5322
  __dirname: path.dirname(filePath)
5212
5323
  };
5213
- const { processDirectives, extractVariables, defaultConfig: defaultConfig3, generateSpaShell, injectRouterScript } = await import("@stacksjs/stx");
5324
+ const { processDirectives, extractVariables, defaultConfig: defaultConfig3, generateSpaShell, injectRouterScript, resetHead } = await import("@stacksjs/stx");
5325
+ resetHead();
5214
5326
  for (const scriptBody of serverScripts) {
5215
5327
  await extractVariables(scriptBody, context, filePath);
5216
5328
  }
5329
+ context.__stx_head_preset = true;
5217
5330
  const config3 = {
5218
5331
  ...defaultConfig3,
5219
5332
  ...componentsDir && { componentsDir },
5220
5333
  ...layoutsDir && { layoutsDir },
5221
- ...partialsDir && { partialsDir }
5334
+ ...partialsDir && { partialsDir },
5335
+ autoShell: true,
5336
+ ssr: stxConfig.ssr ?? defaultStxConfig.ssr ?? true,
5337
+ app: stxConfig.app || {},
5338
+ ..."strict" in stxConfig && { strict: stxConfig.strict },
5339
+ ..."router" in stxConfig && { router: stxConfig.router }
5222
5340
  };
5223
5341
  if (config3.ssr === false || stxConfig.ssr === false) {
5224
5342
  return generateSpaShell({
@@ -5230,11 +5348,12 @@ After installing: bun add bun-plugin-stx`);
5230
5348
  let output = templateContent;
5231
5349
  const dependencies = new Set;
5232
5350
  output = await processDirectives(output, context, filePath, config3, dependencies);
5233
- output = injectRouterScript(output);
5234
- const hasDirectiveTemplates = /<template\s[^>]*(?:@for|:for|@if|:if)/.test(output);
5351
+ output = await injectRouterScript(output);
5352
+ const directiveTemplateRe = /@for|:for|@if|:if|x-for|x-if/;
5353
+ const hasDirectiveTemplates = /<template\s[^>]*(?:@for|:for|@if|:if|x-for|x-if)/.test(output);
5235
5354
  if (hasDirectiveTemplates) {
5236
5355
  output = output.replace(/<template(?:\s[^>]*)?>|<\/template>/gi, (match) => {
5237
- if (/@for|:for|@if|:if/.test(match))
5356
+ if (directiveTemplateRe.test(match))
5238
5357
  return match;
5239
5358
  if (match === "</template>")
5240
5359
  return match;
@@ -5243,25 +5362,9 @@ After installing: bun add bun-plugin-stx`);
5243
5362
  } else {
5244
5363
  output = output.replace(/<template[^>]*>/gi, "").replace(/<\/template>/gi, "");
5245
5364
  }
5246
- const crosswindCSS = await generateCrosswindCSS(output);
5247
- if (crosswindCSS) {
5248
- const styleTag = `<style>/* crosswind css */
5249
- ${crosswindCSS}</style>`;
5250
- if (output.includes("</head>")) {
5251
- output = output.replace("</head>", `${styleTag}
5252
- </head>`);
5253
- } else if (output.includes("<body")) {
5254
- output = output.replace(/<body/i, `${styleTag}
5255
- <body`);
5256
- } else {
5257
- output = styleTag + output;
5258
- }
5259
- }
5260
5365
  return output;
5261
5366
  }
5262
5367
  async function getRoute(requestPath) {
5263
- if (routes.has(requestPath))
5264
- return routes.get(requestPath);
5265
5368
  const files = await discoverFiles();
5266
5369
  const nodePath = await import("path");
5267
5370
  let normalizedPath = requestPath.startsWith("/") ? requestPath.slice(1) : requestPath;
@@ -5290,7 +5393,6 @@ ${crosswindCSS}</style>`;
5290
5393
  const normalizedFilePath = filePath.replace(/^\.\//, "").replace(/\\/g, "/");
5291
5394
  if (normalizedFilePath === possible || normalizedFilePath.endsWith(`/${possible}`)) {
5292
5395
  const output = await processTemplate(filePath);
5293
- routes.set(requestPath, output);
5294
5396
  return output;
5295
5397
  }
5296
5398
  }
@@ -5359,22 +5461,13 @@ ${crosswindCSS}</style>`;
5359
5461
  async function processTemplateDynamic(filePath, paramNames, paramValues, routePath) {
5360
5462
  const path = await import("path");
5361
5463
  const content = await Bun.file(filePath).text();
5362
- const dynScriptRegex = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
5363
- const dynClientScripts = [];
5464
+ const serverScriptRegex = /<script\b([^>]*)\bserver\b[^>]*>([\s\S]*?)<\/script>/gi;
5364
5465
  const dynServerScripts = [];
5365
5466
  let dynScriptMatch;
5366
- while ((dynScriptMatch = dynScriptRegex.exec(content)) !== null) {
5367
- const attrs = dynScriptMatch[1];
5368
- const scriptBody = dynScriptMatch[2];
5369
- const fullScript = dynScriptMatch[0];
5370
- const isClientScript = attrs.includes("client") || attrs.includes('type="module"') || attrs.includes("src=");
5371
- if (isClientScript) {
5372
- dynClientScripts.push(fullScript);
5373
- } else {
5374
- dynServerScripts.push(scriptBody);
5375
- }
5467
+ while ((dynScriptMatch = serverScriptRegex.exec(content)) !== null) {
5468
+ dynServerScripts.push(dynScriptMatch[2]);
5376
5469
  }
5377
- const templateContent = content.replace(/<script\b[^>]*>[\s\S]*?<\/script>/gi, "");
5470
+ const templateContent = content.replace(/<script\b[^>]*\bserver\b[^>]*>[\s\S]*?<\/script>/gi, "");
5378
5471
  const context = {
5379
5472
  __filename: filePath,
5380
5473
  __dirname: path.dirname(filePath),
@@ -5383,39 +5476,53 @@ ${crosswindCSS}</style>`;
5383
5476
  for (let i = 0;i < paramNames.length; i++) {
5384
5477
  context[paramNames[i]] = paramValues[i];
5385
5478
  }
5386
- const { processDirectives, extractVariables, defaultConfig: defaultConfig3, injectRouterScript: injectRouter } = await import("@stacksjs/stx");
5479
+ const { processDirectives, extractVariables, defaultConfig: defaultConfig3, injectRouterScript: injectRouter, resetHead: resetHeadDyn } = await import("@stacksjs/stx");
5480
+ resetHeadDyn();
5387
5481
  for (const scriptBody of dynServerScripts) {
5388
5482
  await extractVariables(scriptBody, context, filePath);
5389
5483
  }
5484
+ context.__stx_head_preset = true;
5390
5485
  const config3 = {
5391
5486
  ...defaultConfig3,
5392
5487
  ...componentsDir && { componentsDir },
5393
5488
  ...layoutsDir && { layoutsDir },
5394
- ...partialsDir && { partialsDir }
5489
+ ...partialsDir && { partialsDir },
5490
+ autoShell: true,
5491
+ ssr: stxConfig.ssr ?? defaultStxConfig.ssr ?? true,
5492
+ app: stxConfig.app || {},
5493
+ ..."strict" in stxConfig && { strict: stxConfig.strict },
5494
+ ..."router" in stxConfig && { router: stxConfig.router }
5395
5495
  };
5396
5496
  let output = templateContent;
5397
5497
  const dependencies = new Set;
5398
5498
  output = await processDirectives(output, context, filePath, config3, dependencies);
5399
- output = injectRouter(output);
5400
- output = output.replace(/<template[^>]*>/gi, "").replace(/<\/template>/gi, "");
5401
- const crosswindCSS = await generateCrosswindCSS(output);
5402
- if (crosswindCSS) {
5403
- const styleTag = `<style>/* crosswind css */
5404
- ${crosswindCSS}</style>`;
5499
+ if (paramNames.length > 0) {
5500
+ const paramsObj = {};
5501
+ for (let i = 0;i < paramNames.length; i++) {
5502
+ paramsObj[paramNames[i]] = paramValues[i];
5503
+ }
5504
+ const paramsScript = `<script>if(window.stx)window.stx._rp=${JSON.stringify(paramsObj)};else window.__stx_rp=${JSON.stringify(paramsObj)};</script>`;
5405
5505
  if (output.includes("</head>")) {
5406
- output = output.replace("</head>", `${styleTag}
5506
+ output = output.replace("</head>", `${paramsScript}
5407
5507
  </head>`);
5408
- } else if (output.includes("<body")) {
5409
- output = output.replace(/<body/i, `${styleTag}
5410
- <body`);
5411
5508
  } else {
5412
- output = styleTag + output;
5509
+ output = `${paramsScript}
5510
+ ${output}`;
5413
5511
  }
5414
5512
  }
5513
+ output = await injectRouter(output);
5514
+ const directiveTemplateRe2 = /@for|:for|@if|:if|x-for|x-if/;
5515
+ output = output.replace(/<template(?:\s[^>]*)?>|<\/template>/gi, (match) => {
5516
+ if (directiveTemplateRe2.test(match))
5517
+ return match;
5518
+ if (match === "</template>")
5519
+ return match;
5520
+ return "";
5521
+ });
5415
5522
  return output;
5416
5523
  }
5417
- if (!options.quiet)
5418
- console.log(` \u2713 Frontend running at http://localhost:${port}`);
5524
+ const startTime = performance.now();
5525
+ await discoverFiles();
5419
5526
  const corsHeaders = {
5420
5527
  "Access-Control-Allow-Origin": "*",
5421
5528
  "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
@@ -5440,6 +5547,31 @@ ${crosswindCSS}</style>`;
5440
5547
  return routeHandler(req);
5441
5548
  }
5442
5549
  }
5550
+ if (path.startsWith("/_stx/component/")) {
5551
+ const componentName = decodeURIComponent(path.slice("/_stx/component/".length));
5552
+ if (componentName) {
5553
+ try {
5554
+ const { processDirectives, defaultConfig: defaultConfig3 } = await import("@stacksjs/stx");
5555
+ const componentTemplate = `<${componentName} />`;
5556
+ const componentOpts = {
5557
+ ...defaultConfig3,
5558
+ ...componentsDir && { componentsDir },
5559
+ ...layoutsDir && { layoutsDir },
5560
+ ...partialsDir && { partialsDir },
5561
+ autoShell: false
5562
+ };
5563
+ const html = await processDirectives(componentTemplate, {}, `${componentsDir}/${componentName}.stx`, componentOpts, new Set);
5564
+ return new Response(html, {
5565
+ headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
5566
+ });
5567
+ } catch (e) {
5568
+ return new Response(`<div class="stx-async-error">${e.message}</div>`, {
5569
+ status: 500,
5570
+ headers: { "Content-Type": "text/html; charset=utf-8", ...corsHeaders }
5571
+ });
5572
+ }
5573
+ }
5574
+ }
5443
5575
  if (path.startsWith("/api/data/") && req.method === "POST") {
5444
5576
  try {
5445
5577
  const tableName = path.replace("/api/data/", "").split("/")[0];
@@ -5449,62 +5581,70 @@ ${crosswindCSS}</style>`;
5449
5581
  headers: { "Content-Type": "application/json", ...corsHeaders }
5450
5582
  });
5451
5583
  }
5584
+ if (!/^[a-zA-Z_]\w*$/.test(tableName)) {
5585
+ return new Response(JSON.stringify({ error: "Invalid table name" }), {
5586
+ status: 400,
5587
+ headers: { "Content-Type": "application/json", ...corsHeaders }
5588
+ });
5589
+ }
5452
5590
  const body = await req.json();
5453
5591
  const nodePath = await import("path");
5454
5592
  const { Database } = await import("bun:sqlite");
5455
5593
  const dbPath = nodePath.resolve(process13.cwd(), "database/stacks.sqlite");
5456
5594
  const db = new Database(dbPath);
5457
- const tableInfo = db.query(`PRAGMA table_info(${tableName})`).all();
5458
- const validColumns = tableInfo.map((c) => c.name).filter((n) => n !== "id" && n !== "created_at" && n !== "updated_at");
5459
- const columns = [];
5460
- const placeholders = [];
5461
- const values = [];
5462
- for (const col of validColumns) {
5463
- if (body[col] !== undefined && body[col] !== "") {
5464
- columns.push(col);
5595
+ try {
5596
+ const tableInfo = db.query(`PRAGMA table_info("${tableName}")`).all();
5597
+ const validColumns = tableInfo.map((c) => c.name).filter((n) => n !== "id" && n !== "created_at" && n !== "updated_at");
5598
+ const columns = [];
5599
+ const placeholders = [];
5600
+ const values = [];
5601
+ for (const col of validColumns) {
5602
+ if (body[col] !== undefined && body[col] !== "") {
5603
+ columns.push(col);
5604
+ placeholders.push("?");
5605
+ values.push(body[col]);
5606
+ }
5607
+ }
5608
+ const now = new Date().toISOString();
5609
+ if (tableInfo.some((c) => c.name === "created_at")) {
5610
+ columns.push("created_at");
5465
5611
  placeholders.push("?");
5466
- values.push(body[col]);
5612
+ values.push(now);
5467
5613
  }
5468
- }
5469
- const now = new Date().toISOString();
5470
- if (tableInfo.some((c) => c.name === "created_at")) {
5471
- columns.push("created_at");
5472
- placeholders.push("?");
5473
- values.push(now);
5474
- }
5475
- if (tableInfo.some((c) => c.name === "updated_at")) {
5476
- columns.push("updated_at");
5477
- placeholders.push("?");
5478
- values.push(now);
5479
- }
5480
- if (columns.length === 0) {
5481
- db.close();
5482
- return new Response(JSON.stringify({ error: "No valid fields provided" }), {
5483
- status: 400,
5614
+ if (tableInfo.some((c) => c.name === "updated_at")) {
5615
+ columns.push("updated_at");
5616
+ placeholders.push("?");
5617
+ values.push(now);
5618
+ }
5619
+ if (columns.length === 0) {
5620
+ return new Response(JSON.stringify({ error: "No valid fields provided" }), {
5621
+ status: 400,
5622
+ headers: { "Content-Type": "application/json", ...corsHeaders }
5623
+ });
5624
+ }
5625
+ const query = `INSERT INTO "${tableName}" (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
5626
+ const stmt = db.prepare(query);
5627
+ const result = stmt.run(...values);
5628
+ return new Response(JSON.stringify({
5629
+ success: true,
5630
+ id: result.lastInsertRowid,
5631
+ message: "Record created successfully"
5632
+ }), {
5633
+ status: 201,
5484
5634
  headers: { "Content-Type": "application/json", ...corsHeaders }
5485
5635
  });
5636
+ } catch (error) {
5637
+ console.error("API Error:", error);
5638
+ return new Response(JSON.stringify({
5639
+ error: error.message || "Failed to create record"
5640
+ }), {
5641
+ status: 500,
5642
+ headers: { "Content-Type": "application/json", ...corsHeaders }
5643
+ });
5644
+ } finally {
5645
+ db.close();
5486
5646
  }
5487
- const query = `INSERT INTO ${tableName} (${columns.join(", ")}) VALUES (${placeholders.join(", ")})`;
5488
- const stmt = db.prepare(query);
5489
- const result = stmt.run(...values);
5490
- db.close();
5491
- return new Response(JSON.stringify({
5492
- success: true,
5493
- id: result.lastInsertRowid,
5494
- message: "Record created successfully"
5495
- }), {
5496
- status: 201,
5497
- headers: { "Content-Type": "application/json", ...corsHeaders }
5498
- });
5499
- } catch (error) {
5500
- console.error("API Error:", error);
5501
- return new Response(JSON.stringify({
5502
- error: error.message || "Failed to create record"
5503
- }), {
5504
- status: 500,
5505
- headers: { "Content-Type": "application/json", ...corsHeaders }
5506
- });
5507
- }
5647
+ } catch {}
5508
5648
  }
5509
5649
  if (path === "/index")
5510
5650
  path = "/";
@@ -5525,9 +5665,92 @@ ${crosswindCSS}</style>`;
5525
5665
  }
5526
5666
  const content = await getRoute(path);
5527
5667
  if (content) {
5528
- return new Response(content, {
5668
+ const isSpaNav = req.headers.get("X-STX-Router") === "true";
5669
+ if (isSpaNav) {
5670
+ const layoutMatch = content.match(/<!-- stx-layout: ([^ ]+) -->/);
5671
+ const pageLayout = layoutMatch ? layoutMatch[1] : "default";
5672
+ let fragment = content;
5673
+ const headStyles = [];
5674
+ const headMatch = content.match(/<head\b[^>]*>([\s\S]*?)<\/head>/i);
5675
+ if (headMatch) {
5676
+ const headContent = headMatch[1];
5677
+ let styleMatch;
5678
+ const styleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
5679
+ while ((styleMatch = styleRe.exec(headContent)) !== null) {
5680
+ if (styleMatch[0].includes("data-crosswind")) {
5681
+ const cssContent = styleMatch[0].replace(/<style[^>]*>/, "").replace(/<\/style>/, "");
5682
+ const hiddenRule = cssContent.indexOf("[hidden]");
5683
+ if (hiddenRule !== -1) {
5684
+ const afterPreflight = cssContent.indexOf("}", hiddenRule) + 1;
5685
+ const utilities = cssContent.slice(afterPreflight).trim();
5686
+ if (utilities) {
5687
+ headStyles.push(`<style data-crosswind="fragment">${utilities}</style>`);
5688
+ }
5689
+ }
5690
+ continue;
5691
+ }
5692
+ headStyles.push(styleMatch[0]);
5693
+ }
5694
+ }
5695
+ const mainOpenMatch = fragment.match(/<main\b[^>]*>/i);
5696
+ const mainCloseIdx = fragment.lastIndexOf("</main>");
5697
+ if (mainOpenMatch && mainCloseIdx !== -1) {
5698
+ const bodyMatch = content.match(/<body\b[^>]*>/i);
5699
+ if (bodyMatch) {
5700
+ const bodyStart = bodyMatch.index + bodyMatch[0].length;
5701
+ const mainIdx = mainOpenMatch.index;
5702
+ const beforeMain = content.slice(bodyStart, mainIdx);
5703
+ let bodyStyleMatch;
5704
+ const bodyStyleRe = /<style\b[^>]*>[\s\S]*?<\/style>/gi;
5705
+ while ((bodyStyleMatch = bodyStyleRe.exec(beforeMain)) !== null) {
5706
+ headStyles.push(bodyStyleMatch[0]);
5707
+ }
5708
+ }
5709
+ const mainStart = mainOpenMatch.index + mainOpenMatch[0].length;
5710
+ fragment = fragment.slice(mainStart, mainCloseIdx).trim();
5711
+ }
5712
+ const pageSetupScripts = [];
5713
+ const allScriptRe = /<script\b[^>]*data-stx-scoped[^>]*>[\s\S]*?<\/script>/gi;
5714
+ let setupMatch;
5715
+ while ((setupMatch = allScriptRe.exec(content)) !== null) {
5716
+ const scriptContent = setupMatch[0];
5717
+ if (scriptContent.includes("__stx_early_mounts"))
5718
+ continue;
5719
+ if (scriptContent.includes("data-stx-reactive") && scriptContent.includes("window.__stx_reactive"))
5720
+ continue;
5721
+ pageSetupScripts.push(scriptContent);
5722
+ }
5723
+ const bridgeInitRe = /<script\b[^>]*data-stx-reactive[^>]*>(?![\s\S]*window\.__stx_reactive)[\s\S]*?<\/script>/gi;
5724
+ while ((setupMatch = bridgeInitRe.exec(content)) !== null) {
5725
+ pageSetupScripts.push(setupMatch[0]);
5726
+ }
5727
+ fragment = fragment.replace(/<script data-stx-scoped>\s*;?\(function\(\)\s*\{[\s\S]*?<\/script>/g, "");
5728
+ const clearStale = "<script data-stx-page>if(window.stx)window.stx._latestSetup=null;</script>";
5729
+ fragment = `${headStyles.join(`
5730
+ `)}
5731
+ ${fragment}
5732
+ ${clearStale}
5733
+ ${pageSetupScripts.join(`
5734
+ `)}`;
5735
+ return new Response(fragment, {
5736
+ headers: {
5737
+ "Content-Type": "text/html; charset=utf-8",
5738
+ "X-STX-Fragment": "true",
5739
+ "X-STX-Layout": pageLayout,
5740
+ "Cache-Control": "no-cache",
5741
+ ...corsHeaders
5742
+ }
5743
+ });
5744
+ }
5745
+ let cleaned = content;
5746
+ let runtimeCount = 0;
5747
+ cleaned = content.replace(/<script data-stx-scoped>\(function\(\)\{'use strict';var cloakStyle[\s\S]*?<\/script>/g, (match) => {
5748
+ runtimeCount++;
5749
+ return runtimeCount === 1 ? match : "";
5750
+ });
5751
+ return new Response(cleaned, {
5529
5752
  headers: {
5530
- "Content-Type": "text/html",
5753
+ "Content-Type": "text/html; charset=utf-8",
5531
5754
  "Cache-Control": "no-cache",
5532
5755
  ...corsHeaders
5533
5756
  }
@@ -5537,7 +5760,7 @@ ${crosswindCSS}</style>`;
5537
5760
  if (contentWithExt) {
5538
5761
  return new Response(contentWithExt, {
5539
5762
  headers: {
5540
- "Content-Type": "text/html",
5763
+ "Content-Type": "text/html; charset=utf-8",
5541
5764
  "Cache-Control": "no-cache",
5542
5765
  ...corsHeaders
5543
5766
  }
@@ -5584,24 +5807,9 @@ ${crosswindCSS}</style>`;
5584
5807
  }
5585
5808
  });
5586
5809
  }
5587
- const contentTypes = {
5588
- css: "text/css",
5589
- js: "application/javascript",
5590
- json: "application/json",
5591
- png: "image/png",
5592
- jpg: "image/jpeg",
5593
- jpeg: "image/jpeg",
5594
- gif: "image/gif",
5595
- svg: "image/svg+xml",
5596
- ico: "image/x-icon",
5597
- woff: "font/woff",
5598
- woff2: "font/woff2",
5599
- ttf: "font/ttf",
5600
- eot: "application/vnd.ms-fontobject"
5601
- };
5602
5810
  return new Response(file, {
5603
5811
  headers: {
5604
- "Content-Type": contentTypes[ext || ""] || "application/octet-stream",
5812
+ "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
5605
5813
  "Cache-Control": "public, max-age=31536000"
5606
5814
  }
5607
5815
  });
@@ -5611,27 +5819,38 @@ ${crosswindCSS}</style>`;
5611
5819
  }
5612
5820
  }
5613
5821
  }
5614
- if (path === "/favicon.ico") {
5615
- const faviconPaths = [
5616
- "./public/favicon.ico",
5617
- "./resources/assets/favicon.ico",
5618
- "./favicon.ico"
5619
- ];
5620
- for (const faviconPath of faviconPaths) {
5621
- try {
5622
- const favicon = Bun.file(faviconPath);
5623
- if (await favicon.exists()) {
5624
- return new Response(favicon, {
5625
- headers: {
5626
- "Content-Type": "image/x-icon",
5627
- "Cache-Control": "public, max-age=86400"
5822
+ if ((req.method === "GET" || req.method === "HEAD") && path !== "/") {
5823
+ const nodePathMod = await import("path");
5824
+ const publicRoot = nodePathMod.resolve(process13.cwd(), publicDir);
5825
+ let safePathname;
5826
+ try {
5827
+ safePathname = decodeURIComponent(path);
5828
+ } catch {
5829
+ safePathname = path;
5830
+ }
5831
+ if (!safePathname.includes("\x00")) {
5832
+ const resolvedPath = nodePathMod.resolve(publicRoot, `.${safePathname}`);
5833
+ const isInsidePublicRoot = resolvedPath === publicRoot || resolvedPath.startsWith(`${publicRoot}${nodePathMod.sep}`);
5834
+ if (isInsidePublicRoot) {
5835
+ try {
5836
+ const file = Bun.file(resolvedPath);
5837
+ if (await file.exists()) {
5838
+ const stat3 = await file.stat().catch(() => null);
5839
+ if (stat3 && !stat3.isDirectory()) {
5840
+ const ext = resolvedPath.split(".").pop()?.toLowerCase();
5841
+ return new Response(file, {
5842
+ headers: {
5843
+ "Content-Type": staticContentTypes[ext || ""] || "application/octet-stream",
5844
+ "Cache-Control": "public, max-age=3600"
5845
+ }
5846
+ });
5628
5847
  }
5629
- });
5630
- }
5631
- } catch {
5632
- continue;
5848
+ }
5849
+ } catch {}
5633
5850
  }
5634
5851
  }
5852
+ }
5853
+ if (path === "/favicon.ico") {
5635
5854
  return new Response(null, { status: 204 });
5636
5855
  }
5637
5856
  const files = await discoverFiles();
@@ -5682,10 +5901,40 @@ ${crosswindCSS}</style>`;
5682
5901
  </html>
5683
5902
  `, {
5684
5903
  status: 404,
5685
- headers: { "Content-Type": "text/html" }
5904
+ headers: { "Content-Type": "text/html; charset=utf-8" }
5686
5905
  });
5687
5906
  }
5688
5907
  });
5908
+ if (!options.quiet) {
5909
+ const elapsed = (performance.now() - startTime).toFixed(0);
5910
+ const routeCount = sourceFiles?.length || 0;
5911
+ const patternsStr = patterns.join(", ");
5912
+ console.log();
5913
+ console.log(` \x1B[36m\x1B[1mstx\x1B[0m`);
5914
+ console.log();
5915
+ console.log(` \x1B[32m\u279C\x1B[0m \x1B[1mLocal\x1B[0m: \x1B[36mhttp://localhost:${port}/\x1B[0m`);
5916
+ console.log(` \x1B[32m\u279C\x1B[0m \x1B[1mRoutes\x1B[0m: \x1B[2m${routeCount} files from ${patternsStr}\x1B[0m`);
5917
+ console.log();
5918
+ console.log(` \x1B[2mready in ${elapsed}ms\x1B[0m`);
5919
+ console.log();
5920
+ console.log(` \x1B[2mPress\x1B[0m o + Enter \x1B[2mto open in browser\x1B[0m`);
5921
+ console.log(` \x1B[2mPress\x1B[0m q + Enter \x1B[2mto quit\x1B[0m`);
5922
+ console.log();
5923
+ if (process13.stdin.isTTY) {
5924
+ process13.stdin.setRawMode(false);
5925
+ const rl = await import("readline");
5926
+ const reader = rl.createInterface({ input: process13.stdin });
5927
+ reader.on("line", (line) => {
5928
+ const cmd = line.trim().toLowerCase();
5929
+ if (cmd === "o") {
5930
+ const openCmd = process13.platform === "darwin" ? "open" : process13.platform === "win32" ? "start" : "xdg-open";
5931
+ Bun.spawn([openCmd, `http://localhost:${port}/`]);
5932
+ } else if (cmd === "q") {
5933
+ process13.exit(0);
5934
+ }
5935
+ });
5936
+ }
5937
+ }
5689
5938
  await Bun.sleep(Number.POSITIVE_INFINITY);
5690
5939
  }
5691
5940
  async function main() {