@rshono/core 1.0.0-rc.2 → 1.0.0-rc.4

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 (123) hide show
  1. package/README.md +45 -29
  2. package/dist/builder/rspack-config.d.ts +3 -3
  3. package/dist/builder/rspack-config.js +3 -3
  4. package/dist/builder/rspack-config.js.map +1 -1
  5. package/dist/cli/build.d.ts +2 -2
  6. package/dist/cli/build.js.map +1 -1
  7. package/dist/cli/dev.d.ts +2 -2
  8. package/dist/cli/dev.js.map +1 -1
  9. package/dist/cli/index.js +11 -10
  10. package/dist/cli/index.js.map +1 -1
  11. package/dist/config.d.ts +4 -17
  12. package/dist/config.d.ts.map +1 -1
  13. package/dist/config.js.map +1 -1
  14. package/dist/deploy/cloudflare/build.js +1 -1
  15. package/dist/deploy/cloudflare/build.js.map +1 -1
  16. package/dist/deploy/cloudflare/runtime.d.ts.map +1 -1
  17. package/dist/deploy/cloudflare/runtime.js +7 -37
  18. package/dist/deploy/cloudflare/runtime.js.map +1 -1
  19. package/dist/deploy/contract.d.ts +14 -13
  20. package/dist/deploy/contract.d.ts.map +1 -1
  21. package/dist/deploy/contract.js.map +1 -1
  22. package/dist/deploy/filesystem.d.ts +1 -1
  23. package/dist/deploy/filesystem.d.ts.map +1 -1
  24. package/dist/deploy/filesystem.js +1 -3
  25. package/dist/deploy/filesystem.js.map +1 -1
  26. package/dist/deploy/node/runtime.d.ts +6 -2
  27. package/dist/deploy/node/runtime.d.ts.map +1 -1
  28. package/dist/deploy/node/runtime.js +30 -5
  29. package/dist/deploy/node/runtime.js.map +1 -1
  30. package/dist/deploy/presets.d.ts +5 -1
  31. package/dist/deploy/presets.d.ts.map +1 -1
  32. package/dist/deploy/presets.js +15 -28
  33. package/dist/deploy/presets.js.map +1 -1
  34. package/dist/deploy/vercel/runtime.d.ts.map +1 -1
  35. package/dist/deploy/vercel/runtime.js +0 -3
  36. package/dist/deploy/vercel/runtime.js.map +1 -1
  37. package/dist/index.d.ts +4 -9
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +6 -3
  40. package/dist/index.js.map +1 -1
  41. package/dist/router.d.ts +14 -15
  42. package/dist/router.d.ts.map +1 -1
  43. package/dist/router.js +5 -6
  44. package/dist/router.js.map +1 -1
  45. package/dist/runtime/client.d.ts +5 -6
  46. package/dist/runtime/client.d.ts.map +1 -1
  47. package/dist/runtime/client.js +5 -6
  48. package/dist/runtime/client.js.map +1 -1
  49. package/dist/runtime/context.d.ts +36 -40
  50. package/dist/runtime/context.d.ts.map +1 -1
  51. package/dist/runtime/context.js +41 -53
  52. package/dist/runtime/context.js.map +1 -1
  53. package/dist/runtime/entry.client.d.ts +4 -0
  54. package/dist/runtime/entry.client.d.ts.map +1 -1
  55. package/dist/runtime/entry.client.js +161 -147
  56. package/dist/runtime/entry.client.js.map +1 -1
  57. package/dist/runtime/entry.rsc.d.ts.map +1 -1
  58. package/dist/runtime/entry.rsc.js +80 -63
  59. package/dist/runtime/entry.rsc.js.map +1 -1
  60. package/dist/runtime/entry.ssr.d.ts +9 -1
  61. package/dist/runtime/entry.ssr.d.ts.map +1 -1
  62. package/dist/runtime/entry.ssr.js +36 -18
  63. package/dist/runtime/entry.ssr.js.map +1 -1
  64. package/dist/runtime/flight-inject.d.ts +31 -0
  65. package/dist/runtime/flight-inject.d.ts.map +1 -0
  66. package/dist/runtime/flight-inject.js +221 -0
  67. package/dist/runtime/flight-inject.js.map +1 -0
  68. package/dist/runtime/navigation.d.ts +10 -39
  69. package/dist/runtime/navigation.d.ts.map +1 -1
  70. package/dist/runtime/navigation.js +7 -53
  71. package/dist/runtime/navigation.js.map +1 -1
  72. package/dist/runtime/request.d.ts +6 -0
  73. package/dist/runtime/request.d.ts.map +1 -1
  74. package/dist/runtime/request.js +8 -0
  75. package/dist/runtime/request.js.map +1 -1
  76. package/dist/runtime/server.d.ts +2 -2
  77. package/dist/runtime/server.d.ts.map +1 -1
  78. package/dist/runtime/server.js +2 -2
  79. package/dist/runtime/server.js.map +1 -1
  80. package/dist/server/headers.d.ts +9 -9
  81. package/dist/server/headers.js +9 -9
  82. package/dist/server/headers.js.map +1 -1
  83. package/dist/server/load-config.d.ts +2 -2
  84. package/dist/server/load-config.d.ts.map +1 -1
  85. package/dist/server/load-config.js +22 -11
  86. package/dist/server/load-config.js.map +1 -1
  87. package/dist/server/prerendered.d.ts +52 -9
  88. package/dist/server/prerendered.d.ts.map +1 -1
  89. package/dist/server/prerendered.js +57 -0
  90. package/dist/server/prerendered.js.map +1 -1
  91. package/dist/server/server-config.d.ts +11 -18
  92. package/dist/server/server-config.d.ts.map +1 -1
  93. package/dist/server/server-config.js +9 -12
  94. package/dist/server/server-config.js.map +1 -1
  95. package/dist/server/ssg.d.ts +2 -2
  96. package/dist/server/ssg.d.ts.map +1 -1
  97. package/dist/server/ssg.js +11 -23
  98. package/dist/server/ssg.js.map +1 -1
  99. package/package.json +13 -16
  100. package/dist/deploy/bun/runtime.d.ts +0 -11
  101. package/dist/deploy/bun/runtime.d.ts.map +0 -1
  102. package/dist/deploy/bun/runtime.js +0 -22
  103. package/dist/deploy/bun/runtime.js.map +0 -1
  104. package/dist/deploy/deno/runtime.d.ts +0 -11
  105. package/dist/deploy/deno/runtime.d.ts.map +0 -1
  106. package/dist/deploy/deno/runtime.js +0 -16
  107. package/dist/deploy/deno/runtime.js.map +0 -1
  108. package/dist/deploy/listen.d.ts +0 -20
  109. package/dist/deploy/listen.d.ts.map +0 -1
  110. package/dist/deploy/listen.js +0 -24
  111. package/dist/deploy/listen.js.map +0 -1
  112. package/dist/deploy/netlify/build.d.ts +0 -8
  113. package/dist/deploy/netlify/build.d.ts.map +0 -1
  114. package/dist/deploy/netlify/build.js +0 -52
  115. package/dist/deploy/netlify/build.js.map +0 -1
  116. package/dist/deploy/netlify/runtime.d.ts +0 -13
  117. package/dist/deploy/netlify/runtime.d.ts.map +0 -1
  118. package/dist/deploy/netlify/runtime.js +0 -24
  119. package/dist/deploy/netlify/runtime.js.map +0 -1
  120. package/dist/server/compress.d.ts +0 -15
  121. package/dist/server/compress.d.ts.map +0 -1
  122. package/dist/server/compress.js +0 -76
  123. package/dist/server/compress.js.map +0 -1
@@ -1,19 +1,20 @@
1
1
  /**
2
2
  * The single source of truth for the framework's built-in defaults.
3
3
  *
4
- * `renderTimeoutMs` and `maxBodyBytes` are resolved here, into the bundle. `port` and `host` are not:
5
- * they stay env-overridable at runtime, so the default is applied where the address is resolved
6
- * (`deploy/listen.ts` for a server bundle, `cli/dev.ts` for the dev server) and this is what both read.
4
+ * `maxBodyBytes` is resolved here, into the bundle. `port` and `host` are not
5
+ * settings at all: they are read from `PORT` / `HOST` (or `--port`) wherever the address is resolved
6
+ * — `deploy/node/runtime.ts` for a server bundle, `cli/dev.ts` for the dev server — and this is the
7
+ * default both fall back to. A config field for them would have been a fourth precedence level for
8
+ * one number, overridden by the environment on every host that matters.
7
9
  */
8
10
  export const SERVER_DEFAULTS = {
9
- renderTimeoutMs: 10_000,
10
11
  maxBodyBytes: 1024 * 1024, // 1 MiB, matching Next.js's server-action body-size limit.
11
12
  port: 3000,
12
13
  host: '0.0.0.0',
13
14
  };
14
15
  /**
15
- * The built-in {@link RSHonoConfig.csp} policy, keyed by directive so
16
- * {@link RSHonoConfig.cspDirectives} can override entries individually.
16
+ * The built-in {@link RshonoConfig.csp} policy, keyed by directive so
17
+ * {@link RshonoConfig.cspDirectives} can override entries individually.
17
18
  *
18
19
  * `script-src` carries the per-request nonce, appended at request time (see `entry.rsc.tsx`).
19
20
  * `style-src` needs `'unsafe-inline'` because React writes inline styles.
@@ -32,7 +33,7 @@ export const CSP_DEFAULTS = {
32
33
  'form-action': "'self'",
33
34
  };
34
35
  const UNITS = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };
35
- /** Parse a {@link RSHonoConfig.bodySizeLimit} value into a byte count (`false`/`0` → `0`, disabling the cap). */
36
+ /** Parse a {@link RshonoConfig.bodySizeLimit} value into a byte count (`false`/`0` → `0`, disabling the cap). */
36
37
  export function parseByteSize(value) {
37
38
  if (value === undefined)
38
39
  return undefined;
@@ -67,7 +68,7 @@ function resolveCspDirectives(overrides) {
67
68
  return Object.fromEntries(Object.entries({ ...CSP_DEFAULTS, ...overrides }).filter(([, value]) => value.trim() !== ''));
68
69
  }
69
70
  /**
70
- * Resolve the user's {@link RSHonoConfig} into the {@link ServerConfig} baked into the bundle.
71
+ * Resolve the user's {@link RshonoConfig} into the {@link ServerConfig} baked into the bundle.
71
72
  *
72
73
  * `isDev` is a build-time input rather than a config field because it decides one thing the user
73
74
  * shouldn't have to: `trustProxy` is forced on under `rshono dev`, where the framework's own proxy
@@ -76,7 +77,6 @@ function resolveCspDirectives(overrides) {
76
77
  export function resolveServerConfig(config, { isDev }) {
77
78
  return {
78
79
  isDev,
79
- renderTimeoutMs: config.renderTimeout ?? SERVER_DEFAULTS.renderTimeoutMs,
80
80
  trustProxy: isDev || (config.trustProxy ?? false),
81
81
  cspEnabled: config.csp ?? false,
82
82
  cspDirectives: resolveCspDirectives(config.cspDirectives),
@@ -86,9 +86,6 @@ export function resolveServerConfig(config, { isDev }) {
86
86
  .filter(Boolean)
87
87
  .map(normalizeOrigin),
88
88
  maxBodyBytes: parseByteSize(config.bodySizeLimit) ?? SERVER_DEFAULTS.maxBodyBytes,
89
- compress: config.compress ?? true,
90
- port: config.port,
91
- host: config.host,
92
89
  };
93
90
  }
94
91
  //# sourceMappingURL=server-config.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RSHonoConfig } from '../config.js';\n\n/**\n * The framework settings the server bundle needs at request time, fully resolved\n * (defaults applied, `bodySizeLimit` parsed to bytes, origins normalized to hosts).\n *\n * The value is produced once by {@link resolveServerConfig} from `rshono.config.ts`\n * and compiled into the server bundle as the `__RSHONO_CONFIG__` literal (see\n * `builder/rspack-config.ts`) — there is no runtime env-var interface for these.\n */\nexport interface ServerConfig {\n /**\n * `true` when the bundle came from `rshono dev`.\n *\n * Baked in rather than read from `process.env.NODE_ENV` at runtime: it is decided by which command\n * produced the bundle, and a deploy target need not have a `process` to read it from.\n */\n isDev: boolean;\n /** Deadline in ms for a single request (server action + flight + SSR). */\n renderTimeoutMs: number;\n /** Honour `X-Forwarded-Host` / `-Proto` when resolving the browser-facing URL. Forced on in dev. */\n trustProxy: boolean;\n /** Send a strict per-request-nonce Content-Security-Policy with every HTML document. */\n cspEnabled: boolean;\n /** The resolved CSP directives (built-in defaults with the user's merged over them), minus the nonce. */\n cspDirectives: Record<string, string>;\n /** CSRF origin check on server-action POSTs. */\n checkOrigin: boolean;\n /** Extra origins allowed to post server actions, normalized to lowercase `URL.host` values. */\n allowedOrigins: string[];\n /** Max request body in bytes before a 413; `0` disables the cap. */\n maxBodyBytes: number;\n /** Gzip compressible responses on the way out. */\n compress: boolean;\n /** Default listen port for `start` (overridden by `PORT`). */\n port?: number;\n /** Default bind address for `start` (overridden by `HOST`). */\n host?: string;\n}\n\n/**\n * The single source of truth for the framework's built-in defaults.\n *\n * `renderTimeoutMs` and `maxBodyBytes` are resolved here, into the bundle. `port` and `host` are not:\n * they stay env-overridable at runtime, so the default is applied where the address is resolved\n * (`deploy/listen.ts` for a server bundle, `cli/dev.ts` for the dev server) and this is what both read.\n */\nexport const SERVER_DEFAULTS = {\n renderTimeoutMs: 10_000,\n maxBodyBytes: 1024 * 1024, // 1 MiB, matching Next.js's server-action body-size limit.\n port: 3000,\n host: '0.0.0.0',\n} as const;\n\n/**\n * The built-in {@link RSHonoConfig.csp} policy, keyed by directive so\n * {@link RSHonoConfig.cspDirectives} can override entries individually.\n *\n * `script-src` carries the per-request nonce, appended at request time (see `entry.rsc.tsx`).\n * `style-src` needs `'unsafe-inline'` because React writes inline styles.\n */\nexport const CSP_DEFAULTS: Record<string, string> = {\n 'default-src': \"'self'\",\n 'script-src': \"'self'\",\n 'style-src': \"'self' 'unsafe-inline'\",\n 'img-src': \"'self' data:\",\n 'connect-src': \"'self'\",\n // Not covered by default-src, and each closes an injection route of its own: a stray <base>\n // retargeting every relative URL, plugin content, framing (clickjacking), off-site form posts.\n 'base-uri': \"'self'\",\n 'object-src': \"'none'\",\n 'frame-ancestors': \"'none'\",\n 'form-action': \"'self'\",\n};\n\nconst UNITS: Record<string, number> = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };\n\n/** Parse a {@link RSHonoConfig.bodySizeLimit} value into a byte count (`false`/`0` → `0`, disabling the cap). */\nexport function parseByteSize(value: string | number | false | undefined): number | undefined {\n if (value === undefined) return undefined;\n if (value === false) return 0;\n if (typeof value === 'number') return value;\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*(b|kb|mb|gb)?\\s*$/i.exec(value);\n if (!match) {\n throw new Error(`[rshono] invalid bodySizeLimit ${JSON.stringify(value)} — use e.g. '1mb', 1048576, or false.`);\n }\n return Math.floor(Number(match[1]) * UNITS[(match[2] ?? 'b').toLowerCase()]);\n}\n\n/**\n * Normalize a config `allowedOrigins` entry (full origin or bare host) to a lowercase `URL.host`,\n * ready for a direct comparison against a parsed `Origin` header's host.\n *\n * A bare `host:port` has to be retried against a base, because on its own `'localhost:4000'`\n * parses as the *scheme* `localhost:` with path `4000` — leaving an empty host that would\n * silently never match anything. Throws rather than passing a junk entry through, so a typo\n * fails the build instead of quietly disabling the allowlist entry it was meant to add.\n */\nfunction normalizeOrigin(entry: string): string {\n const host = URL.parse(entry)?.host || URL.parse(`http://${entry}`)?.host;\n if (!host) {\n throw new Error(\n `[rshono] invalid allowedOrigins entry ${JSON.stringify(entry)} — use a full origin ('https://admin.example.com') or a bare host ('localhost:4000').`,\n );\n }\n return host.toLowerCase();\n}\n\n/** Drop directives the user blanked out, so `cspDirectives: { 'frame-ancestors': '' }` removes one. */\nfunction resolveCspDirectives(overrides: Record<string, string> | undefined): Record<string, string> {\n return Object.fromEntries(Object.entries({ ...CSP_DEFAULTS, ...overrides }).filter(([, value]) => value.trim() !== ''));\n}\n\n/**\n * Resolve the user's {@link RSHonoConfig} into the {@link ServerConfig} baked into the bundle.\n *\n * `isDev` is a build-time input rather than a config field because it decides one thing the user\n * shouldn't have to: `trustProxy` is forced on under `rshono dev`, where the framework's own proxy\n * is the only way in (it sets the forwarded headers itself and binds to localhost).\n */\nexport function resolveServerConfig(config: RSHonoConfig, { isDev }: { isDev: boolean }): ServerConfig {\n return {\n isDev,\n renderTimeoutMs: config.renderTimeout ?? SERVER_DEFAULTS.renderTimeoutMs,\n trustProxy: isDev || (config.trustProxy ?? false),\n cspEnabled: config.csp ?? false,\n cspDirectives: resolveCspDirectives(config.cspDirectives),\n checkOrigin: config.checkOrigin ?? true,\n allowedOrigins: (config.allowedOrigins ?? [])\n .map((entry) => entry.trim())\n .filter(Boolean)\n .map(normalizeOrigin),\n maxBodyBytes: parseByteSize(config.bodySizeLimit) ?? SERVER_DEFAULTS.maxBodyBytes,\n compress: config.compress ?? true,\n port: config.port,\n host: config.host,\n };\n}\n"]}
1
+ {"version":3,"file":"server-config.js","sourceRoot":"","sources":["../../src/server/server-config.ts"],"names":[],"mappings":"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","sourcesContent":["import type { RshonoConfig } from '../config.js';\n\n/**\n * The framework settings the server bundle needs at request time, fully resolved\n * (defaults applied, `bodySizeLimit` parsed to bytes, origins normalized to hosts).\n *\n * The value is produced once by {@link resolveServerConfig} from `rshono.config.ts`\n * and compiled into the server bundle as the `__RSHONO_CONFIG__` literal (see\n * `builder/rspack-config.ts`) — there is no runtime env-var interface for these.\n */\nexport interface ServerConfig {\n /**\n * `true` when the bundle came from `rshono dev`.\n *\n * Baked in rather than read from `process.env.NODE_ENV` at runtime: it is decided by which command\n * produced the bundle, and a deploy target need not have a `process` to read it from.\n */\n isDev: boolean;\n /** Honour `X-Forwarded-Host` / `-Proto` when resolving the browser-facing URL. Forced on in dev. */\n trustProxy: boolean;\n /** Send a strict per-request-nonce Content-Security-Policy with every HTML document. */\n cspEnabled: boolean;\n /** The resolved CSP directives (built-in defaults with the user's merged over them), minus the nonce. */\n cspDirectives: Record<string, string>;\n /** CSRF origin check on server-action POSTs. */\n checkOrigin: boolean;\n /** Extra origins allowed to post server actions, normalized to lowercase `URL.host` values. */\n allowedOrigins: string[];\n /** Max request body in bytes before a 413; `0` disables the cap. */\n maxBodyBytes: number;\n}\n\n/**\n * The single source of truth for the framework's built-in defaults.\n *\n * `maxBodyBytes` is resolved here, into the bundle. `port` and `host` are not\n * settings at all: they are read from `PORT` / `HOST` (or `--port`) wherever the address is resolved\n * — `deploy/node/runtime.ts` for a server bundle, `cli/dev.ts` for the dev server — and this is the\n * default both fall back to. A config field for them would have been a fourth precedence level for\n * one number, overridden by the environment on every host that matters.\n */\nexport const SERVER_DEFAULTS = {\n maxBodyBytes: 1024 * 1024, // 1 MiB, matching Next.js's server-action body-size limit.\n port: 3000,\n host: '0.0.0.0',\n} as const;\n\n/**\n * The built-in {@link RshonoConfig.csp} policy, keyed by directive so\n * {@link RshonoConfig.cspDirectives} can override entries individually.\n *\n * `script-src` carries the per-request nonce, appended at request time (see `entry.rsc.tsx`).\n * `style-src` needs `'unsafe-inline'` because React writes inline styles.\n */\nexport const CSP_DEFAULTS: Record<string, string> = {\n 'default-src': \"'self'\",\n 'script-src': \"'self'\",\n 'style-src': \"'self' 'unsafe-inline'\",\n 'img-src': \"'self' data:\",\n 'connect-src': \"'self'\",\n // Not covered by default-src, and each closes an injection route of its own: a stray <base>\n // retargeting every relative URL, plugin content, framing (clickjacking), off-site form posts.\n 'base-uri': \"'self'\",\n 'object-src': \"'none'\",\n 'frame-ancestors': \"'none'\",\n 'form-action': \"'self'\",\n};\n\nconst UNITS: Record<string, number> = { b: 1, kb: 1024, mb: 1024 ** 2, gb: 1024 ** 3 };\n\n/** Parse a {@link RshonoConfig.bodySizeLimit} value into a byte count (`false`/`0` → `0`, disabling the cap). */\nexport function parseByteSize(value: string | number | false | undefined): number | undefined {\n if (value === undefined) return undefined;\n if (value === false) return 0;\n if (typeof value === 'number') return value;\n const match = /^\\s*(\\d+(?:\\.\\d+)?)\\s*(b|kb|mb|gb)?\\s*$/i.exec(value);\n if (!match) {\n throw new Error(`[rshono] invalid bodySizeLimit ${JSON.stringify(value)} — use e.g. '1mb', 1048576, or false.`);\n }\n return Math.floor(Number(match[1]) * UNITS[(match[2] ?? 'b').toLowerCase()]);\n}\n\n/**\n * Normalize a config `allowedOrigins` entry (full origin or bare host) to a lowercase `URL.host`,\n * ready for a direct comparison against a parsed `Origin` header's host.\n *\n * A bare `host:port` has to be retried against a base, because on its own `'localhost:4000'`\n * parses as the *scheme* `localhost:` with path `4000` — leaving an empty host that would\n * silently never match anything. Throws rather than passing a junk entry through, so a typo\n * fails the build instead of quietly disabling the allowlist entry it was meant to add.\n */\nfunction normalizeOrigin(entry: string): string {\n const host = URL.parse(entry)?.host || URL.parse(`http://${entry}`)?.host;\n if (!host) {\n throw new Error(\n `[rshono] invalid allowedOrigins entry ${JSON.stringify(entry)} — use a full origin ('https://admin.example.com') or a bare host ('localhost:4000').`,\n );\n }\n return host.toLowerCase();\n}\n\n/** Drop directives the user blanked out, so `cspDirectives: { 'frame-ancestors': '' }` removes one. */\nfunction resolveCspDirectives(overrides: Record<string, string> | undefined): Record<string, string> {\n return Object.fromEntries(Object.entries({ ...CSP_DEFAULTS, ...overrides }).filter(([, value]) => value.trim() !== ''));\n}\n\n/**\n * Resolve the user's {@link RshonoConfig} into the {@link ServerConfig} baked into the bundle.\n *\n * `isDev` is a build-time input rather than a config field because it decides one thing the user\n * shouldn't have to: `trustProxy` is forced on under `rshono dev`, where the framework's own proxy\n * is the only way in (it sets the forwarded headers itself and binds to localhost).\n */\nexport function resolveServerConfig(config: RshonoConfig, { isDev }: { isDev: boolean }): ServerConfig {\n return {\n isDev,\n trustProxy: isDev || (config.trustProxy ?? false),\n cspEnabled: config.csp ?? false,\n cspDirectives: resolveCspDirectives(config.cspDirectives),\n checkOrigin: config.checkOrigin ?? true,\n allowedOrigins: (config.allowedOrigins ?? [])\n .map((entry) => entry.trim())\n .filter(Boolean)\n .map(normalizeOrigin),\n maxBodyBytes: parseByteSize(config.bodySizeLimit) ?? SERVER_DEFAULTS.maxBodyBytes,\n };\n}\n"]}
@@ -1,7 +1,7 @@
1
1
  import { type Route } from '../router.js';
2
2
  import { type PrerenderedPage, type PrerenderVariant } from './prerendered.js';
3
3
  /**
4
- * Resolve {@link RSHonoConfig.siteUrl} to the origin prerendering should render against.
4
+ * Resolve {@link RshonoConfig.siteUrl} to the origin prerendering should render against.
5
5
  *
6
6
  * A path is rejected rather than dropped: `'https://example.com/docs'` almost certainly means the
7
7
  * author expects a base path, and silently serving from the root would be a confusing way to find
@@ -13,7 +13,7 @@ interface PrerenderOptions {
13
13
  routes: readonly Route[];
14
14
  fetch: (request: Request) => Response | Promise<Response>;
15
15
  ssgDir: string;
16
- /** {@link RSHonoConfig.siteUrl} — the origin absolute URLs in the output are built against. */
16
+ /** {@link RshonoConfig.siteUrl} — the origin absolute URLs in the output are built against. */
17
17
  siteUrl?: string;
18
18
  }
19
19
  export interface PrerenderResult {
@@ -1 +1 @@
1
- {"version":3,"file":"ssg.d.ts","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"AAIA,OAAO,EAA+B,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAA6C,KAAK,eAAe,EAAE,KAAK,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAS1H;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAUrE;AAoCD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CAgC9I;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IACzB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAcD,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA2D/F"}
1
+ {"version":3,"file":"ssg.d.ts","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"AAGA,OAAO,EAA+B,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AACvE,OAAO,EAML,KAAK,eAAe,EACpB,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAS1B;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAUrE;AA4BD,wBAAsB,eAAe,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,GAAE,gBAAyB,GAAG,OAAO,CAAC,eAAe,GAAG,IAAI,CAAC,CA8B9I;AAED,UAAU,gBAAgB;IACxB,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IACzB,KAAK,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,QAAQ,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC1D,MAAM,EAAE,MAAM,CAAC;IACf,+FAA+F;IAC/F,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAcD,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CA2D/F"}
@@ -1,17 +1,16 @@
1
- import { createHash } from 'node:crypto';
2
1
  import { mkdirSync, writeFileSync } from 'node:fs';
3
2
  import { readFile } from 'node:fs/promises';
4
3
  import { dirname, join, resolve, sep } from 'node:path';
5
4
  import { isPageRoute } from '../router.js';
6
- import { prerenderedRelPath, ssgFilePath, VARIANTS } from './prerendered.js';
5
+ import { createPageCache, prerenderedRelPath, ssgFilePath, toPrerenderedPage, VARIANTS, } from './prerendered.js';
7
6
  /**
8
- * Stand-in origin for a build that didn't declare {@link RSHonoConfig.siteUrl}. Deliberately
7
+ * Stand-in origin for a build that didn't declare {@link RshonoConfig.siteUrl}. Deliberately
9
8
  * obviously-wrong rather than a guess: a page that bakes this into a canonical tag should be easy
10
9
  * to spot, and the build warns when static routes are prerendered without a real origin.
11
10
  */
12
11
  const DEFAULT_SSG_ORIGIN = 'http://localhost';
13
12
  /**
14
- * Resolve {@link RSHonoConfig.siteUrl} to the origin prerendering should render against.
13
+ * Resolve {@link RshonoConfig.siteUrl} to the origin prerendering should render against.
15
14
  *
16
15
  * A path is rejected rather than dropped: `'https://example.com/docs'` almost certainly means the
17
16
  * author expects a base path, and silently serving from the root would be a confusing way to find
@@ -51,16 +50,8 @@ function interpolatePath(pattern, params) {
51
50
  })
52
51
  .join('/');
53
52
  }
54
- /**
55
- * Prerendered pages, keyed by the request that produced them (see {@link readPrerendered}).
56
- *
57
- * Bounded so a site with thousands of prerendered pages keeps a working set rather than the whole
58
- * build in memory. Only *hits* are cached: caching misses would let anyone mint entries by
59
- * requesting paths that don't exist. The files are written at build time and never change while
60
- * the server is up, so an entry never needs invalidating.
61
- */
62
- const pageCache = new Map();
63
- const MAX_CACHED_PAGES = 128;
53
+ /** Prerendered pages, keyed by the request that produced them (see {@link readPrerendered}). */
54
+ const pageCache = createPageCache();
64
55
  export async function readPrerendered(ssgDir, requestPath, variant = 'html') {
65
56
  // Keyed by what the request carried, not by the resolved filename, so a hit costs one Map lookup
66
57
  // instead of re-deriving the path every time. Safe because only *hits* are cached: an entry
@@ -78,22 +69,19 @@ export async function readPrerendered(ssgDir, requestPath, variant = 'html') {
78
69
  const file = resolve(root, relPath);
79
70
  if (!file.startsWith(root + sep))
80
71
  return null;
72
+ // No encoding argument: the bytes are what gets served, and decoding them to a string here would
73
+ // only mean re-encoding them on every request that hits the cache. Copied out of the Buffer rather
74
+ // than kept as one, because `readFile` can hand back a view into Node's shared allocation pool and
75
+ // this is retained for the life of the process.
81
76
  let body;
82
77
  try {
83
- body = await readFile(file, 'utf8');
78
+ body = new Uint8Array(await readFile(file));
84
79
  }
85
80
  catch {
86
81
  return null;
87
82
  }
88
- const page = {
89
- body,
90
- contentLength: String(Buffer.byteLength(body)),
91
- etag: `W/"${createHash('sha256').update(body).digest('base64url').slice(0, 22)}"`,
92
- };
83
+ const page = await toPrerenderedPage(body);
93
84
  pageCache.set(key, page);
94
- // Insertion-ordered, so the first key is the oldest. One entry in means at most one out.
95
- if (pageCache.size > MAX_CACHED_PAGES)
96
- pageCache.delete(pageCache.keys().next().value);
97
85
  return page;
98
86
  }
99
87
  /** Renders one representation of a path, or `null` if the app didn't answer with it. */
@@ -1 +1 @@
1
- {"version":3,"file":"ssg.js","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto';\nimport { mkdirSync, writeFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, join, resolve, sep } from 'node:path';\nimport { isPageRoute, type PageRoute, type Route } from '../router.js';\nimport { prerenderedRelPath, ssgFilePath, VARIANTS, type PrerenderedPage, type PrerenderVariant } from './prerendered.js';\n\n/**\n * Stand-in origin for a build that didn't declare {@link RSHonoConfig.siteUrl}. Deliberately\n * obviously-wrong rather than a guess: a page that bakes this into a canonical tag should be easy\n * to spot, and the build warns when static routes are prerendered without a real origin.\n */\nconst DEFAULT_SSG_ORIGIN = 'http://localhost';\n\n/**\n * Resolve {@link RSHonoConfig.siteUrl} to the origin prerendering should render against.\n *\n * A path is rejected rather than dropped: `'https://example.com/docs'` almost certainly means the\n * author expects a base path, and silently serving from the root would be a confusing way to find\n * out that isn't supported.\n */\nexport function resolveSiteOrigin(siteUrl: string | undefined): string {\n if (!siteUrl) return DEFAULT_SSG_ORIGIN;\n const parsed = URL.parse(siteUrl);\n if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {\n throw new Error(`[rshono] invalid siteUrl ${JSON.stringify(siteUrl)} — use a full origin, e.g. 'https://example.com'.`);\n }\n if (parsed.pathname !== '/' || parsed.search || parsed.hash) {\n throw new Error(`[rshono] siteUrl ${JSON.stringify(siteUrl)} must be a bare origin — a base path is not supported.`);\n }\n return parsed.origin;\n}\n\nfunction interpolatePath(pattern: string, params: Record<string, string>): string {\n return pattern\n .split('/')\n .map((segment) => {\n if (!segment.startsWith(':')) {\n if (segment.includes('*')) {\n throw new Error(`Cannot prerender \"${pattern}\": wildcard segments are not supported by staticPaths.`);\n }\n return segment;\n }\n const name = segment.slice(1);\n if (!/^\\w+$/.test(name)) {\n throw new Error(`Cannot prerender \"${pattern}\": optional/regex params are not supported by staticPaths.`);\n }\n const value = params[name];\n if (value === undefined) {\n throw new Error(`staticPaths for \"${pattern}\" returned a param set without \"${name}\".`);\n }\n return encodeURIComponent(value);\n })\n .join('/');\n}\n\n/**\n * Prerendered pages, keyed by the request that produced them (see {@link readPrerendered}).\n *\n * Bounded so a site with thousands of prerendered pages keeps a working set rather than the whole\n * build in memory. Only *hits* are cached: caching misses would let anyone mint entries by\n * requesting paths that don't exist. The files are written at build time and never change while\n * the server is up, so an entry never needs invalidating.\n */\nconst pageCache = new Map<string, PrerenderedPage>();\nconst MAX_CACHED_PAGES = 128;\n\nexport async function readPrerendered(ssgDir: string, requestPath: string, variant: PrerenderVariant = 'html'): Promise<PrerenderedPage | null> {\n // Keyed by what the request carried, not by the resolved filename, so a hit costs one Map lookup\n // instead of re-deriving the path every time. Safe because only *hits* are cached: an entry\n // exists only if this exact key already passed the checks below.\n const key = `${ssgDir}\\0${variant}\\0${requestPath}`;\n const cached = pageCache.get(key);\n if (cached) return cached;\n\n const relPath = prerenderedRelPath(requestPath, variant);\n if (relPath === null) return null;\n // Belt and braces: the shared guard already refused a traversal, and this proves the resolved file\n // is under the root whatever else the path contained.\n const root = resolve(ssgDir);\n const file = resolve(root, relPath);\n if (!file.startsWith(root + sep)) return null;\n\n let body: string;\n try {\n body = await readFile(file, 'utf8');\n } catch {\n return null;\n }\n\n const page: PrerenderedPage = {\n body,\n contentLength: String(Buffer.byteLength(body)),\n etag: `W/\"${createHash('sha256').update(body).digest('base64url').slice(0, 22)}\"`,\n };\n pageCache.set(key, page);\n // Insertion-ordered, so the first key is the oldest. One entry in means at most one out.\n if (pageCache.size > MAX_CACHED_PAGES) pageCache.delete(pageCache.keys().next().value!);\n return page;\n}\n\ninterface PrerenderOptions {\n routes: readonly Route[];\n fetch: (request: Request) => Response | Promise<Response>;\n ssgDir: string;\n /** {@link RSHonoConfig.siteUrl} — the origin absolute URLs in the output are built against. */\n siteUrl?: string;\n}\n\nexport interface PrerenderResult {\n written: string[];\n skipped: string[];\n}\n\n/** Renders one representation of a path, or `null` if the app didn't answer with it. */\nasync function renderVariant(\n fetch: PrerenderOptions['fetch'],\n url: string,\n variant: PrerenderVariant,\n): Promise<{ body: string } | { status: number } | null> {\n const response = await fetch(new Request(url, { headers: { Accept: VARIANTS[variant].accept } }));\n if (response.status !== 200) return { status: response.status };\n if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) return null;\n return { body: await response.text() };\n}\n\nexport async function prerenderStaticRoutes(options: PrerenderOptions): Promise<PrerenderResult> {\n const { routes, fetch, ssgDir } = options;\n const staticRoutes = routes.filter((r): r is PageRoute => isPageRoute(r) && r.render === 'static');\n const origin = resolveSiteOrigin(options.siteUrl);\n\n if (staticRoutes.length > 0 && !options.siteUrl) {\n console.warn(\n ` ⚠ No siteUrl in rshono.config — prerendered pages are built against ${DEFAULT_SSG_ORIGIN}, so any absolute URL\\n` +\n ` they derive from a page's \\`url\\` prop (canonical tags, og:url, absolute links) will point there.`,\n );\n }\n\n const written: string[] = [];\n const skipped: string[] = [];\n\n for (const route of staticRoutes) {\n let paths: string[];\n if (!/[:*]/.test(route.path)) {\n paths = [route.path];\n } else {\n if (!route.staticPaths) {\n console.warn(` ⚠ Static route \"${route.path}\" has params but no staticPaths — will SSR per request.`);\n skipped.push(route.path);\n continue;\n }\n paths = (await route.staticPaths()).map((params) => interpolatePath(route.path, params));\n }\n\n for (const path of paths) {\n const document = await renderVariant(fetch, origin + path, 'html');\n if (document === null || !('body' in document)) {\n const rendered = document === null ? 'a non-HTML response' : `${document.status}`;\n console.warn(` ⚠ \"${path}\" rendered ${rendered} at build time — skipping, will SSR per request.`);\n skipped.push(path);\n continue;\n }\n\n const write = (variant: PrerenderVariant, body: string) => {\n const file = join(ssgDir, ssgFilePath(path, variant)!);\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(file, body);\n };\n write('html', document.body);\n\n // The soft-navigation representation of the same page. Best-effort: if it doesn't come back\n // cleanly the document is still valid on its own, and serving falls back to rendering flight\n // per request — the behaviour before this was written at all.\n const flight = await renderVariant(fetch, origin + path, 'flight');\n if (flight !== null && 'body' in flight) {\n write('flight', flight.body);\n } else {\n console.warn(` ⚠ \"${path}\" produced no flight payload — soft navigations to it will render per request.`);\n }\n\n written.push(path);\n }\n }\n\n return { written, skipped };\n}\n"]}
1
+ {"version":3,"file":"ssg.js","sourceRoot":"","sources":["../../src/server/ssg.ts"],"names":[],"mappings":"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","sourcesContent":["import { mkdirSync, writeFileSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, join, resolve, sep } from 'node:path';\nimport { isPageRoute, type PageRoute, type Route } from '../router.js';\nimport {\n createPageCache,\n prerenderedRelPath,\n ssgFilePath,\n toPrerenderedPage,\n VARIANTS,\n type PrerenderedPage,\n type PrerenderVariant,\n} from './prerendered.js';\n\n/**\n * Stand-in origin for a build that didn't declare {@link RshonoConfig.siteUrl}. Deliberately\n * obviously-wrong rather than a guess: a page that bakes this into a canonical tag should be easy\n * to spot, and the build warns when static routes are prerendered without a real origin.\n */\nconst DEFAULT_SSG_ORIGIN = 'http://localhost';\n\n/**\n * Resolve {@link RshonoConfig.siteUrl} to the origin prerendering should render against.\n *\n * A path is rejected rather than dropped: `'https://example.com/docs'` almost certainly means the\n * author expects a base path, and silently serving from the root would be a confusing way to find\n * out that isn't supported.\n */\nexport function resolveSiteOrigin(siteUrl: string | undefined): string {\n if (!siteUrl) return DEFAULT_SSG_ORIGIN;\n const parsed = URL.parse(siteUrl);\n if (!parsed || (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')) {\n throw new Error(`[rshono] invalid siteUrl ${JSON.stringify(siteUrl)} — use a full origin, e.g. 'https://example.com'.`);\n }\n if (parsed.pathname !== '/' || parsed.search || parsed.hash) {\n throw new Error(`[rshono] siteUrl ${JSON.stringify(siteUrl)} must be a bare origin — a base path is not supported.`);\n }\n return parsed.origin;\n}\n\nfunction interpolatePath(pattern: string, params: Record<string, string>): string {\n return pattern\n .split('/')\n .map((segment) => {\n if (!segment.startsWith(':')) {\n if (segment.includes('*')) {\n throw new Error(`Cannot prerender \"${pattern}\": wildcard segments are not supported by staticPaths.`);\n }\n return segment;\n }\n const name = segment.slice(1);\n if (!/^\\w+$/.test(name)) {\n throw new Error(`Cannot prerender \"${pattern}\": optional/regex params are not supported by staticPaths.`);\n }\n const value = params[name];\n if (value === undefined) {\n throw new Error(`staticPaths for \"${pattern}\" returned a param set without \"${name}\".`);\n }\n return encodeURIComponent(value);\n })\n .join('/');\n}\n\n/** Prerendered pages, keyed by the request that produced them (see {@link readPrerendered}). */\nconst pageCache = createPageCache();\n\nexport async function readPrerendered(ssgDir: string, requestPath: string, variant: PrerenderVariant = 'html'): Promise<PrerenderedPage | null> {\n // Keyed by what the request carried, not by the resolved filename, so a hit costs one Map lookup\n // instead of re-deriving the path every time. Safe because only *hits* are cached: an entry\n // exists only if this exact key already passed the checks below.\n const key = `${ssgDir}\\0${variant}\\0${requestPath}`;\n const cached = pageCache.get(key);\n if (cached) return cached;\n\n const relPath = prerenderedRelPath(requestPath, variant);\n if (relPath === null) return null;\n // Belt and braces: the shared guard already refused a traversal, and this proves the resolved file\n // is under the root whatever else the path contained.\n const root = resolve(ssgDir);\n const file = resolve(root, relPath);\n if (!file.startsWith(root + sep)) return null;\n\n // No encoding argument: the bytes are what gets served, and decoding them to a string here would\n // only mean re-encoding them on every request that hits the cache. Copied out of the Buffer rather\n // than kept as one, because `readFile` can hand back a view into Node's shared allocation pool and\n // this is retained for the life of the process.\n let body: Uint8Array<ArrayBuffer>;\n try {\n body = new Uint8Array(await readFile(file));\n } catch {\n return null;\n }\n\n const page = await toPrerenderedPage(body);\n pageCache.set(key, page);\n return page;\n}\n\ninterface PrerenderOptions {\n routes: readonly Route[];\n fetch: (request: Request) => Response | Promise<Response>;\n ssgDir: string;\n /** {@link RshonoConfig.siteUrl} — the origin absolute URLs in the output are built against. */\n siteUrl?: string;\n}\n\nexport interface PrerenderResult {\n written: string[];\n skipped: string[];\n}\n\n/** Renders one representation of a path, or `null` if the app didn't answer with it. */\nasync function renderVariant(\n fetch: PrerenderOptions['fetch'],\n url: string,\n variant: PrerenderVariant,\n): Promise<{ body: string } | { status: number } | null> {\n const response = await fetch(new Request(url, { headers: { Accept: VARIANTS[variant].accept } }));\n if (response.status !== 200) return { status: response.status };\n if (!(response.headers.get('Content-Type') ?? '').includes(VARIANTS[variant].contentType)) return null;\n return { body: await response.text() };\n}\n\nexport async function prerenderStaticRoutes(options: PrerenderOptions): Promise<PrerenderResult> {\n const { routes, fetch, ssgDir } = options;\n const staticRoutes = routes.filter((r): r is PageRoute => isPageRoute(r) && r.render === 'static');\n const origin = resolveSiteOrigin(options.siteUrl);\n\n if (staticRoutes.length > 0 && !options.siteUrl) {\n console.warn(\n ` ⚠ No siteUrl in rshono.config — prerendered pages are built against ${DEFAULT_SSG_ORIGIN}, so any absolute URL\\n` +\n ` they derive from a page's \\`url\\` prop (canonical tags, og:url, absolute links) will point there.`,\n );\n }\n\n const written: string[] = [];\n const skipped: string[] = [];\n\n for (const route of staticRoutes) {\n let paths: string[];\n if (!/[:*]/.test(route.path)) {\n paths = [route.path];\n } else {\n if (!route.staticPaths) {\n console.warn(` ⚠ Static route \"${route.path}\" has params but no staticPaths — will SSR per request.`);\n skipped.push(route.path);\n continue;\n }\n paths = (await route.staticPaths()).map((params) => interpolatePath(route.path, params));\n }\n\n for (const path of paths) {\n const document = await renderVariant(fetch, origin + path, 'html');\n if (document === null || !('body' in document)) {\n const rendered = document === null ? 'a non-HTML response' : `${document.status}`;\n console.warn(` ⚠ \"${path}\" rendered ${rendered} at build time — skipping, will SSR per request.`);\n skipped.push(path);\n continue;\n }\n\n const write = (variant: PrerenderVariant, body: string) => {\n const file = join(ssgDir, ssgFilePath(path, variant)!);\n mkdirSync(dirname(file), { recursive: true });\n writeFileSync(file, body);\n };\n write('html', document.body);\n\n // The soft-navigation representation of the same page. Best-effort: if it doesn't come back\n // cleanly the document is still valid on its own, and serving falls back to rendering flight\n // per request — the behaviour before this was written at all.\n const flight = await renderVariant(fetch, origin + path, 'flight');\n if (flight !== null && 'body' in flight) {\n write('flight', flight.body);\n } else {\n console.warn(` ⚠ \"${path}\" produced no flight payload — soft navigations to it will render per request.`);\n }\n\n written.push(path);\n }\n }\n\n return { written, skipped };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rshono/core",
3
- "version": "1.0.0-rc.2",
3
+ "version": "1.0.0-rc.4",
4
4
  "description": "Minimalist web framework — Hono + Rspack + React Server Components",
5
5
  "author": "Lasse <lasse@lassetange.com> (https://www.lassetange.com)",
6
6
  "license": "ISC",
@@ -45,28 +45,17 @@
45
45
  "bin"
46
46
  ],
47
47
  "engines": {
48
- "node": ">=22.1.0"
48
+ "node": ">=22.18.0"
49
49
  },
50
50
  "publishConfig": {
51
51
  "access": "public"
52
52
  },
53
- "scripts": {
54
- "build": "tsc -p tsconfig.build.json && node scripts/copy-dist-assets.mjs",
55
- "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
56
- "dev": "tsc -p tsconfig.build.json --watch",
57
- "typecheck": "tsc --noEmit",
58
- "test": "npm run build && node --test --test-concurrency=1 \"test/*.test.mjs\"",
59
- "test:browser": "npm run build && playwright test",
60
- "prepack": "npm run build"
61
- },
62
53
  "dependencies": {
63
54
  "@hono/node-server": "^2.0.11",
64
- "@rspack/core": "2.1.5",
55
+ "@rspack/core": "2.1.7",
65
56
  "@rspack/plugin-react-refresh": "2.0.2",
66
57
  "react-refresh": "0.18.0",
67
- "react-server-dom-rspack": "0.0.2",
68
- "rsc-html-stream": "^0.0.7",
69
- "tsx": "^4.23.1"
58
+ "react-server-dom-rspack": "0.0.3"
70
59
  },
71
60
  "devDependencies": {
72
61
  "@playwright/test": "^1.58.0",
@@ -82,5 +71,13 @@
82
71
  "hono": "^4.8.0",
83
72
  "react": "^19.1.0",
84
73
  "react-dom": "^19.1.0"
74
+ },
75
+ "scripts": {
76
+ "build": "tsc -p tsconfig.build.json && node scripts/copy-dist-assets.mjs",
77
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
78
+ "dev": "tsc -p tsconfig.build.json --watch",
79
+ "typecheck": "tsc --noEmit",
80
+ "test": "npm run build && node --test --test-concurrency=1 \"test/*.test.mjs\"",
81
+ "test:browser": "npm run build && playwright test"
85
82
  }
86
- }
83
+ }
@@ -1,11 +0,0 @@
1
- import type { DeployRuntime } from '../contract.js';
2
- /**
3
- * Bun: a long-lived process like Node, but the runtime — not the app — opens the socket. Bun reads
4
- * `fetch`, `port` and `hostname` off the module's default export and serves it, so there is nothing to
5
- * bind here and no server handle to close on shutdown.
6
- *
7
- * Everything else is Node's: Bun implements the `node:` APIs `server/` uses, so assets, gzip and
8
- * `.env` loading are the same code.
9
- */
10
- export declare const runtime: DeployRuntime;
11
- //# sourceMappingURL=runtime.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/deploy/bun/runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAIpD;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,EAAE,aAUrB,CAAC"}
@@ -1,22 +0,0 @@
1
- import { fileSystemRuntime } from '../filesystem.js';
2
- import { listenAddress, readyMessage } from '../listen.js';
3
- /**
4
- * Bun: a long-lived process like Node, but the runtime — not the app — opens the socket. Bun reads
5
- * `fetch`, `port` and `hostname` off the module's default export and serves it, so there is nothing to
6
- * bind here and no server handle to close on shutdown.
7
- *
8
- * Everything else is Node's: Bun implements the `node:` APIs `server/` uses, so assets, gzip and
9
- * `.env` loading are the same code.
10
- */
11
- export const runtime = {
12
- ...fileSystemRuntime,
13
- serveApp(app) {
14
- const address = listenAddress();
15
- // The prerender pass imports this bundle; returning the export is inert either way, so unlike the
16
- // Node preset there is nothing to suppress. Only the log line is worth skipping.
17
- if (!process.env.RSHONO_PRERENDER)
18
- console.log(readyMessage(address));
19
- return { fetch: app.fetch, ...address };
20
- },
21
- };
22
- //# sourceMappingURL=runtime.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/deploy/bun/runtime.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE3D;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,OAAO,GAAkB;IACpC,GAAG,iBAAiB;IAEpB,QAAQ,CAAC,GAAS;QAChB,MAAM,OAAO,GAAG,aAAa,EAAE,CAAC;QAChC,kGAAkG;QAClG,iFAAiF;QACjF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,gBAAgB;YAAE,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;QACtE,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,GAAG,OAAO,EAAE,CAAC;IAC1C,CAAC;CACF,CAAC","sourcesContent":["import type { Hono } from 'hono';\nimport type { DeployRuntime } from '../contract.js';\nimport { fileSystemRuntime } from '../filesystem.js';\nimport { listenAddress, readyMessage } from '../listen.js';\n\n/**\n * Bun: a long-lived process like Node, but the runtime — not the app — opens the socket. Bun reads\n * `fetch`, `port` and `hostname` off the module's default export and serves it, so there is nothing to\n * bind here and no server handle to close on shutdown.\n *\n * Everything else is Node's: Bun implements the `node:` APIs `server/` uses, so assets, gzip and\n * `.env` loading are the same code.\n */\nexport const runtime: DeployRuntime = {\n ...fileSystemRuntime,\n\n serveApp(app: Hono): unknown {\n const address = listenAddress();\n // The prerender pass imports this bundle; returning the export is inert either way, so unlike the\n // Node preset there is nothing to suppress. Only the log line is worth skipping.\n if (!process.env.RSHONO_PRERENDER) console.log(readyMessage(address));\n return { fetch: app.fetch, ...address };\n },\n};\n"]}
@@ -1,11 +0,0 @@
1
- import type { DeployRuntime } from '../contract.js';
2
- /**
3
- * Deno: `deno serve` and Deno Deploy both look for a `fetch` on the default export, so the handoff is
4
- * an export rather than a listener — which is also what makes one build work for both.
5
- *
6
- * The compiler settings stay Node's deliberately. Deno's `node:` compatibility covers everything
7
- * `server/` uses, so the Node builds of React and the RSC runtime run as-is, and the alternative would
8
- * be a second bundle shape to keep working for no behavioural gain.
9
- */
10
- export declare const runtime: DeployRuntime;
11
- //# sourceMappingURL=runtime.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/deploy/deno/runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD;;;;;;;GAOG;AACH,eAAO,MAAM,OAAO,EAAE,aAMrB,CAAC"}
@@ -1,16 +0,0 @@
1
- import { fileSystemRuntime } from '../filesystem.js';
2
- /**
3
- * Deno: `deno serve` and Deno Deploy both look for a `fetch` on the default export, so the handoff is
4
- * an export rather than a listener — which is also what makes one build work for both.
5
- *
6
- * The compiler settings stay Node's deliberately. Deno's `node:` compatibility covers everything
7
- * `server/` uses, so the Node builds of React and the RSC runtime run as-is, and the alternative would
8
- * be a second bundle shape to keep working for no behavioural gain.
9
- */
10
- export const runtime = {
11
- ...fileSystemRuntime,
12
- serveApp(app) {
13
- return { fetch: app.fetch };
14
- },
15
- };
16
- //# sourceMappingURL=runtime.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/deploy/deno/runtime.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,OAAO,GAAkB;IACpC,GAAG,iBAAiB;IAEpB,QAAQ,CAAC,GAAS;QAChB,OAAO,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC;IAC9B,CAAC;CACF,CAAC","sourcesContent":["import type { Hono } from 'hono';\nimport type { DeployRuntime } from '../contract.js';\nimport { fileSystemRuntime } from '../filesystem.js';\n\n/**\n * Deno: `deno serve` and Deno Deploy both look for a `fetch` on the default export, so the handoff is\n * an export rather than a listener — which is also what makes one build work for both.\n *\n * The compiler settings stay Node's deliberately. Deno's `node:` compatibility covers everything\n * `server/` uses, so the Node builds of React and the RSC runtime run as-is, and the alternative would\n * be a second bundle shape to keep working for no behavioural gain.\n */\nexport const runtime: DeployRuntime = {\n ...fileSystemRuntime,\n\n serveApp(app: Hono): unknown {\n return { fetch: app.fetch };\n },\n};\n"]}
@@ -1,20 +0,0 @@
1
- /** What to bind, for the targets where rshono owns the process rather than being handed a request. */
2
- export interface ListenAddress {
3
- port: number;
4
- hostname: string;
5
- }
6
- /**
7
- * Resolves the address to listen on: an explicit override (the dev server, which picks the port for
8
- * its worker) beats the environment, which beats `rshono.config.ts`, which beats the built-in default.
9
- *
10
- * `PORT` and `HOST` stay env-overridable because that is the deployment convention everywhere this
11
- * runs — a container, a process manager, a PaaS. `??` rather than `||` so an explicit `PORT=0`, which
12
- * means "any free port", is honoured.
13
- */
14
- export declare function listenAddress(overrides?: {
15
- port?: number;
16
- hostname?: string;
17
- }): ListenAddress;
18
- /** What to print once listening — `localhost` rather than the wildcard the socket is actually bound to. */
19
- export declare function readyMessage({ port, hostname }: ListenAddress): string;
20
- //# sourceMappingURL=listen.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"listen.d.ts","sourceRoot":"","sources":["../../src/deploy/listen.ts"],"names":[],"mappings":"AAOA,sGAAsG;AACtG,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,SAAS,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAM7F;AAED,2GAA2G;AAC3G,wBAAgB,YAAY,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,aAAa,GAAG,MAAM,CAEtE"}
@@ -1,24 +0,0 @@
1
- import { SERVER_DEFAULTS } from '../server/server-config.js';
2
- const CONFIG = __RSHONO_CONFIG__;
3
- /** Bound to every interface — so the address printed on start is `localhost`, not this. */
4
- const WILDCARD_HOST = '0.0.0.0';
5
- /**
6
- * Resolves the address to listen on: an explicit override (the dev server, which picks the port for
7
- * its worker) beats the environment, which beats `rshono.config.ts`, which beats the built-in default.
8
- *
9
- * `PORT` and `HOST` stay env-overridable because that is the deployment convention everywhere this
10
- * runs — a container, a process manager, a PaaS. `??` rather than `||` so an explicit `PORT=0`, which
11
- * means "any free port", is honoured.
12
- */
13
- export function listenAddress(overrides) {
14
- const envPort = process.env.PORT !== undefined ? Number(process.env.PORT) : undefined;
15
- return {
16
- port: overrides?.port ?? envPort ?? CONFIG.port ?? SERVER_DEFAULTS.port,
17
- hostname: overrides?.hostname ?? process.env.HOST ?? CONFIG.host ?? SERVER_DEFAULTS.host,
18
- };
19
- }
20
- /** What to print once listening — `localhost` rather than the wildcard the socket is actually bound to. */
21
- export function readyMessage({ port, hostname }) {
22
- return ` ➜ rshono serving on http://${hostname === WILDCARD_HOST ? 'localhost' : hostname}:${port}`;
23
- }
24
- //# sourceMappingURL=listen.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"listen.js","sourceRoot":"","sources":["../../src/deploy/listen.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAE7D,MAAM,MAAM,GAAG,iBAAiB,CAAC;AAEjC,2FAA2F;AAC3F,MAAM,aAAa,GAAG,SAAS,CAAC;AAQhC;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,SAAgD;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACtF,OAAO;QACL,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,OAAO,IAAI,MAAM,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI;QACvE,QAAQ,EAAE,SAAS,EAAE,QAAQ,IAAI,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,CAAC,IAAI,IAAI,eAAe,CAAC,IAAI;KACzF,CAAC;AACJ,CAAC;AAED,2GAA2G;AAC3G,MAAM,UAAU,YAAY,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAiB;IAC5D,OAAO,gCAAgC,QAAQ,KAAK,aAAa,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,QAAQ,IAAI,IAAI,EAAE,CAAC;AACvG,CAAC","sourcesContent":["import { SERVER_DEFAULTS } from '../server/server-config.js';\n\nconst CONFIG = __RSHONO_CONFIG__;\n\n/** Bound to every interface — so the address printed on start is `localhost`, not this. */\nconst WILDCARD_HOST = '0.0.0.0';\n\n/** What to bind, for the targets where rshono owns the process rather than being handed a request. */\nexport interface ListenAddress {\n port: number;\n hostname: string;\n}\n\n/**\n * Resolves the address to listen on: an explicit override (the dev server, which picks the port for\n * its worker) beats the environment, which beats `rshono.config.ts`, which beats the built-in default.\n *\n * `PORT` and `HOST` stay env-overridable because that is the deployment convention everywhere this\n * runs — a container, a process manager, a PaaS. `??` rather than `||` so an explicit `PORT=0`, which\n * means \"any free port\", is honoured.\n */\nexport function listenAddress(overrides?: { port?: number; hostname?: string }): ListenAddress {\n const envPort = process.env.PORT !== undefined ? Number(process.env.PORT) : undefined;\n return {\n port: overrides?.port ?? envPort ?? CONFIG.port ?? SERVER_DEFAULTS.port,\n hostname: overrides?.hostname ?? process.env.HOST ?? CONFIG.host ?? SERVER_DEFAULTS.host,\n };\n}\n\n/** What to print once listening — `localhost` rather than the wildcard the socket is actually bound to. */\nexport function readyMessage({ port, hostname }: ListenAddress): string {\n return ` ➜ rshono serving on http://${hostname === WILDCARD_HOST ? 'localhost' : hostname}:${port}`;\n}\n"]}
@@ -1,8 +0,0 @@
1
- import type { DeployBuildContext } from '../presets.js';
2
- /**
3
- * `dist/server/main.mjs` is kept at that exact path inside the function bundle because the runtime
4
- * derives the project root from where it sits (see `deploy/filesystem.ts`), which is what puts
5
- * `dist/ssg` within reach at request time.
6
- */
7
- export declare function finalizeNetlifyBuild(ctx: DeployBuildContext): Promise<void>;
8
- //# sourceMappingURL=build.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"build.d.ts","sourceRoot":"","sources":["../../../src/deploy/netlify/build.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AA0BxD;;;;GAIG;AACH,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBjF"}
@@ -1,52 +0,0 @@
1
- import { cpSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
- import { rm } from 'node:fs/promises';
3
- import { join } from 'node:path';
4
- /**
5
- * What Netlify uploads: a publish directory the CDN serves, and an internal function directory the
6
- * platform picks handlers up from without the project needing a `netlify/functions` folder of its own.
7
- */
8
- const PUBLISH_DIR = join('.netlify', 'publish');
9
- const FUNCTIONS_DIR = join('.netlify', 'functions-internal');
10
- const FUNCTION_NAME = 'rshono-server';
11
- /**
12
- * The function entry Netlify loads. It re-exports the built bundle's default handler and, next to it,
13
- * the v2 `config` that claims every path — so routing is declared in code rather than in a redirect
14
- * rule the project would have to maintain.
15
- *
16
- * `preferStatic` is what keeps the CDN in front: a path that exists in the publish directory is served
17
- * from there and the function is never invoked.
18
- */
19
- const FUNCTION_ENTRY = `export { default } from '../../dist/server/main.mjs';
20
-
21
- export const config = {
22
- path: '/*',
23
- preferStatic: true,
24
- };
25
- `;
26
- /**
27
- * `dist/server/main.mjs` is kept at that exact path inside the function bundle because the runtime
28
- * derives the project root from where it sits (see `deploy/filesystem.ts`), which is what puts
29
- * `dist/ssg` within reach at request time.
30
- */
31
- export async function finalizeNetlifyBuild(ctx) {
32
- const publishDir = join(ctx.rootDir, PUBLISH_DIR);
33
- const functionsDir = join(ctx.rootDir, FUNCTIONS_DIR);
34
- // Rebuilt from scratch, so a deleted page or asset cannot survive in the uploaded output.
35
- await rm(publishDir, { recursive: true, force: true });
36
- await rm(functionsDir, { recursive: true, force: true });
37
- mkdirSync(publishDir, { recursive: true });
38
- mkdirSync(functionsDir, { recursive: true });
39
- cpSync(ctx.staticDir, join(publishDir, '_static'), { recursive: true });
40
- if (ctx.publicDir)
41
- cpSync(ctx.publicDir, publishDir, { recursive: true });
42
- // The one header the CDN cannot infer; everything else keeps Netlify's defaults.
43
- writeFileSync(join(publishDir, '_headers'), '/_static/*\n Cache-Control: public, max-age=31536000, immutable\n');
44
- cpSync(join(ctx.distDir, 'server'), join(functionsDir, 'dist', 'server'), { recursive: true });
45
- if (existsSync(ctx.ssgDir))
46
- cpSync(ctx.ssgDir, join(functionsDir, 'dist', 'ssg'), { recursive: true });
47
- if (ctx.publicDir)
48
- cpSync(ctx.publicDir, join(functionsDir, 'dist', 'public'), { recursive: true });
49
- writeFileSync(join(functionsDir, `${FUNCTION_NAME}.mjs`), FUNCTION_ENTRY);
50
- console.log(` • assembled ${PUBLISH_DIR} and ${FUNCTIONS_DIR}`);
51
- }
52
- //# sourceMappingURL=build.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"build.js","sourceRoot":"","sources":["../../../src/deploy/netlify/build.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACvE,OAAO,EAAE,EAAE,EAAE,MAAM,kBAAkB,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAGjC;;;GAGG;AACH,MAAM,WAAW,GAAG,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;AAChD,MAAM,aAAa,GAAG,IAAI,CAAC,UAAU,EAAE,oBAAoB,CAAC,CAAC;AAC7D,MAAM,aAAa,GAAG,eAAe,CAAC;AAEtC;;;;;;;GAOG;AACH,MAAM,cAAc,GAAG;;;;;;CAMtB,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,GAAuB;IAChE,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC;IAClD,MAAM,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC;IAEtD,0FAA0F;IAC1F,MAAM,EAAE,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACvD,MAAM,EAAE,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC;IACzD,SAAS,CAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC3C,SAAS,CAAC,YAAY,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE7C,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACxE,IAAI,GAAG,CAAC,SAAS;QAAE,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC1E,iFAAiF;IACjF,aAAa,CAAC,IAAI,CAAC,UAAU,EAAE,UAAU,CAAC,EAAE,oEAAoE,CAAC,CAAC;IAElH,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,QAAQ,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC/F,IAAI,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC;QAAE,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACvG,IAAI,GAAG,CAAC,SAAS;QAAE,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACpG,aAAa,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,aAAa,MAAM,CAAC,EAAE,cAAc,CAAC,CAAC;IAE1E,OAAO,CAAC,GAAG,CAAC,iBAAiB,WAAW,QAAQ,aAAa,EAAE,CAAC,CAAC;AACnE,CAAC","sourcesContent":["import { cpSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';\nimport { rm } from 'node:fs/promises';\nimport { join } from 'node:path';\nimport type { DeployBuildContext } from '../presets.js';\n\n/**\n * What Netlify uploads: a publish directory the CDN serves, and an internal function directory the\n * platform picks handlers up from without the project needing a `netlify/functions` folder of its own.\n */\nconst PUBLISH_DIR = join('.netlify', 'publish');\nconst FUNCTIONS_DIR = join('.netlify', 'functions-internal');\nconst FUNCTION_NAME = 'rshono-server';\n\n/**\n * The function entry Netlify loads. It re-exports the built bundle's default handler and, next to it,\n * the v2 `config` that claims every path — so routing is declared in code rather than in a redirect\n * rule the project would have to maintain.\n *\n * `preferStatic` is what keeps the CDN in front: a path that exists in the publish directory is served\n * from there and the function is never invoked.\n */\nconst FUNCTION_ENTRY = `export { default } from '../../dist/server/main.mjs';\n\nexport const config = {\n path: '/*',\n preferStatic: true,\n};\n`;\n\n/**\n * `dist/server/main.mjs` is kept at that exact path inside the function bundle because the runtime\n * derives the project root from where it sits (see `deploy/filesystem.ts`), which is what puts\n * `dist/ssg` within reach at request time.\n */\nexport async function finalizeNetlifyBuild(ctx: DeployBuildContext): Promise<void> {\n const publishDir = join(ctx.rootDir, PUBLISH_DIR);\n const functionsDir = join(ctx.rootDir, FUNCTIONS_DIR);\n\n // Rebuilt from scratch, so a deleted page or asset cannot survive in the uploaded output.\n await rm(publishDir, { recursive: true, force: true });\n await rm(functionsDir, { recursive: true, force: true });\n mkdirSync(publishDir, { recursive: true });\n mkdirSync(functionsDir, { recursive: true });\n\n cpSync(ctx.staticDir, join(publishDir, '_static'), { recursive: true });\n if (ctx.publicDir) cpSync(ctx.publicDir, publishDir, { recursive: true });\n // The one header the CDN cannot infer; everything else keeps Netlify's defaults.\n writeFileSync(join(publishDir, '_headers'), '/_static/*\\n Cache-Control: public, max-age=31536000, immutable\\n');\n\n cpSync(join(ctx.distDir, 'server'), join(functionsDir, 'dist', 'server'), { recursive: true });\n if (existsSync(ctx.ssgDir)) cpSync(ctx.ssgDir, join(functionsDir, 'dist', 'ssg'), { recursive: true });\n if (ctx.publicDir) cpSync(ctx.publicDir, join(functionsDir, 'dist', 'public'), { recursive: true });\n writeFileSync(join(functionsDir, `${FUNCTION_NAME}.mjs`), FUNCTION_ENTRY);\n\n console.log(` • assembled ${PUBLISH_DIR} and ${FUNCTIONS_DIR}`);\n}\n"]}
@@ -1,13 +0,0 @@
1
- import type { DeployRuntime } from '../contract.js';
2
- /**
3
- * Netlify, as one Functions v2 handler behind the CDN.
4
- *
5
- * Same division as Vercel: the publish directory holds `/_static` and `public/`, so the CDN answers
6
- * those and the function is only reached for a page. Prerendered pages ship inside the function and are
7
- * read from disk, because serving them from the CDN would lose the `Accept` negotiation that decides
8
- * between a document and a flight payload.
9
- *
10
- * Functions v2 streams a `Response` body, so streamed SSR survives.
11
- */
12
- export declare const runtime: DeployRuntime;
13
- //# sourceMappingURL=runtime.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../../src/deploy/netlify/runtime.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAGpD;;;;;;;;;GASG;AACH,eAAO,MAAM,OAAO,EAAE,aAarB,CAAC"}
@@ -1,24 +0,0 @@
1
- import { handle } from 'hono/netlify';
2
- import { fileSystemRuntime } from '../filesystem.js';
3
- /**
4
- * Netlify, as one Functions v2 handler behind the CDN.
5
- *
6
- * Same division as Vercel: the publish directory holds `/_static` and `public/`, so the CDN answers
7
- * those and the function is only reached for a page. Prerendered pages ship inside the function and are
8
- * read from disk, because serving them from the CDN would lose the `Accept` negotiation that decides
9
- * between a document and a flight payload.
10
- *
11
- * Functions v2 streams a `Response` body, so streamed SSR survives.
12
- */
13
- export const runtime = {
14
- ...fileSystemRuntime,
15
- mountStaticAssets() {
16
- // Served from the publish directory before the function runs.
17
- },
18
- serveApp(app) {
19
- return handle(app);
20
- },
21
- // Netlify compresses at the edge.
22
- compress: null,
23
- };
24
- //# sourceMappingURL=runtime.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/deploy/netlify/runtime.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AAEtC,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,OAAO,GAAkB;IACpC,GAAG,iBAAiB;IAEpB,iBAAiB;QACf,8DAA8D;IAChE,CAAC;IAED,QAAQ,CAAC,GAAS;QAChB,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;IACrB,CAAC;IAED,kCAAkC;IAClC,QAAQ,EAAE,IAAI;CACf,CAAC","sourcesContent":["import type { Hono } from 'hono';\nimport { handle } from 'hono/netlify';\nimport type { DeployRuntime } from '../contract.js';\nimport { fileSystemRuntime } from '../filesystem.js';\n\n/**\n * Netlify, as one Functions v2 handler behind the CDN.\n *\n * Same division as Vercel: the publish directory holds `/_static` and `public/`, so the CDN answers\n * those and the function is only reached for a page. Prerendered pages ship inside the function and are\n * read from disk, because serving them from the CDN would lose the `Accept` negotiation that decides\n * between a document and a flight payload.\n *\n * Functions v2 streams a `Response` body, so streamed SSR survives.\n */\nexport const runtime: DeployRuntime = {\n ...fileSystemRuntime,\n\n mountStaticAssets(): void {\n // Served from the publish directory before the function runs.\n },\n\n serveApp(app: Hono): unknown {\n return handle(app);\n },\n\n // Netlify compresses at the edge.\n compress: null,\n};\n"]}