bun-plugin-stx 0.2.76 → 0.2.78

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.d.ts CHANGED
@@ -12,6 +12,36 @@
12
12
  * projects that nest routable views under `pages/`
13
13
  */
14
14
  export declare function buildDynamicRouteRegexes(fileRouteBase: string): RegExp[];
15
+ /**
16
+ * Are we serving in production? In production the built-in 404 must not
17
+ * enumerate the app's route list (information disclosure) and must not
18
+ * inject the HMR client.
19
+ *
20
+ * Reuses the standard Stacks/Node signals — `NODE_ENV` / `APP_ENV`. serve.ts
21
+ * has no pre-existing prod/dev flag threaded through its options, so these
22
+ * env vars are the canonical signal (the same ones the wider stacks codebase
23
+ * keys production behaviour off of).
24
+ */
25
+ export declare function isProductionServe(): boolean;
26
+ /**
27
+ * Render the built-in fallback 404 page.
28
+ *
29
+ * - In development (`isProduction: false`) it lists the discovered routes as
30
+ * clickable links — a genuinely useful dev affordance — and reflects the
31
+ * requested path (HTML-escaped).
32
+ * - In production (`isProduction: true`) it renders a clean, neutral, self-
33
+ * contained page with NO route list and NO reflected path, so nothing about
34
+ * the app's internal structure leaks to anonymous visitors.
35
+ *
36
+ * The page is a complete HTML document, styled inline (system font stack,
37
+ * centered, light + dark via `prefers-color-scheme`, no external assets).
38
+ * The caller is responsible for HMR injection (dev only) and the 404 status.
39
+ */
40
+ export declare function render404Page(opts: {
41
+ path: string
42
+ routes?: string[]
43
+ isProduction: boolean
44
+ }): string;
15
45
  export declare function bundleBrowserAsset(entrypoint: string): Promise<Response>;
16
46
  /**
17
47
  * Start the STX development server
package/dist/serve.js CHANGED
@@ -8709,6 +8709,104 @@ function buildDynamicRouteRegexes(fileRouteBase) {
8709
8709
  }
8710
8710
  return patterns;
8711
8711
  }
8712
+ function escapeHtml(input) {
8713
+ return input.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
8714
+ }
8715
+ function isProductionServe() {
8716
+ return process19.env.NODE_ENV === "production" || process19.env.APP_ENV === "production";
8717
+ }
8718
+ function render404Page(opts) {
8719
+ const { path, routes = [], isProduction } = opts;
8720
+ const baseStyles = `
8721
+ *, *::before, *::after { box-sizing: border-box; }
8722
+ body {
8723
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
8724
+ margin: 0;
8725
+ min-height: 100vh;
8726
+ display: flex;
8727
+ align-items: center;
8728
+ justify-content: center;
8729
+ padding: 2rem;
8730
+ background: #ffffff;
8731
+ color: #1a202c;
8732
+ line-height: 1.5;
8733
+ }
8734
+ main { max-width: 32rem; width: 100%; text-align: center; }
8735
+ .code { font-size: 3.5rem; font-weight: 700; letter-spacing: -0.02em; margin: 0; }
8736
+ h1 { font-size: 1.25rem; font-weight: 600; margin: 0.75rem 0 0.5rem; }
8737
+ p { margin: 0.5rem 0; color: #4a5568; }
8738
+ a { color: #4a5568; }
8739
+ .home {
8740
+ display: inline-block;
8741
+ margin-top: 1.5rem;
8742
+ padding: 0.6rem 1.25rem;
8743
+ border-radius: 0.5rem;
8744
+ background: #1a202c;
8745
+ color: #ffffff;
8746
+ text-decoration: none;
8747
+ font-weight: 500;
8748
+ font-size: 0.95rem;
8749
+ }
8750
+ .home:hover { opacity: 0.85; }
8751
+ @media (prefers-color-scheme: dark) {
8752
+ body { background: #0f1115; color: #e2e8f0; }
8753
+ p { color: #a0aec0; }
8754
+ a { color: #a0aec0; }
8755
+ .home { background: #e2e8f0; color: #0f1115; }
8756
+ }`;
8757
+ if (isProduction) {
8758
+ return `<!DOCTYPE html>
8759
+ <html lang="en">
8760
+ <head>
8761
+ <meta charset="utf-8">
8762
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8763
+ <meta name="robots" content="noindex">
8764
+ <title>404 - Page Not Found</title>
8765
+ <style>${baseStyles}
8766
+ </style>
8767
+ </head>
8768
+ <body>
8769
+ <main>
8770
+ <p class="code">404</p>
8771
+ <h1>Page not found</h1>
8772
+ <p>The page you're looking for doesn't exist or may have moved.</p>
8773
+ <a class="home" href="/">Go to homepage</a>
8774
+ </main>
8775
+ </body>
8776
+ </html>`;
8777
+ }
8778
+ const routesList = routes.map((route) => `<li><a href="/${escapeHtml(route)}">/${escapeHtml(route)}</a></li>`).join(`
8779
+ `);
8780
+ return `<!DOCTYPE html>
8781
+ <html lang="en">
8782
+ <head>
8783
+ <meta charset="utf-8">
8784
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8785
+ <title>404 - Not Found</title>
8786
+ <style>${baseStyles}
8787
+ main { max-width: 40rem; }
8788
+ .routes { list-style: none; padding: 0; margin: 2rem 0 0; text-align: left; }
8789
+ .routes li { margin: 0.35rem 0; }
8790
+ .routes a { color: #667eea; text-decoration: none; font-size: 1.05rem; }
8791
+ .routes a:hover { text-decoration: underline; }
8792
+ .dev-note { font-size: 0.85rem; margin-top: 1.5rem; opacity: 0.7; }
8793
+ @media (prefers-color-scheme: dark) { .routes a { color: #9aa8ff; } }
8794
+ </style>
8795
+ </head>
8796
+ <body>
8797
+ <main>
8798
+ <p class="code">404</p>
8799
+ <h1>Page not found</h1>
8800
+ <p>The page "${escapeHtml(path)}" doesn't exist.</p>
8801
+ <h2 style="font-size:1rem;margin-top:2rem;">Available pages</h2>
8802
+ <ul class="routes">${routesList ? `
8803
+ ${routesList}
8804
+ ` : ""}</ul>
8805
+ <p class="dev-note">This route list is shown in development only.</p>
8806
+ </main>
8807
+ </body>
8808
+ </html>`;
8809
+ }
8712
8810
  var defaultStxConfig = {
8713
8811
  partialsDir: "partials",
8714
8812
  componentsDir: "components",
@@ -10124,53 +10222,40 @@ ${pageSetupScripts.join(`
10124
10222
  if (path === "/favicon.ico") {
10125
10223
  return new Response(null, { status: 204 });
10126
10224
  }
10127
- const files = await discoverFiles();
10128
- const availableRoutes = [];
10129
- for (const filePath of files) {
10130
- let normalizedPath = filePath.replace(/^\.\//, "").replace(/\\/g, "/");
10131
- for (const pattern of patterns) {
10132
- const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\/$/, "");
10133
- if (normalizedPath.startsWith(`${normalizedPattern}/`)) {
10134
- normalizedPath = normalizedPath.slice(normalizedPattern.length + 1);
10135
- break;
10225
+ const isProd = isProductionServe();
10226
+ for (const custom404 of ["/404", "/errors/404"]) {
10227
+ try {
10228
+ const customContent = await getRoute(custom404);
10229
+ if (customContent) {
10230
+ return new Response(isProd ? customContent : injectHmrClient(customContent), {
10231
+ status: 404,
10232
+ headers: { "Content-Type": "text/html; charset=utf-8" }
10233
+ });
10136
10234
  }
10137
- }
10138
- let route2 = normalizedPath.replace(/\.(stx|md|html)$/, "");
10139
- if (route2.startsWith("pages/")) {
10140
- route2 = route2.slice(6);
10141
- }
10142
- availableRoutes.push(`<li><a href="/${route2}">/${route2}</a></li>`);
10235
+ } catch {}
10143
10236
  }
10144
- const routesList = availableRoutes.join(`
10145
- `);
10146
- return new Response(injectHmrClient(`
10147
- <!DOCTYPE html>
10148
- <html>
10149
- <head>
10150
- <title>404 - Not Found</title>
10151
- <style>
10152
- body {
10153
- font-family: system-ui, sans-serif;
10154
- max-width: 800px;
10155
- margin: 4rem auto;
10156
- padding: 2rem;
10157
- text-align: center;
10237
+ let availableRoutes;
10238
+ if (!isProd) {
10239
+ const files = await discoverFiles();
10240
+ availableRoutes = [];
10241
+ for (const filePath of files) {
10242
+ let normalizedPath = filePath.replace(/^\.\//, "").replace(/\\/g, "/");
10243
+ for (const pattern of patterns) {
10244
+ const normalizedPattern = pattern.replace(/\\/g, "/").replace(/\/$/, "");
10245
+ if (normalizedPath.startsWith(`${normalizedPattern}/`)) {
10246
+ normalizedPath = normalizedPath.slice(normalizedPattern.length + 1);
10247
+ break;
10248
+ }
10249
+ }
10250
+ let route2 = normalizedPath.replace(/\.(stx|md|html)$/, "");
10251
+ if (route2.startsWith("pages/")) {
10252
+ route2 = route2.slice(6);
10253
+ }
10254
+ availableRoutes.push(route2);
10255
+ }
10158
10256
  }
10159
- h1 { color: #e53e3e; }
10160
- ul { list-style: none; padding: 0; margin: 2rem 0; }
10161
- li { margin: 0.5rem 0; }
10162
- a { color: #667eea; text-decoration: none; font-size: 1.1rem; }
10163
- a:hover { text-decoration: underline; }
10164
- </style>
10165
- </head>
10166
- <body>
10167
- <h1>404 - Page Not Found</h1>
10168
- <p>The page "${path}" doesn't exist.</p>
10169
- <h2>Available pages:</h2>
10170
- <ul>${routesList}</ul>
10171
- </body>
10172
- </html>
10173
- `), {
10257
+ const notFoundHtml = render404Page({ path, routes: availableRoutes, isProduction: isProd });
10258
+ return new Response(isProd ? notFoundHtml : injectHmrClient(notFoundHtml), {
10174
10259
  status: 404,
10175
10260
  headers: { "Content-Type": "text/html; charset=utf-8" }
10176
10261
  });
@@ -10255,6 +10340,8 @@ if (import.meta.main) {
10255
10340
  }
10256
10341
  export {
10257
10342
  serve,
10343
+ render404Page,
10344
+ isProductionServe,
10258
10345
  bundleBrowserAsset,
10259
10346
  buildDynamicRouteRegexes
10260
10347
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-plugin-stx",
3
3
  "type": "module",
4
- "version": "0.2.76",
4
+ "version": "0.2.78",
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",