xpine 0.0.58 → 0.0.60

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.
Files changed (53) hide show
  1. package/.claude/settings.local.json +2 -1
  2. package/.claude/worktrees/happy-lalande-c6c39c/.claude/settings.local.json +33 -0
  3. package/.claude/worktrees/happy-lalande-c6c39c/.d.ts +1 -0
  4. package/.claude/worktrees/happy-lalande-c6c39c/.gitattributes +2 -0
  5. package/.claude/worktrees/happy-lalande-c6c39c/README.md +571 -0
  6. package/.claude/worktrees/happy-lalande-c6c39c/TODO +2 -0
  7. package/.claude/worktrees/happy-lalande-c6c39c/eslint.config.mjs +27 -0
  8. package/.claude/worktrees/happy-lalande-c6c39c/jsx-runtime.d.ts +1 -0
  9. package/.claude/worktrees/happy-lalande-c6c39c/package-lock.json +5862 -0
  10. package/.claude/worktrees/happy-lalande-c6c39c/package.json +62 -0
  11. package/.claude/worktrees/happy-lalande-c6c39c/tsconfig.json +43 -0
  12. package/.claude/worktrees/happy-lalande-c6c39c/types.ts +55 -0
  13. package/.claude/worktrees/happy-lalande-c6c39c/xpine.config.mjs +1 -0
  14. package/.claude/worktrees/jolly-hofstadter-2764be/.claude/settings.local.json +33 -0
  15. package/.claude/worktrees/jolly-hofstadter-2764be/.d.ts +1 -0
  16. package/.claude/worktrees/jolly-hofstadter-2764be/.gitattributes +2 -0
  17. package/.claude/worktrees/jolly-hofstadter-2764be/README.md +571 -0
  18. package/.claude/worktrees/jolly-hofstadter-2764be/TODO +2 -0
  19. package/.claude/worktrees/jolly-hofstadter-2764be/eslint.config.mjs +27 -0
  20. package/.claude/worktrees/jolly-hofstadter-2764be/jsx-runtime.d.ts +1 -0
  21. package/.claude/worktrees/jolly-hofstadter-2764be/package-lock.json +5862 -0
  22. package/.claude/worktrees/jolly-hofstadter-2764be/package.json +62 -0
  23. package/.claude/worktrees/jolly-hofstadter-2764be/tsconfig.json +43 -0
  24. package/.claude/worktrees/jolly-hofstadter-2764be/types.ts +55 -0
  25. package/.claude/worktrees/jolly-hofstadter-2764be/xpine.config.mjs +1 -0
  26. package/README.md +104 -0
  27. package/create-xpine-app/README.md +20 -0
  28. package/create-xpine-app/index.js +80 -0
  29. package/create-xpine-app/package.json +30 -0
  30. package/create-xpine-app/template/README.md +50 -0
  31. package/create-xpine-app/template/eslint.config.mjs +27 -0
  32. package/create-xpine-app/template/gitignore +5 -0
  33. package/create-xpine-app/template/package.json +32 -0
  34. package/create-xpine-app/template/tsconfig.json +36 -0
  35. package/create-xpine-app/template/xpine.config.mjs +1 -0
  36. package/dist/index.d.ts +1 -0
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +269 -106
  39. package/dist/src/auth.d.ts.map +1 -1
  40. package/dist/src/express.d.ts +5 -0
  41. package/dist/src/express.d.ts.map +1 -1
  42. package/dist/src/scripts/build.d.ts.map +1 -1
  43. package/dist/src/scripts/xpine-build.js +6 -2
  44. package/dist/src/scripts/xpine-dev.js +15 -21
  45. package/dist/src/util/html.d.ts +9 -1
  46. package/dist/src/util/html.d.ts.map +1 -1
  47. package/dist/src/util/regex.d.ts +2 -0
  48. package/dist/src/util/regex.d.ts.map +1 -1
  49. package/dist/types.d.ts +4 -1
  50. package/dist/types.d.ts.map +1 -1
  51. package/eslint.config.mjs +1 -1
  52. package/package.json +1 -1
  53. package/types.ts +5 -1
package/dist/index.js CHANGED
@@ -326,6 +326,9 @@ var regex_default = {
326
326
  configFile: /\+config\.[tj]sx?/g,
327
327
  dynamicRoutes: /(\[)(.*?)(\])/g,
328
328
  isDynamicRoute: /\[(.*)\]/g,
329
+ // Multi-segment (slash-containing) dynamic route, e.g. [...slug]
330
+ spreadRoute: /\[\.\.\.(.*?)\]/g,
331
+ isSpreadRoute: /\[\.\.\..*?\]/,
329
332
  endsWithTSX: /\.tsx$/,
330
333
  endsWithJSX: /\.jsx$/,
331
334
  endsWithJs: /\.js$/,
@@ -467,6 +470,12 @@ import { globSync } from "glob";
467
470
  // src/auth.ts
468
471
  import jsonwebtoken from "jsonwebtoken";
469
472
  var { verify, sign } = jsonwebtoken;
473
+ var JWT_ALGORITHM = "HS256";
474
+ function getSecret() {
475
+ const secret = process.env.JWT_PRIVATE_KEY;
476
+ if (!secret) throw new Error("JWT_PRIVATE_KEY is not set");
477
+ return secret;
478
+ }
470
479
  async function signUser(email, username) {
471
480
  return new Promise((resolve, reject) => {
472
481
  sign(
@@ -476,11 +485,10 @@ async function signUser(email, username) {
476
485
  username
477
486
  }
478
487
  },
479
- // @ts-ignore
480
- process.env.JWT_PRIVATE_KEY,
481
- { expiresIn: "30d" },
488
+ getSecret(),
489
+ { expiresIn: "30d", algorithm: JWT_ALGORITHM },
482
490
  (err, token) => {
483
- if (err) reject(err);
491
+ if (err) return reject(err);
484
492
  resolve(token);
485
493
  }
486
494
  );
@@ -488,8 +496,8 @@ async function signUser(email, username) {
488
496
  }
489
497
  async function verifyUser(token) {
490
498
  return new Promise((resolve, reject) => {
491
- verify(token, process.env.JWT_PRIVATE_KEY, (err, authorizedData) => {
492
- if (err) reject(err);
499
+ verify(token, getSecret(), { algorithms: [JWT_ALGORITHM] }, (err, authorizedData) => {
500
+ if (err) return reject(err);
493
501
  resolve(authorizedData);
494
502
  });
495
503
  });
@@ -506,8 +514,163 @@ import requestIP from "request-ip";
506
514
  import fs5 from "fs-extra";
507
515
  import path5 from "path";
508
516
  import EventEmitter from "events";
517
+
518
+ // src/csrf.ts
519
+ import crypto from "crypto";
520
+ var SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
521
+ function resolveSecret(options) {
522
+ const secret = options?.secret || process.env.CSRF_SECRET || process.env.JWT_PRIVATE_KEY;
523
+ if (!secret) throw new Error("CSRF secret is not set (set CSRF_SECRET or JWT_PRIVATE_KEY)");
524
+ return secret;
525
+ }
526
+ function hmac(value, secret) {
527
+ return crypto.createHmac("sha256", secret).update(value).digest("base64url");
528
+ }
529
+ function issueToken(secret) {
530
+ const random = crypto.randomBytes(32).toString("base64url");
531
+ return `${random}.${hmac(random, secret)}`;
532
+ }
533
+ function isValidToken(token, secret) {
534
+ if (typeof token !== "string" || !token) return false;
535
+ const idx = token.lastIndexOf(".");
536
+ if (idx <= 0 || idx === token.length - 1) return false;
537
+ const random = token.slice(0, idx);
538
+ const signature = token.slice(idx + 1);
539
+ return timingSafeEqual(signature, hmac(random, secret));
540
+ }
541
+ function timingSafeEqual(a, b) {
542
+ const ab = Buffer.from(String(a));
543
+ const bb = Buffer.from(String(b));
544
+ if (ab.length !== bb.length) return false;
545
+ return crypto.timingSafeEqual(ab, bb);
546
+ }
547
+ function readSubmittedToken(req, headerName, fieldName) {
548
+ const header = req.headers[headerName];
549
+ const fromHeader = Array.isArray(header) ? header[0] : header;
550
+ if (fromHeader) return fromHeader;
551
+ if (req.body && typeof req.body[fieldName] === "string") return req.body[fieldName];
552
+ return "";
553
+ }
554
+ function createCsrfMiddleware(options) {
555
+ const secret = resolveSecret(options);
556
+ const cookieName = options?.cookieName || "csrfToken";
557
+ const headerName = (options?.headerName || "x-csrf-token").toLowerCase();
558
+ const fieldName = options?.fieldName || "_csrf";
559
+ const ignorePaths = options?.ignorePaths || [];
560
+ const cookieOptions = {
561
+ httpOnly: false,
562
+ // must be readable by client JS for the double-submit
563
+ sameSite: options?.cookie?.sameSite ?? "lax",
564
+ secure: options?.cookie?.secure ?? process.env.NODE_ENV !== "development",
565
+ path: options?.cookie?.path ?? "/",
566
+ maxAge: options?.cookie?.maxAge
567
+ };
568
+ return function csrfMiddleware(req, res, next) {
569
+ const existing = req.cookies?.[cookieName] || "";
570
+ let token = existing;
571
+ if (!isValidToken(existing, secret)) {
572
+ token = issueToken(secret);
573
+ res.cookie(cookieName, token, cookieOptions);
574
+ }
575
+ res.locals.csrfToken = token;
576
+ if (SAFE_METHODS.has(req.method)) return next();
577
+ if (ignorePaths.some((prefix) => req.path.startsWith(prefix))) return next();
578
+ const submitted = readSubmittedToken(req, headerName, fieldName);
579
+ if (isValidToken(existing, secret) && timingSafeEqual(submitted, existing)) {
580
+ return next();
581
+ }
582
+ res.status(403).json({ message: "Invalid CSRF token" });
583
+ };
584
+ }
585
+
586
+ // src/express.ts
509
587
  var methods = ["get", "post", "put", "patch", "delete"];
510
588
  var isDev = process.env.NODE_ENV === "development";
589
+ function parseRoutePath(route) {
590
+ const slugRoute = route.replace(/[ ]/g, "");
591
+ const foundMethod = methods.find((method) => slugRoute.toUpperCase().endsWith(`.${method.toUpperCase()}`));
592
+ const templateRoute = foundMethod ? slugRoute.slice(0, -(foundMethod.length + 1)) : slugRoute;
593
+ let formattedRouteItem = templateRoute.replace(regex_default.spreadRoute, (_match, name) => ":" + name);
594
+ if (slugRoute.match(regex_default.isDynamicRoute)) {
595
+ for (const match of formattedRouteItem.matchAll(regex_default.dynamicRoutes)) {
596
+ formattedRouteItem = formattedRouteItem.replace(match[0], ":" + match[2]);
597
+ }
598
+ }
599
+ if (formattedRouteItem.match(regex_default.catchAllRoute)) {
600
+ formattedRouteItem = formattedRouteItem.replace(regex_default.catchAllRoute, "/*");
601
+ }
602
+ return {
603
+ foundMethod,
604
+ formattedRouteItem,
605
+ templateRoute,
606
+ isSpread: regex_default.isSpreadRoute.test(templateRoute)
607
+ };
608
+ }
609
+ function spreadConcretePath(templateRoute, entry) {
610
+ return templateRoute.replace(regex_default.spreadRoute, (_match, name) => entry[name] ?? "").replace(regex_default.dynamicRoutes, (_match, _open, name) => entry[name] ?? "").replace(/\/{2,}/g, "/");
611
+ }
612
+ function spreadWildcardPath(templateRoute) {
613
+ return templateRoute.replace(regex_default.spreadRoute, "*").replace(regex_default.dynamicRoutes, (_match, _open, name) => ":" + name);
614
+ }
615
+ function spreadSlugName(templateRoute) {
616
+ const match = templateRoute.match(/\[\.\.\.(.*?)\]/);
617
+ return match ? match[1] : null;
618
+ }
619
+ async function resolveConfig(configFilePaths, componentImport) {
620
+ const baseConfig = configFilePaths ? await getCompleteConfig(configFilePaths, Date.now()) : {};
621
+ return componentImport?.config ? { ...baseConfig, ...componentImport.config } : baseConfig;
622
+ }
623
+ async function renderJSX(componentFn, config2, req, res, urlPath) {
624
+ const data = config2?.data ? await config2.data(req) : null;
625
+ const children = await componentFn({ req, res, data, config: config2, routePath: urlPath });
626
+ const output = config2?.wrapper ? await config2.wrapper({ req, children, config: config2, data, routePath: urlPath }) : children;
627
+ return doctypeHTML + output;
628
+ }
629
+ async function resolveSpreadRoute(templateRoute, config2, originalRoute) {
630
+ let entries = [];
631
+ if (typeof config2?.staticPaths === "function") {
632
+ try {
633
+ entries = await config2.staticPaths() || [];
634
+ } catch (err) {
635
+ console.error(err);
636
+ console.error("Could not resolve staticPaths for spread route", originalRoute);
637
+ }
638
+ }
639
+ return {
640
+ templateRoute,
641
+ entries,
642
+ validator: config2?.isValid || null
643
+ };
644
+ }
645
+ function registerSpreadRoutes(register, spread) {
646
+ const slugName = spreadSlugName(spread.templateRoute);
647
+ for (const entry of spread.entries) {
648
+ register(spreadConcretePath(spread.templateRoute, entry), [
649
+ (req, _res, next) => {
650
+ Object.assign(req.params, entry);
651
+ next();
652
+ }
653
+ ]);
654
+ }
655
+ const { validator } = spread;
656
+ if (validator) {
657
+ register(spreadWildcardPath(spread.templateRoute), [
658
+ async (req, _res, next) => {
659
+ const slug = req.params[0];
660
+ if (slugName) req.params[slugName] = slug;
661
+ try {
662
+ if (await validator(slug, req)) {
663
+ next();
664
+ return;
665
+ }
666
+ } catch (err) {
667
+ console.error(err);
668
+ }
669
+ next("route");
670
+ }
671
+ ]);
672
+ }
673
+ }
511
674
  var OnInitEmitter = class extends EventEmitter {
512
675
  };
513
676
  var onInitEmitter = new OnInitEmitter();
@@ -532,33 +695,12 @@ function getAllBuiltRoutes() {
532
695
  }
533
696
  async function createRouteFunction(route, configFiles) {
534
697
  const isJSX = route.originalRoute.endsWith(".tsx") || route.originalRoute.endsWith(".jsx");
535
- const slugRoute = route.route.replace(/[ ]/g, "");
536
- const foundMethod = methods.find((method) => slugRoute.toUpperCase().endsWith(`.${method.toUpperCase()}`));
537
- const isDynamicRoute = slugRoute.match(regex_default.isDynamicRoute);
538
- let formattedRouteItem = slugRoute;
539
- if (foundMethod) formattedRouteItem = formattedRouteItem.split(".").shift();
540
- if (isDynamicRoute) {
541
- const result = [...formattedRouteItem.matchAll(regex_default.dynamicRoutes)];
542
- for (const match of result) {
543
- formattedRouteItem = formattedRouteItem.replace(match[0], ":" + match[2]);
544
- }
545
- }
546
- const hasCatchAll = formattedRouteItem.match(regex_default.catchAllRoute);
547
- if (hasCatchAll) formattedRouteItem = formattedRouteItem.replace(regex_default.catchAllRoute, "/*");
698
+ const { foundMethod, formattedRouteItem, templateRoute, isSpread } = parseRoutePath(route.route);
548
699
  const componentImport = await import(route.path);
549
700
  const componentFn = isDev ? null : componentImport?.default;
550
- if (componentImport?.config?.onInit) {
551
- await componentImport.config?.onInit();
552
- }
553
- let config2 = {};
701
+ if (componentImport?.config?.onInit) await componentImport.config.onInit();
554
702
  const configFilePaths = getConfigFiles(route.originalRoute, configFiles);
555
- config2 = configFilePaths && await getCompleteConfig(configFilePaths, Date.now());
556
- if (componentImport?.config) {
557
- config2 = {
558
- ...config2,
559
- ...componentImport.config
560
- };
561
- }
703
+ const config2 = await resolveConfig(configFilePaths, componentImport);
562
704
  async function routeFn(req, res) {
563
705
  const urlPath = req?.route?.path || "/";
564
706
  try {
@@ -568,47 +710,37 @@ async function createRouteFunction(route, configFiles) {
568
710
  return;
569
711
  }
570
712
  if (componentFn && !isDev) {
571
- if (isJSX) {
572
- const data = config2?.data ? await config2.data(req) : null;
573
- const originalResult = await componentFn({ req, res, data, routePath: urlPath });
574
- const output = config2?.wrapper ? await config2.wrapper({ req, children: originalResult, config: config2, data, routePath: urlPath }) : originalResult;
575
- res.send(doctypeHTML + output);
576
- } else {
577
- await componentFn(req, res);
578
- }
713
+ if (isJSX) res.send(await renderJSX(componentFn, config2, req, res, urlPath));
714
+ else await componentFn(req, res);
579
715
  return;
580
716
  }
581
717
  await triggerXPineOnLoad(true);
582
- const componentImportDev = await import(route.path + `?cache=${Date.now()}`);
583
- const componentFnDev = componentImportDev.default;
718
+ const devImport = await import(route.path + `?cache=${Date.now()}`);
584
719
  onInitEmitter.emit("triggerOnInit", getAllBuiltRoutes());
585
720
  if (isJSX) {
586
- let config3 = configFilePaths && await getCompleteConfig(configFilePaths, Date.now());
587
- if (componentImportDev?.config) {
588
- config3 = {
589
- ...config3,
590
- ...componentImportDev.config
591
- };
592
- }
593
- const data = config3?.data ? await config3.data(req) : null;
594
- const originalResult = await componentFnDev({ req, res, data, config: config3, routePath: urlPath });
595
- const output = config3?.wrapper ? await config3.wrapper({ req, children: originalResult, config: config3, data, routePath: urlPath }) : originalResult;
721
+ const devConfig = await resolveConfig(configFilePaths, devImport);
722
+ const html2 = await renderJSX(devImport.default, devConfig, req, res, urlPath);
596
723
  context.clear();
597
- res.send(doctypeHTML + output);
724
+ res.send(html2);
598
725
  } else {
599
- await componentFnDev(req, res);
726
+ await devImport.default(req, res);
600
727
  }
601
728
  } catch (err) {
602
729
  console.error(err);
603
- res.status(err?.status || 500).send(err?.message || "Error");
730
+ const status = err?.status || 500;
731
+ const message = isDev || err?.status ? err?.message || "Error" : "Internal Server Error";
732
+ res.status(status).send(message);
604
733
  }
605
734
  }
606
- return {
607
- formattedRouteItem,
608
- foundMethod,
609
- route,
610
- config: config2,
611
- routeFn
735
+ const spread = isSpread ? await resolveSpreadRoute(templateRoute, config2, route.originalRoute) : null;
736
+ return { formattedRouteItem, foundMethod, route, config: config2, routeFn, spread };
737
+ }
738
+ function buildRouteRegister(router, method, config2, routeFn) {
739
+ return (routePath, preHandlers = []) => {
740
+ const handlers = [...preHandlers];
741
+ if (config2?.routeMiddleware) handlers.push(config2.routeMiddleware);
742
+ handlers.push(routeFn);
743
+ router[method](routePath, ...handlers);
612
744
  };
613
745
  }
614
746
  async function createRouter() {
@@ -619,46 +751,50 @@ async function createRouter() {
619
751
  await triggerXPineOnLoad();
620
752
  for (const route of routeMap) {
621
753
  try {
622
- const { formattedRouteItem, foundMethod, config: config2, routeFn } = await createRouteFunction(route, configFiles);
623
- if (config2?.routeMiddleware) {
624
- router[foundMethod || "get"](formattedRouteItem, config2.routeMiddleware, routeFn);
625
- } else {
626
- router[foundMethod || "get"](formattedRouteItem, routeFn);
627
- }
628
- routeResults.push({
629
- formattedRouteItem,
630
- foundMethod,
631
- route,
632
- routeFn
633
- });
754
+ const { formattedRouteItem, foundMethod, config: routeConfig, routeFn, spread } = await createRouteFunction(route, configFiles);
755
+ const register = buildRouteRegister(router, foundMethod || "get", routeConfig, routeFn);
756
+ if (spread) registerSpreadRoutes(register, spread);
757
+ else register(formattedRouteItem);
758
+ routeResults.push({ formattedRouteItem, foundMethod, route, routeFn });
634
759
  } catch (err) {
635
760
  console.error(err);
636
761
  }
637
762
  }
638
- return {
639
- router,
640
- routeResults
641
- };
763
+ return { router, routeResults };
642
764
  }
643
765
  async function verifyUserMiddleware(req, _res, next) {
644
- const { usertoken } = req.cookies;
766
+ const usertoken = req.cookies?.usertoken;
645
767
  if (!usertoken) {
646
768
  req.user = null;
769
+ next();
770
+ return;
647
771
  }
648
772
  try {
649
773
  const { user } = await verifyUser(usertoken);
650
774
  req.user = user;
651
- } catch (err) {
775
+ } catch {
652
776
  req.user = null;
653
777
  }
654
778
  next();
655
779
  }
656
780
  async function verifyAuthenticatedBundleMiddleware(req, res, next) {
657
- if (!req?.originalUrl?.startsWith("/scripts/")) {
781
+ let decodedPath;
782
+ try {
783
+ decodedPath = decodeURIComponent(req?.path || "");
784
+ } catch {
785
+ next();
786
+ return;
787
+ }
788
+ if (!decodedPath.startsWith("/scripts/")) {
789
+ next();
790
+ return;
791
+ }
792
+ const fileName = decodedPath.split("/").pop() || "";
793
+ if (!fileName.endsWith(".js")) {
658
794
  next();
659
795
  return;
660
796
  }
661
- const bundle = req?.originalUrl?.split("/")?.pop()?.split(".js")?.shift();
797
+ const bundle = fileName.slice(0, -".js".length);
662
798
  const foundBundle = config?.bundles?.find((bundleItem) => bundleItem?.id === bundle);
663
799
  if (!foundBundle) {
664
800
  next();
@@ -677,6 +813,9 @@ async function createXPineRouter(app, beforeErrorRoute) {
677
813
  app.use(verifyAuthenticatedBundleMiddleware);
678
814
  app.use(express.static(config.distPublicDir));
679
815
  app.use(requestIP.mw());
816
+ if (config?.csrf) {
817
+ app.use(createCsrfMiddleware(typeof config.csrf === "object" ? config.csrf : void 0));
818
+ }
680
819
  const { router, routeResults } = await createRouter();
681
820
  app.use(function replaceableRouter(req, res, next) {
682
821
  router(req, res, next);
@@ -696,11 +835,13 @@ function routeHasStaticPath(route, params) {
696
835
  const paramEntries = Object.entries(params);
697
836
  let routeToStaticPath = route;
698
837
  for (const [key, value] of paramEntries) {
699
- routeToStaticPath = routeToStaticPath.replace(`:${key}`, value);
700
- if (key === "0") routeToStaticPath = routeToStaticPath.replace(/\/\*/g, `/${value}`);
838
+ routeToStaticPath = routeToStaticPath.replace(`:${key}`, () => value);
839
+ if (key === "0") routeToStaticPath = routeToStaticPath.replace(/\/\*/g, () => `/${value}`);
701
840
  }
702
841
  routeToStaticPath += "/index.html";
703
- const outputPath = path5.join(config.distPagesDir, routeToStaticPath);
842
+ const pagesDir = path5.resolve(config.distPagesDir);
843
+ const outputPath = path5.resolve(pagesDir, "." + path5.posix.normalize("/" + routeToStaticPath));
844
+ if (outputPath !== pagesDir && !outputPath.startsWith(pagesDir + path5.sep)) return false;
704
845
  if (fs5.existsSync(outputPath)) return outputPath;
705
846
  return false;
706
847
  }
@@ -1118,7 +1259,7 @@ async function buildStaticFiles(config2, component, componentImport, builtCompon
1118
1259
  function determineOutputPathForStaticPath(builtComponentPath, componentDynamicPaths) {
1119
1260
  if (componentDynamicPaths?.length) {
1120
1261
  return componentDynamicPaths.reduce((total, current) => {
1121
- return total.replace(`/[${current}]`, "");
1262
+ return total.replace(`/[...${current}]`, "").replace(`/[${current}]`, "");
1122
1263
  }, path6.dirname(builtComponentPath));
1123
1264
  } else {
1124
1265
  if (builtComponentPath?.match(regex_default.endsWithIndex)) {
@@ -1134,7 +1275,8 @@ function getComponentDynamicPaths(componentPath) {
1134
1275
  if (!matches?.length) return null;
1135
1276
  const output = [];
1136
1277
  for (const match of matches) {
1137
- output.push(match[2]);
1278
+ if (!match[2]) continue;
1279
+ output.push(match[2].replace(/^\.\.\./, ""));
1138
1280
  }
1139
1281
  return output;
1140
1282
  }
@@ -1316,9 +1458,9 @@ async function runDevServer() {
1316
1458
  });
1317
1459
  }
1318
1460
  function asyncServerClose(server) {
1319
- return new Promise((resolve, reject) => {
1320
- server.close();
1321
- resolve(true);
1461
+ return new Promise((resolve) => {
1462
+ server.close(() => resolve(true));
1463
+ server.closeAllConnections?.();
1322
1464
  });
1323
1465
  }
1324
1466
  function createRebuildEmitter(delay = 500) {
@@ -1326,39 +1468,51 @@ function createRebuildEmitter(delay = 500) {
1326
1468
  }
1327
1469
  ;
1328
1470
  const rebuildEmitter = new RebuildEmitter();
1329
- let start = 0;
1330
- let hasEmitted = false;
1471
+ let timeout = null;
1331
1472
  rebuildEmitter.on("rebuild-server", () => {
1332
- hasEmitted = false;
1333
- trigger();
1473
+ if (timeout) clearTimeout(timeout);
1474
+ timeout = setTimeout(() => {
1475
+ timeout = null;
1476
+ rebuildEmitter.emit("done");
1477
+ }, delay);
1334
1478
  });
1335
- function trigger() {
1336
- start = Date.now();
1337
- const interval = setInterval(() => {
1338
- if (hasEmitted) return;
1339
- const now = Date.now();
1340
- if (now - start >= delay) {
1341
- rebuildEmitter.emit("done");
1342
- clearInterval(interval);
1343
- hasEmitted = true;
1344
- }
1345
- }, 100);
1346
- }
1347
1479
  return rebuildEmitter;
1348
1480
  }
1349
1481
 
1350
1482
  // src/util/html.ts
1483
+ var RawHtml = class {
1484
+ constructor(value) {
1485
+ this.value = value;
1486
+ }
1487
+ toString() {
1488
+ return this.value;
1489
+ }
1490
+ };
1491
+ var RAW_TEXT_ELEMENTS = /* @__PURE__ */ new Set(["script", "style"]);
1492
+ function escapeHtml(value) {
1493
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
1494
+ }
1495
+ function raw(value) {
1496
+ return new RawHtml(value);
1497
+ }
1498
+ function renderChild(child, rawText) {
1499
+ if (Array.isArray(child)) return child.map((item) => renderChild(item, rawText)).join("");
1500
+ if (child instanceof RawHtml) return child.value;
1501
+ if (!child) return "";
1502
+ return rawText ? String(child) : escapeHtml(child);
1503
+ }
1351
1504
  var html = class {
1505
+ static raw = raw;
1352
1506
  static attributeObjectToString(props) {
1353
1507
  if (!props) return "";
1354
1508
  return Object.entries(props).filter(([, value]) => value !== null && value !== void 0).map(([key, value], index) => {
1355
1509
  const start = index === 0 ? " " : "";
1356
- return `${start}${key}="${value}"`;
1510
+ return `${start}${key}="${escapeHtml(value)}"`;
1357
1511
  }).join(" ");
1358
1512
  }
1359
1513
  static async fragment(props) {
1360
1514
  const childrenResult = await Promise.all(props.children.flat());
1361
- return childrenResult.filter(Boolean).join("");
1515
+ return new RawHtml(childrenResult.map((child) => renderChild(child, false)).join(""));
1362
1516
  }
1363
1517
  static async createElement(type, props, ...children) {
1364
1518
  const childrenResult = await Promise.all(children.flat());
@@ -1366,7 +1520,9 @@ var html = class {
1366
1520
  const result = await type({ ...props, children: childrenResult });
1367
1521
  return result;
1368
1522
  }
1369
- return `<${type}${this.attributeObjectToString(props)}>${childrenResult.filter(Boolean).join("")}</${type}>`;
1523
+ const rawText = RAW_TEXT_ELEMENTS.has(type);
1524
+ const inner = childrenResult.map((child) => renderChild(child, rawText)).join("");
1525
+ return new RawHtml(`<${type}${this.attributeObjectToString(props)}>${inner}</${type}>`);
1370
1526
  }
1371
1527
  };
1372
1528
  function JSXRuntime() {
@@ -1379,6 +1535,7 @@ function getCurrentBreakpoint() {
1379
1535
  }
1380
1536
  export {
1381
1537
  JSXRuntime,
1538
+ RawHtml,
1382
1539
  addToContextArray,
1383
1540
  buildApp,
1384
1541
  buildFilesWithConfigs,
@@ -1389,9 +1546,11 @@ export {
1389
1546
  config,
1390
1547
  context,
1391
1548
  createContext,
1549
+ createCsrfMiddleware,
1392
1550
  createRouter,
1393
1551
  createXPineRouter,
1394
1552
  determineOutputPathForStaticPath,
1553
+ escapeHtml,
1395
1554
  filePathToURLPath,
1396
1555
  fromRoot,
1397
1556
  getComponentDynamicPaths,
@@ -1399,9 +1558,13 @@ export {
1399
1558
  getTokenFromRequest,
1400
1559
  html,
1401
1560
  logSize,
1561
+ raw,
1402
1562
  routeHasStaticPath,
1403
1563
  runDevServer,
1404
1564
  setupEnv,
1405
1565
  signUser,
1566
+ spreadConcretePath,
1567
+ spreadSlugName,
1568
+ spreadWildcardPath,
1406
1569
  verifyUser
1407
1570
  };
@@ -1 +1 @@
1
- {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAGzC,wBAAsB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,oBAiB7D;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAQ5D;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,UAMrD"}
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAgBzC,wBAAsB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,oBAgB7D;AAED,wBAAsB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAO5D;AAED,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,aAAa,UAKrD"}
@@ -1,4 +1,9 @@
1
1
  import { Express } from 'express';
2
+ export declare function spreadConcretePath(templateRoute: string, entry: {
3
+ [key: string]: string;
4
+ }): string;
5
+ export declare function spreadWildcardPath(templateRoute: string): string;
6
+ export declare function spreadSlugName(templateRoute: string): string | null;
2
7
  export declare function createRouter(): Promise<{
3
8
  router: import("express-serve-static-core").Router;
4
9
  routeResults: any[];
@@ -1 +1 @@
1
- {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAgB,OAAO,EAAqB,MAAM,SAAS,CAAC;AAqJ5E,wBAAsB,YAAY;;;GAkCjC;AAsCD,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,iBA0B1F;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,KAAK,CAanG;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,OAAc,UAQzE"}
1
+ {"version":3,"file":"express.d.ts","sourceRoot":"","sources":["../../src/express.ts"],"names":[],"mappings":"AAAA,OAAgB,EAAgB,OAAO,EAA6C,MAAM,SAAS,CAAC;AA8FpG,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,MAAM,CAKlG;AAGD,wBAAgB,kBAAkB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,CAIhE;AAGD,wBAAgB,cAAc,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAGnE;AAqLD,wBAAsB,YAAY;;;GAuBjC;AAuDD,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,GAAG,EAAE,gBAAgB,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,iBAgC1F;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,KAAK,CAoBnG;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,OAAc,UAQzE"}
@@ -1 +1 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/scripts/build.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,UAAU,EAA2B,aAAa,EAAE,MAAM,aAAa,CAAC;AAajF,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAA;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,iBAgDhD;AA4JD,wBAAsB,yBAAyB,kBAiC9C;AAED,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,WAAkB,iBAa9F;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,aAAa,EAAE,iBAezE;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,GAAG,EAAE,kBAAkB,EAAE,MAAM,iBA2FpI;AAED,wBAAgB,gCAAgC,CAAC,kBAAkB,EAAE,MAAM,EAAE,qBAAqB,CAAC,EAAE,MAAM,EAAE,UAe5G;AAED,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAQxE;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACZ,CAAA;AAGD,wBAAsB,eAAe,CAAC,aAAa,EAAE,aAAa,EAAE,EAAE,KAAK,CAAC,EAAE,OAAO,iBAoDpF;AAED,wBAAsB,YAAY,kBA4CjC"}
1
+ {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/scripts/build.ts"],"names":[],"mappings":"AAoBA,OAAO,EAAE,UAAU,EAA2B,aAAa,EAAE,MAAM,aAAa,CAAC;AAajF,KAAK,YAAY,GAAG;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B,CAAA;AAED,wBAAsB,QAAQ,CAAC,IAAI,EAAE,YAAY,iBAgDhD;AA4JD,wBAAsB,yBAAyB,kBAiC9C;AAED,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,eAAe,WAAkB,iBAa9F;AAED,wBAAsB,qBAAqB,CAAC,aAAa,EAAE,aAAa,EAAE,iBAezE;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,eAAe,EAAE,GAAG,EAAE,kBAAkB,EAAE,MAAM,iBA2FpI;AAED,wBAAgB,gCAAgC,CAAC,kBAAkB,EAAE,MAAM,EAAE,qBAAqB,CAAC,EAAE,MAAM,EAAE,UAgB5G;AAED,wBAAgB,wBAAwB,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM,EAAE,CAUxE;AAED,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,EAAE,MAAM,CAAC;CACZ,CAAA;AAGD,wBAAsB,eAAe,CAAC,aAAa,EAAE,aAAa,EAAE,EAAE,KAAK,CAAC,EAAE,OAAO,iBAoDpF;AAED,wBAAsB,YAAY,kBA4CjC"}
@@ -321,6 +321,9 @@ var regex_default = {
321
321
  configFile: /\+config\.[tj]sx?/g,
322
322
  dynamicRoutes: /(\[)(.*?)(\])/g,
323
323
  isDynamicRoute: /\[(.*)\]/g,
324
+ // Multi-segment (slash-containing) dynamic route, e.g. [...slug]
325
+ spreadRoute: /\[\.\.\.(.*?)\]/g,
326
+ isSpreadRoute: /\[\.\.\..*?\]/,
324
327
  endsWithTSX: /\.tsx$/,
325
328
  endsWithJSX: /\.jsx$/,
326
329
  endsWithJs: /\.js$/,
@@ -891,7 +894,7 @@ async function buildStaticFiles(config2, component, componentImport, builtCompon
891
894
  function determineOutputPathForStaticPath(builtComponentPath, componentDynamicPaths) {
892
895
  if (componentDynamicPaths?.length) {
893
896
  return componentDynamicPaths.reduce((total, current) => {
894
- return total.replace(`/[${current}]`, "");
897
+ return total.replace(`/[...${current}]`, "").replace(`/[${current}]`, "");
895
898
  }, path5.dirname(builtComponentPath));
896
899
  } else {
897
900
  if (builtComponentPath?.match(regex_default.endsWithIndex)) {
@@ -907,7 +910,8 @@ function getComponentDynamicPaths(componentPath) {
907
910
  if (!matches?.length) return null;
908
911
  const output = [];
909
912
  for (const match of matches) {
910
- output.push(match[2]);
913
+ if (!match[2]) continue;
914
+ output.push(match[2].replace(/^\.\.\./, ""));
911
915
  }
912
916
  return output;
913
917
  }