@rshono/core 1.0.0-rc.19 → 1.0.0-rc.20

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 (151) hide show
  1. package/README.md +93 -2
  2. package/dist/builder/env-shadow-loader.cjs +41 -1
  3. package/dist/builder/page-entry-loader.cjs +28 -1
  4. package/dist/builder/page-files.d.ts +0 -1
  5. package/dist/builder/public-env.d.ts +0 -1
  6. package/dist/builder/react-versions.d.ts +0 -1
  7. package/dist/builder/rspack-config.d.ts +0 -1
  8. package/dist/builder/rspack-config.js +4 -0
  9. package/dist/builder/rspack-config.js.map +1 -1
  10. package/dist/builder/server-only-imports.d.ts +0 -1
  11. package/dist/cli/build.d.ts +0 -1
  12. package/dist/cli/build.js +37 -35
  13. package/dist/cli/build.js.map +1 -1
  14. package/dist/cli/dev.d.ts +0 -1
  15. package/dist/cli/dev.js +15 -1
  16. package/dist/cli/dev.js.map +1 -1
  17. package/dist/cli/exit.d.ts +12 -0
  18. package/dist/cli/exit.js +16 -0
  19. package/dist/cli/exit.js.map +1 -0
  20. package/dist/cli/index.d.ts +0 -1
  21. package/dist/cli/index.js +74 -20
  22. package/dist/cli/index.js.map +1 -1
  23. package/dist/cli/start.d.ts +0 -1
  24. package/dist/cli/start.js +3 -2
  25. package/dist/cli/start.js.map +1 -1
  26. package/dist/config.d.ts +0 -1
  27. package/dist/deploy/aws-lambda/runtime.d.ts +0 -1
  28. package/dist/deploy/build-marker.d.ts +7 -2
  29. package/dist/deploy/build-marker.js +6 -0
  30. package/dist/deploy/build-marker.js.map +1 -1
  31. package/dist/deploy/cloudflare/build.d.ts +0 -1
  32. package/dist/deploy/cloudflare/build.js +6 -0
  33. package/dist/deploy/cloudflare/build.js.map +1 -1
  34. package/dist/deploy/cloudflare/runtime.d.ts +0 -1
  35. package/dist/deploy/cloudflare/runtime.js +24 -2
  36. package/dist/deploy/cloudflare/runtime.js.map +1 -1
  37. package/dist/deploy/contract.d.ts +20 -2
  38. package/dist/deploy/contract.js.map +1 -1
  39. package/dist/deploy/filesystem.d.ts +0 -1
  40. package/dist/deploy/node/runtime.d.ts +0 -1
  41. package/dist/deploy/presets.d.ts +7 -1
  42. package/dist/deploy/presets.js +13 -2
  43. package/dist/deploy/presets.js.map +1 -1
  44. package/dist/deploy/public-paths.d.ts +42 -0
  45. package/dist/deploy/public-paths.js +66 -0
  46. package/dist/deploy/public-paths.js.map +1 -0
  47. package/dist/deploy/vercel/build.d.ts +0 -1
  48. package/dist/deploy/vercel/build.js +6 -0
  49. package/dist/deploy/vercel/build.js.map +1 -1
  50. package/dist/deploy/vercel/runtime.d.ts +0 -1
  51. package/dist/index.d.ts +0 -1
  52. package/dist/router.d.ts +20 -5
  53. package/dist/router.js.map +1 -1
  54. package/dist/runtime/boundaries.d.ts +0 -1
  55. package/dist/runtime/client.d.ts +0 -1
  56. package/dist/runtime/context.d.ts +15 -2
  57. package/dist/runtime/context.js +15 -1
  58. package/dist/runtime/context.js.map +1 -1
  59. package/dist/runtime/control.d.ts +15 -1
  60. package/dist/runtime/control.js +17 -0
  61. package/dist/runtime/control.js.map +1 -1
  62. package/dist/runtime/dev-protocol.d.ts +0 -1
  63. package/dist/runtime/empty-server-app.d.ts +0 -1
  64. package/dist/runtime/entry.client.d.ts +0 -1
  65. package/dist/runtime/entry.client.js +31 -4
  66. package/dist/runtime/entry.client.js.map +1 -1
  67. package/dist/runtime/entry.rsc.d.ts +6 -4
  68. package/dist/runtime/entry.rsc.js +473 -56
  69. package/dist/runtime/entry.rsc.js.map +1 -1
  70. package/dist/runtime/entry.ssr.d.ts +19 -5
  71. package/dist/runtime/entry.ssr.js +22 -47
  72. package/dist/runtime/entry.ssr.js.map +1 -1
  73. package/dist/runtime/failure-document.d.ts +11 -0
  74. package/dist/runtime/failure-document.js +35 -0
  75. package/dist/runtime/failure-document.js.map +1 -0
  76. package/dist/runtime/flight-inject.d.ts +0 -1
  77. package/dist/runtime/flight-inject.js +15 -9
  78. package/dist/runtime/flight-inject.js.map +1 -1
  79. package/dist/runtime/hot-update.d.ts +0 -1
  80. package/dist/runtime/navigation.d.ts +6 -1
  81. package/dist/runtime/navigation.js +6 -0
  82. package/dist/runtime/navigation.js.map +1 -1
  83. package/dist/runtime/request.d.ts +23 -1
  84. package/dist/runtime/request.js +39 -0
  85. package/dist/runtime/request.js.map +1 -1
  86. package/dist/runtime/server.d.ts +0 -1
  87. package/dist/runtime/validate-entries.d.ts +0 -1
  88. package/dist/runtime/validate-entries.js +114 -10
  89. package/dist/runtime/validate-entries.js.map +1 -1
  90. package/dist/server/headers.d.ts +37 -4
  91. package/dist/server/headers.js +49 -13
  92. package/dist/server/headers.js.map +1 -1
  93. package/dist/server/load-config.d.ts +0 -1
  94. package/dist/server/load-env.d.ts +0 -1
  95. package/dist/server/prerendered.d.ts +14 -2
  96. package/dist/server/prerendered.js +42 -4
  97. package/dist/server/prerendered.js.map +1 -1
  98. package/dist/server/server-config.d.ts +0 -1
  99. package/dist/server/shutdown.d.ts +0 -1
  100. package/dist/server/ssg.d.ts +6 -1
  101. package/dist/server/ssg.js +30 -7
  102. package/dist/server/ssg.js.map +1 -1
  103. package/dist/server/static.d.ts +0 -1
  104. package/dist/server/static.js +5 -5
  105. package/dist/server/static.js.map +1 -1
  106. package/package.json +1 -1
  107. package/dist/builder/page-files.d.ts.map +0 -1
  108. package/dist/builder/public-env.d.ts.map +0 -1
  109. package/dist/builder/react-versions.d.ts.map +0 -1
  110. package/dist/builder/rspack-config.d.ts.map +0 -1
  111. package/dist/builder/server-only-imports.d.ts.map +0 -1
  112. package/dist/cli/build.d.ts.map +0 -1
  113. package/dist/cli/dev.d.ts.map +0 -1
  114. package/dist/cli/index.d.ts.map +0 -1
  115. package/dist/cli/start.d.ts.map +0 -1
  116. package/dist/config.d.ts.map +0 -1
  117. package/dist/deploy/aws-lambda/runtime.d.ts.map +0 -1
  118. package/dist/deploy/build-marker.d.ts.map +0 -1
  119. package/dist/deploy/cloudflare/build.d.ts.map +0 -1
  120. package/dist/deploy/cloudflare/runtime.d.ts.map +0 -1
  121. package/dist/deploy/contract.d.ts.map +0 -1
  122. package/dist/deploy/filesystem.d.ts.map +0 -1
  123. package/dist/deploy/node/runtime.d.ts.map +0 -1
  124. package/dist/deploy/presets.d.ts.map +0 -1
  125. package/dist/deploy/vercel/build.d.ts.map +0 -1
  126. package/dist/deploy/vercel/runtime.d.ts.map +0 -1
  127. package/dist/index.d.ts.map +0 -1
  128. package/dist/router.d.ts.map +0 -1
  129. package/dist/runtime/boundaries.d.ts.map +0 -1
  130. package/dist/runtime/client.d.ts.map +0 -1
  131. package/dist/runtime/context.d.ts.map +0 -1
  132. package/dist/runtime/control.d.ts.map +0 -1
  133. package/dist/runtime/dev-protocol.d.ts.map +0 -1
  134. package/dist/runtime/empty-server-app.d.ts.map +0 -1
  135. package/dist/runtime/entry.client.d.ts.map +0 -1
  136. package/dist/runtime/entry.rsc.d.ts.map +0 -1
  137. package/dist/runtime/entry.ssr.d.ts.map +0 -1
  138. package/dist/runtime/flight-inject.d.ts.map +0 -1
  139. package/dist/runtime/hot-update.d.ts.map +0 -1
  140. package/dist/runtime/navigation.d.ts.map +0 -1
  141. package/dist/runtime/request.d.ts.map +0 -1
  142. package/dist/runtime/server.d.ts.map +0 -1
  143. package/dist/runtime/validate-entries.d.ts.map +0 -1
  144. package/dist/server/headers.d.ts.map +0 -1
  145. package/dist/server/load-config.d.ts.map +0 -1
  146. package/dist/server/load-env.d.ts.map +0 -1
  147. package/dist/server/prerendered.d.ts.map +0 -1
  148. package/dist/server/server-config.d.ts.map +0 -1
  149. package/dist/server/shutdown.d.ts.map +0 -1
  150. package/dist/server/ssg.d.ts.map +0 -1
  151. package/dist/server/static.d.ts.map +0 -1
@@ -1,3 +1,4 @@
1
+ import { ASSET_MISS_CACHE_CONTROL } from '../../server/headers.js';
1
2
  import { createPageCache, ssgAssetPath, ssgFilePath, SSG_MANIFEST_FILE, toPrerenderedPage } from '../../server/prerendered.js';
2
3
  /**
3
4
  * Where `finalize` puts the prerendered pages inside the assets directory.
@@ -69,12 +70,33 @@ export const runtime = {
69
70
  // Normally dead — the CDN answers `/_static/*` before the worker is invoked — but keeps the deployment
70
71
  // correct, if slower, under an assets configuration that routes everything to the worker first.
71
72
  // `GET` covers `HEAD` — Hono dispatches one as the other. See `HTTPMethod`.
72
- app.get('/_static/*', async (c, next) => {
73
+ //
74
+ // Terminal, like the filesystem mount's last handler, and this used to be `next()`: a miss walked the
75
+ // whole route table, then `mountPublicFallback`, and landed in `app.notFound` — which for anything asking
76
+ // for HTML is a full server render of the app's 404 page under a prefix no app can own. A browser asking
77
+ // for a real subresource sends `Accept: */*` and got the plain 404 either way, so what it cost was a
78
+ // render for a crawler, a probe, or a hand-typed URL. `RESERVED_PREFIX`'s doc in `validate-entries.ts`
79
+ // says the mount ends in a terminal 404 and the reserved-route check leans on it; on this target it did
80
+ // not. `null` — no assets binding at all — answers the same way: nothing else in the worker can serve
81
+ // this prefix, so falling through would only find a longer route to the same status.
82
+ app.get('/_static/*', async (c) => {
73
83
  const asset = await assetResponse(c, c.req.path, c.req.raw.headers);
74
- return asset && asset.status !== 404 ? serveAsset(asset) : next();
84
+ if (asset && asset.status !== 404)
85
+ return serveAsset(asset);
86
+ return c.text('Not Found', 404, { 'cache-control': ASSET_MISS_CACHE_CONTROL });
75
87
  });
76
88
  },
77
89
  mountPublicFallback(app) {
90
+ // Live here and a no-op on `vercel`, which is the whole difference between the two CDN targets and is
91
+ // written up on {@link DeployRuntime.mountPublicFallback}. Normally dead for the same reason the
92
+ // `/_static` mount above is — the CDN answers first — but `public/` is in the *same* binding this can
93
+ // read, so an assets configuration that routes to the worker first keeps its files instead of losing
94
+ // them. Vercel has nothing equivalent: `public/` went into the static output and is not uploaded with
95
+ // the function, so there is nothing there for a mount to answer from.
96
+ //
97
+ // Registered after every route, so inside the worker a route still beats a `public/` file — the
98
+ // ordering the contract describes, reached on the one target whose CDN can be told to stand aside. At
99
+ // the CDN the file wins, which is what `publicRouteCollisions` warns about at build time.
78
100
  app.get('/*', async (c, next) => {
79
101
  // The prerender tree lives in the same store, but the app only serves it through `readPrerendered`.
80
102
  if (c.req.path.startsWith(`${SSG_PREFIX}/`))
@@ -1 +1 @@
1
- {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/deploy/cloudflare/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context, Hono } from 'hono';\nimport { createPageCache, ssgAssetPath, ssgFilePath, SSG_MANIFEST_FILE, toPrerenderedPage, type PrerenderedPage } from '../../server/prerendered.js';\nimport type { DeployRuntime } from '../contract.js';\n\n/**\n * Where `finalize` puts the prerendered pages inside the assets directory.\n *\n * A prefix of its own, rather than the pages' real URLs, is what keeps every page URL reaching the worker: one\n * URL answers with a document or a flight payload on the `RSC` request header, and a path-keyed CDN cannot make that\n * choice. The tree is reachable here too, so `finalize` marks it `noindex` rather than leaving a crawler to\n * find the same page twice.\n */\nconst SSG_PREFIX = '/__ssg';\n\n/** The Workers Assets binding, as much of it as this file uses. */\ninterface AssetsBinding {\n fetch(request: Request): Promise<Response>;\n}\n\nfunction assetsBinding(c: Context): AssetsBinding | null {\n const binding = (c.env as { ASSETS?: AssetsBinding } | undefined)?.ASSETS;\n return binding && typeof binding.fetch === 'function' ? binding : null;\n}\n\n/**\n * Asks the assets binding for one path, or `null` when there is no binding — which is how a caller tells \"this\n * deployment serves its assets some other way\" from \"that file is not there\".\n *\n * `headers` is forwarded only where the *client's* conditional and range requests should be answered by the\n * store: an `If-None-Match` on a page being fetched to serve would come back 304 with no body.\n */\nasync function assetResponse(c: Context, path: string, headers?: Headers): Promise<Response | null> {\n const binding = assetsBinding(c);\n if (!binding) return null;\n // Resolved against the request so the lookup carries this deployment's own origin; only the path is used.\n const url = new URL(path, c.req.url);\n return binding.fetch(new Request(url, { method: 'GET', headers }));\n}\n\n/**\n * Hands an asset response back as the app's own. Rebuilt rather than returned as-is, because a `Response` from\n * `fetch` has immutable headers and the framework's outermost middleware writes to them on the way out.\n */\nfunction serveAsset(asset: Response): Response {\n return new Response(asset.body, asset);\n}\n\n/** Prerendered pages, keyed by the path that produced them. Per isolate rather than per process. */\nconst pageCache = createPageCache();\n\n/** The store's own index of itself, fetched once per isolate — see {@link SSG_MANIFEST_FILE}. */\nlet storeIndex: Promise<ReadonlySet<string> | null> | undefined;\n\n/**\n * What the store holds, or `null` where there is no manifest to read — an older build, or a deployment with\n * no assets binding at all. `null` gates nothing, which is how it behaved before there was an index.\n *\n * Worth one fetch per isolate: without it every request for a path the build did not prerender spends a\n * subrequest asking the binding for a file that is not there, and misses are deliberately not cached.\n */\nfunction knownFiles(c: Context): Promise<ReadonlySet<string> | null> {\n storeIndex ??= assetResponse(c, `${SSG_PREFIX}/${SSG_MANIFEST_FILE}`)\n .then(async (asset) => {\n if (!asset || asset.status !== 200) return null;\n const manifest = (await asset.json()) as { files?: string[] };\n return new Set(manifest.files ?? []);\n })\n .catch(() => null);\n return storeIndex;\n}\n\n/**\n * Cloudflare Workers: the host owns the process, so there is nothing to listen on, and there is no filesystem,\n * so `.env` files are out. What is left is the assets binding, which every read here goes through.\n */\nexport const runtime: DeployRuntime = {\n serveApp(app: Hono): unknown {\n // `app.fetch` already takes `(request, env, ctx)`, which is why bindings arrive as `c.env`.\n return { fetch: app.fetch };\n },\n\n mountStaticAssets(app: Hono): void {\n // Normally dead — the CDN answers `/_static/*` before the worker is invoked — but keeps the deployment\n // correct, if slower, under an assets configuration that routes everything to the worker first.\n // `GET` covers `HEAD` — Hono dispatches one as the other. See `HTTPMethod`.\n app.get('/_static/*', async (c, next) => {\n const asset = await assetResponse(c, c.req.path, c.req.raw.headers);\n return asset && asset.status !== 404 ? serveAsset(asset) : next();\n });\n },\n\n mountPublicFallback(app: Hono): void {\n app.get('/*', async (c, next) => {\n // The prerender tree lives in the same store, but the app only serves it through `readPrerendered`.\n if (c.req.path.startsWith(`${SSG_PREFIX}/`)) return next();\n const asset = await assetResponse(c, c.req.path, c.req.raw.headers);\n return asset && asset.status !== 404 ? serveAsset(asset) : next();\n });\n },\n\n async readPrerendered(c: Context, variant): Promise<PrerenderedPage | null> {\n const relPath = ssgFilePath(c.req.path, variant);\n if (relPath === null) return null;\n\n const key = `${variant}\\0${relPath}`;\n const cached = pageCache.get(key);\n if (cached) return cached;\n\n const index = await knownFiles(c);\n if (index && !index.has(relPath)) return null;\n\n // Escaped on the way into the URL and decoded again by the store, so this lands on the file the build\n // wrote under exactly `relPath` — a page whose slug holds a `#` or a `?` included.\n const asset = await assetResponse(c, `${SSG_PREFIX}/${ssgAssetPath(relPath)}`);\n if (!asset || asset.status !== 200) return null;\n\n // The store's own validator where it has one — it already describes these exact bytes.\n const page = await toPrerenderedPage(new Uint8Array(await asset.arrayBuffer()), asset.headers.get('etag'));\n pageCache.set(key, page);\n return page;\n },\n\n loadEnv(): void {\n // Nothing to load: secrets and bindings arrive per request as `c.env`, which `getRequestContext().env`\n // already merges.\n },\n};\n"]}
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../../src/deploy/cloudflare/runtime.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Context, Hono } from 'hono';\nimport { ASSET_MISS_CACHE_CONTROL } from '../../server/headers.js';\nimport { createPageCache, ssgAssetPath, ssgFilePath, SSG_MANIFEST_FILE, toPrerenderedPage, type PrerenderedPage } from '../../server/prerendered.js';\nimport type { DeployRuntime } from '../contract.js';\n\n/**\n * Where `finalize` puts the prerendered pages inside the assets directory.\n *\n * A prefix of its own, rather than the pages' real URLs, is what keeps every page URL reaching the worker: one\n * URL answers with a document or a flight payload on the `RSC` request header, and a path-keyed CDN cannot make that\n * choice. The tree is reachable here too, so `finalize` marks it `noindex` rather than leaving a crawler to\n * find the same page twice.\n */\nconst SSG_PREFIX = '/__ssg';\n\n/** The Workers Assets binding, as much of it as this file uses. */\ninterface AssetsBinding {\n fetch(request: Request): Promise<Response>;\n}\n\nfunction assetsBinding(c: Context): AssetsBinding | null {\n const binding = (c.env as { ASSETS?: AssetsBinding } | undefined)?.ASSETS;\n return binding && typeof binding.fetch === 'function' ? binding : null;\n}\n\n/**\n * Asks the assets binding for one path, or `null` when there is no binding — which is how a caller tells \"this\n * deployment serves its assets some other way\" from \"that file is not there\".\n *\n * `headers` is forwarded only where the *client's* conditional and range requests should be answered by the\n * store: an `If-None-Match` on a page being fetched to serve would come back 304 with no body.\n */\nasync function assetResponse(c: Context, path: string, headers?: Headers): Promise<Response | null> {\n const binding = assetsBinding(c);\n if (!binding) return null;\n // Resolved against the request so the lookup carries this deployment's own origin; only the path is used.\n const url = new URL(path, c.req.url);\n return binding.fetch(new Request(url, { method: 'GET', headers }));\n}\n\n/**\n * Hands an asset response back as the app's own. Rebuilt rather than returned as-is, because a `Response` from\n * `fetch` has immutable headers and the framework's outermost middleware writes to them on the way out.\n */\nfunction serveAsset(asset: Response): Response {\n return new Response(asset.body, asset);\n}\n\n/** Prerendered pages, keyed by the path that produced them. Per isolate rather than per process. */\nconst pageCache = createPageCache();\n\n/** The store's own index of itself, fetched once per isolate — see {@link SSG_MANIFEST_FILE}. */\nlet storeIndex: Promise<ReadonlySet<string> | null> | undefined;\n\n/**\n * What the store holds, or `null` where there is no manifest to read — an older build, or a deployment with\n * no assets binding at all. `null` gates nothing, which is how it behaved before there was an index.\n *\n * Worth one fetch per isolate: without it every request for a path the build did not prerender spends a\n * subrequest asking the binding for a file that is not there, and misses are deliberately not cached.\n */\nfunction knownFiles(c: Context): Promise<ReadonlySet<string> | null> {\n storeIndex ??= assetResponse(c, `${SSG_PREFIX}/${SSG_MANIFEST_FILE}`)\n .then(async (asset) => {\n if (!asset || asset.status !== 200) return null;\n const manifest = (await asset.json()) as { files?: string[] };\n return new Set(manifest.files ?? []);\n })\n .catch(() => null);\n return storeIndex;\n}\n\n/**\n * Cloudflare Workers: the host owns the process, so there is nothing to listen on, and there is no filesystem,\n * so `.env` files are out. What is left is the assets binding, which every read here goes through.\n */\nexport const runtime: DeployRuntime = {\n serveApp(app: Hono): unknown {\n // `app.fetch` already takes `(request, env, ctx)`, which is why bindings arrive as `c.env`.\n return { fetch: app.fetch };\n },\n\n mountStaticAssets(app: Hono): void {\n // Normally dead — the CDN answers `/_static/*` before the worker is invoked — but keeps the deployment\n // correct, if slower, under an assets configuration that routes everything to the worker first.\n // `GET` covers `HEAD` — Hono dispatches one as the other. See `HTTPMethod`.\n //\n // Terminal, like the filesystem mount's last handler, and this used to be `next()`: a miss walked the\n // whole route table, then `mountPublicFallback`, and landed in `app.notFound` — which for anything asking\n // for HTML is a full server render of the app's 404 page under a prefix no app can own. A browser asking\n // for a real subresource sends `Accept: */*` and got the plain 404 either way, so what it cost was a\n // render for a crawler, a probe, or a hand-typed URL. `RESERVED_PREFIX`'s doc in `validate-entries.ts`\n // says the mount ends in a terminal 404 and the reserved-route check leans on it; on this target it did\n // not. `null` — no assets binding at all — answers the same way: nothing else in the worker can serve\n // this prefix, so falling through would only find a longer route to the same status.\n app.get('/_static/*', async (c) => {\n const asset = await assetResponse(c, c.req.path, c.req.raw.headers);\n if (asset && asset.status !== 404) return serveAsset(asset);\n return c.text('Not Found', 404, { 'cache-control': ASSET_MISS_CACHE_CONTROL });\n });\n },\n\n mountPublicFallback(app: Hono): void {\n // Live here and a no-op on `vercel`, which is the whole difference between the two CDN targets and is\n // written up on {@link DeployRuntime.mountPublicFallback}. Normally dead for the same reason the\n // `/_static` mount above is — the CDN answers first — but `public/` is in the *same* binding this can\n // read, so an assets configuration that routes to the worker first keeps its files instead of losing\n // them. Vercel has nothing equivalent: `public/` went into the static output and is not uploaded with\n // the function, so there is nothing there for a mount to answer from.\n //\n // Registered after every route, so inside the worker a route still beats a `public/` file — the\n // ordering the contract describes, reached on the one target whose CDN can be told to stand aside. At\n // the CDN the file wins, which is what `publicRouteCollisions` warns about at build time.\n app.get('/*', async (c, next) => {\n // The prerender tree lives in the same store, but the app only serves it through `readPrerendered`.\n if (c.req.path.startsWith(`${SSG_PREFIX}/`)) return next();\n const asset = await assetResponse(c, c.req.path, c.req.raw.headers);\n return asset && asset.status !== 404 ? serveAsset(asset) : next();\n });\n },\n\n async readPrerendered(c: Context, variant): Promise<PrerenderedPage | null> {\n const relPath = ssgFilePath(c.req.path, variant);\n if (relPath === null) return null;\n\n const key = `${variant}\\0${relPath}`;\n const cached = pageCache.get(key);\n if (cached) return cached;\n\n const index = await knownFiles(c);\n if (index && !index.has(relPath)) return null;\n\n // Escaped on the way into the URL and decoded again by the store, so this lands on the file the build\n // wrote under exactly `relPath` — a page whose slug holds a `#` or a `?` included.\n const asset = await assetResponse(c, `${SSG_PREFIX}/${ssgAssetPath(relPath)}`);\n if (!asset || asset.status !== 200) return null;\n\n // The store's own validator where it has one — it already describes these exact bytes.\n const page = await toPrerenderedPage(new Uint8Array(await asset.arrayBuffer()), asset.headers.get('etag'));\n pageCache.set(key, page);\n return page;\n },\n\n loadEnv(): void {\n // Nothing to load: secrets and bindings arrive per request as `c.env`, which `getRequestContext().env`\n // already merges.\n },\n};\n"]}
@@ -46,7 +46,26 @@ export interface DeployRuntime {
46
46
  * serves it.
47
47
  */
48
48
  mountStaticAssets(app: Hono): void;
49
- /** Mounts `public/` at the web root, after every route, so it only answers paths no route claimed. */
49
+ /**
50
+ * Mounts `public/` at the web root, after every route, so it only answers paths no route claimed.
51
+ *
52
+ * **That ordering is only within the app**, which is the whole surface on `node` and `aws-lambda` and not
53
+ * on the two targets with a CDN in front. There `public/` is part of the static output and the platform
54
+ * answers from it *before* the app is invoked — `{ handle: 'filesystem' }` ahead of the function on
55
+ * Vercel, Workers Assets ahead of the worker on Cloudflare — so it is a CDN-first store rather than a
56
+ * fallback. `rshono build` warns about a `public/` file that lands on a route's path for exactly that
57
+ * reason; see `publicRouteCollisions`.
58
+ *
59
+ * The two targets do not answer that the same way, and the difference is worth having written down here
60
+ * rather than discovered in one of them. **Vercel implements this as a no-op**: nothing is uploaded with
61
+ * the function for it to read, so a mount could never answer. **Cloudflare implements it for real**,
62
+ * against the `ASSETS` binding — normally dead, since the CDN is checked first, but the binding is the
63
+ * same store either way and an assets configuration that routes to the worker first would otherwise lose
64
+ * `public/` entirely. That is the reason its `mountStaticAssets` is live too. So on Cloudflare a `public/`
65
+ * file is CDN-first *and*, inside the worker, a genuine fallback that a route still beats — which is the
66
+ * ordering this method's first line describes, reached by the one target whose CDN can be told to stand
67
+ * aside.
68
+ */
50
69
  mountPublicFallback(app: Hono): void;
51
70
  /**
52
71
  * Reads the page prerendered for `c.req.path`, or `null` when there is none — in which case the route
@@ -60,4 +79,3 @@ export interface DeployRuntime {
60
79
  /** Loads `.env` files where the platform has a filesystem. Env is bindings elsewhere. */
61
80
  loadEnv(): void;
62
81
  }
63
- //# sourceMappingURL=contract.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/deploy/contract.ts"],"names":[],"mappings":"","sourcesContent":["import type { Context, Hono } from 'hono';\nimport type { PrerenderVariant, PrerenderedPage } from '../server/prerendered.js';\n\n/**\n * A hosting platform `rshono build` targets. Selected with `deploy` in `rshono.config.ts`, the\n * `--deploy` flag or the `RSHONO_DEPLOY` env var.\n *\n * - `'node'` (the default) — rshono binds the port and you run the build with `rshono start`. Covers a\n * VPS, a container, a PaaS, and — through `node:` compatibility — Bun and Deno.\n * - `'cloudflare'` — a Worker; the entry exports `{ fetch }`.\n * - `'vercel'` — a Vercel function, plus the on-disk layout and config streaming needs there.\n * - `'aws-lambda'` — a streaming Lambda handler.\n *\n * There is one target per *handoff* — who opens the socket, and what shape a request arrives in — since\n * that is the part an app cannot arrange for itself. `rshono dev` always runs the `node` server.\n *\n * @example\n * ```ts\n * export default defineConfig({ deploy: 'cloudflare' });\n * ```\n *\n * @see {@link https://www.rshono.com/docs/deployment#the-targets | Docs — the targets}\n */\nexport type DeployTarget = 'node' | 'cloudflare' | 'vercel' | 'aws-lambda';\n\n/**\n * Everything the app server needs from the platform it runs on — the whole of what \"which platform is\n * this\" means at request time, since `runtime/entry.rsc.tsx` is written against this and nothing else.\n *\n * One preset implements it per target, and the build-time `@rshono/deploy` alias resolves to exactly\n * that module, so only the selected platform's code is ever in the bundle.\n */\nexport interface DeployRuntime {\n /**\n * Hands the app to the platform and returns whatever the entry module should `export default` there:\n * nothing where rshono owns the process (this binds the port), otherwise the export the platform looks\n * for — `{ fetch }` on Workers, a streaming handler on Lambda, and on Vercel a Node\n * `(IncomingMessage, ServerResponse)` listener, which is what its `Nodejs` launcher calls.\n *\n * Note that a web `Request` is not the common currency here: two of the three targets are handed one by\n * their platform, and Vercel is not, so converting is part of the handoff rather than something the\n * request-handling code below can assume has already happened.\n */\n serveApp(app: Hono): unknown;\n /**\n * Mounts the hashed client bundle at `/_static` — after src/server.ts, so the app's own middleware covers an\n * asset response too, and ahead of the page routes, which cannot claim that prefix. A no-op where a CDN\n * serves it.\n */\n mountStaticAssets(app: Hono): void;\n /** Mounts `public/` at the web root, after every route, so it only answers paths no route claimed. */\n mountPublicFallback(app: Hono): void;\n /**\n * Reads the page prerendered for `c.req.path`, or `null` when there is none — in which case the route\n * renders per request.\n *\n * Takes the whole {@link Context} because without a filesystem the store *is* a request-scoped\n * binding (`c.env.ASSETS` on Workers). The path is untrusted either way, so an implementation treats\n * traversal as a miss — see `ssgFilePath`, the one mapping from a path to the file that holds its page.\n */\n readPrerendered(c: Context, variant: PrerenderVariant): Promise<PrerenderedPage | null>;\n /** Loads `.env` files where the platform has a filesystem. Env is bindings elsewhere. */\n loadEnv(): void;\n}\n"]}
1
+ {"version":3,"file":"contract.js","sourceRoot":"","sources":["../../src/deploy/contract.ts"],"names":[],"mappings":"","sourcesContent":["import type { Context, Hono } from 'hono';\nimport type { PrerenderVariant, PrerenderedPage } from '../server/prerendered.js';\n\n/**\n * A hosting platform `rshono build` targets. Selected with `deploy` in `rshono.config.ts`, the\n * `--deploy` flag or the `RSHONO_DEPLOY` env var.\n *\n * - `'node'` (the default) — rshono binds the port and you run the build with `rshono start`. Covers a\n * VPS, a container, a PaaS, and — through `node:` compatibility — Bun and Deno.\n * - `'cloudflare'` — a Worker; the entry exports `{ fetch }`.\n * - `'vercel'` — a Vercel function, plus the on-disk layout and config streaming needs there.\n * - `'aws-lambda'` — a streaming Lambda handler.\n *\n * There is one target per *handoff* — who opens the socket, and what shape a request arrives in — since\n * that is the part an app cannot arrange for itself. `rshono dev` always runs the `node` server.\n *\n * @example\n * ```ts\n * export default defineConfig({ deploy: 'cloudflare' });\n * ```\n *\n * @see {@link https://www.rshono.com/docs/deployment#the-targets | Docs — the targets}\n */\nexport type DeployTarget = 'node' | 'cloudflare' | 'vercel' | 'aws-lambda';\n\n/**\n * Everything the app server needs from the platform it runs on — the whole of what \"which platform is\n * this\" means at request time, since `runtime/entry.rsc.tsx` is written against this and nothing else.\n *\n * One preset implements it per target, and the build-time `@rshono/deploy` alias resolves to exactly\n * that module, so only the selected platform's code is ever in the bundle.\n */\nexport interface DeployRuntime {\n /**\n * Hands the app to the platform and returns whatever the entry module should `export default` there:\n * nothing where rshono owns the process (this binds the port), otherwise the export the platform looks\n * for — `{ fetch }` on Workers, a streaming handler on Lambda, and on Vercel a Node\n * `(IncomingMessage, ServerResponse)` listener, which is what its `Nodejs` launcher calls.\n *\n * Note that a web `Request` is not the common currency here: two of the three targets are handed one by\n * their platform, and Vercel is not, so converting is part of the handoff rather than something the\n * request-handling code below can assume has already happened.\n */\n serveApp(app: Hono): unknown;\n /**\n * Mounts the hashed client bundle at `/_static` — after src/server.ts, so the app's own middleware covers an\n * asset response too, and ahead of the page routes, which cannot claim that prefix. A no-op where a CDN\n * serves it.\n */\n mountStaticAssets(app: Hono): void;\n /**\n * Mounts `public/` at the web root, after every route, so it only answers paths no route claimed.\n *\n * **That ordering is only within the app**, which is the whole surface on `node` and `aws-lambda` and not\n * on the two targets with a CDN in front. There `public/` is part of the static output and the platform\n * answers from it *before* the app is invoked — `{ handle: 'filesystem' }` ahead of the function on\n * Vercel, Workers Assets ahead of the worker on Cloudflare — so it is a CDN-first store rather than a\n * fallback. `rshono build` warns about a `public/` file that lands on a route's path for exactly that\n * reason; see `publicRouteCollisions`.\n *\n * The two targets do not answer that the same way, and the difference is worth having written down here\n * rather than discovered in one of them. **Vercel implements this as a no-op**: nothing is uploaded with\n * the function for it to read, so a mount could never answer. **Cloudflare implements it for real**,\n * against the `ASSETS` binding — normally dead, since the CDN is checked first, but the binding is the\n * same store either way and an assets configuration that routes to the worker first would otherwise lose\n * `public/` entirely. That is the reason its `mountStaticAssets` is live too. So on Cloudflare a `public/`\n * file is CDN-first *and*, inside the worker, a genuine fallback that a route still beats — which is the\n * ordering this method's first line describes, reached by the one target whose CDN can be told to stand\n * aside.\n */\n mountPublicFallback(app: Hono): void;\n /**\n * Reads the page prerendered for `c.req.path`, or `null` when there is none — in which case the route\n * renders per request.\n *\n * Takes the whole {@link Context} because without a filesystem the store *is* a request-scoped\n * binding (`c.env.ASSETS` on Workers). The path is untrusted either way, so an implementation treats\n * traversal as a miss — see `ssgFilePath`, the one mapping from a path to the file that holds its page.\n */\n readPrerendered(c: Context, variant: PrerenderVariant): Promise<PrerenderedPage | null>;\n /** Loads `.env` files where the platform has a filesystem. Env is bindings elsewhere. */\n loadEnv(): void;\n}\n"]}
@@ -5,4 +5,3 @@ import type { DeployRuntime } from './contract.js';
5
5
  * those presets is this object plus its own {@link DeployRuntime.serveApp}.
6
6
  */
7
7
  export declare const fileSystemRuntime: Omit<DeployRuntime, 'serveApp'>;
8
- //# sourceMappingURL=filesystem.d.ts.map
@@ -7,4 +7,3 @@ import type { DeployRuntime } from '../contract.js';
7
7
  * `@hono/node-server`, and both implement the `node:` APIs it needs.
8
8
  */
9
9
  export declare const runtime: DeployRuntime;
10
- //# sourceMappingURL=runtime.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import type { RspackOptions } from '@rspack/core';
2
+ import type { Route } from '../router.js';
2
3
  import type { DeployTarget } from './contract.js';
3
4
  /** What a preset's {@link DeployPreset.finalize} is told about the build it is arranging. */
4
5
  export interface DeployBuildContext {
@@ -12,6 +13,12 @@ export interface DeployBuildContext {
12
13
  publicDir: string | null;
13
14
  /** Prerendered pages, `<root>/dist/ssg` — empty when the app has no `render: 'static'` routes. */
14
15
  ssgDir: string;
16
+ /**
17
+ * The app's route table, as the bundle validated it. Read by the two CDN presets: their platform serves
18
+ * the static output before the app is invoked, so a `public/` file that lands on a route's path answers
19
+ * instead of it — see `publicRouteCollisions`.
20
+ */
21
+ routes: readonly Route[];
15
22
  }
16
23
  /**
17
24
  * The build-time half of a deploy target: which runtime module the bundle gets, how the server compiler has
@@ -83,4 +90,3 @@ export interface DeploySources {
83
90
  * `RSHONO_DEPLOY=` in CI falls through rather than failing the build.
84
91
  */
85
92
  export declare function resolveDeployPreset(sources?: DeploySources): DeployPreset;
86
- //# sourceMappingURL=presets.d.ts.map
@@ -86,12 +86,23 @@ const PRESETS = {
86
86
  };
87
87
  /** Every target `deploy` accepts, for error messages and docs. */
88
88
  export const DEPLOY_TARGETS = Object.keys(PRESETS);
89
+ /**
90
+ * `PRESETS[target]` for a target that really is one.
91
+ *
92
+ * `Object.hasOwn` rather than a bare bracket access, which resolves every `Object.prototype` key —
93
+ * `constructor`, `__proto__`, `toString` — to an inherited value that then passes a truthiness guard. A typo
94
+ * that happens to be one of those used to reach the builder and die on `preset.runtimeModule.split('/')`
95
+ * instead of getting the message written for an unknown target.
96
+ */
97
+ function presetFor(target) {
98
+ return Object.hasOwn(PRESETS, target) ? PRESETS[target] : undefined;
99
+ }
89
100
  /**
90
101
  * How to deploy what a given target built, or `null` for a name this rshono does not know — which a `dist/`
91
102
  * from a newer version can legitimately carry.
92
103
  */
93
104
  export function deployHintFor(target) {
94
- return PRESETS[target]?.deployHint ?? null;
105
+ return presetFor(target)?.deployHint ?? null;
95
106
  }
96
107
  /**
97
108
  * Resolves the preset to build with: the flag wins over the environment, which wins over the config file,
@@ -102,7 +113,7 @@ export function resolveDeployPreset(sources = {}) {
102
113
  const target = sources.flag?.trim() || sources.env?.trim() || sources.config?.trim();
103
114
  if (!target)
104
115
  return NODE_PRESET;
105
- const preset = PRESETS[target];
116
+ const preset = presetFor(target);
106
117
  if (!preset) {
107
118
  throw new Error(`[rshono] unknown deploy target ${JSON.stringify(target)} — expected one of: ${DEPLOY_TARGETS.join(', ')}.`);
108
119
  }
@@ -1 +1 @@
1
- {"version":3,"file":"presets.js","sourceRoot":"","sources":["../../src/deploy/presets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAEhE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AA4DxD;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAiB;IACvC,IAAI,EAAE,MAAM;IACZ,aAAa,EAAE,wBAAwB;IACvC,UAAU,EAAE,oBAAoB;CACjC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,iBAAiB,GAAiB;IACtC,IAAI,EAAE,YAAY;IAClB,aAAa,EAAE,8BAA8B;IAC7C,UAAU,EAAE,+BAA+B;IAC3C,WAAW,EAAE,IAAI;IACjB,iBAAiB,EAAE,CAAC,SAAS,CAAC;IAC9B,aAAa,EAAE,CAAC,YAAY,CAAC;IAC7B,eAAe,CAAC,MAAM;QACpB,MAAM,CAAC,MAAM,GAAG,WAAW,CAAC;QAC5B,MAAM,CAAC,aAAa,GAAG,eAAe,CAAC;QACvC,MAAM,CAAC,SAAS,GAAG,CAAC,uBAAuB,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IAC3D,CAAC;IACD,QAAQ,EAAE,uBAAuB;CAClC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,SAAS,kBAAkB,CAAC,MAAqB;IAC/C,MAAM,CAAC,SAAS,GAAG,EAAE,CAAC;AACxB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,aAAa,GAAiB;IAClC,IAAI,EAAE,QAAQ;IACd,aAAa,EAAE,0BAA0B;IACzC,UAAU,EAAE,wCAAwC;IACpD,eAAe,EAAE,kBAAkB;IACnC,QAAQ,EAAE,mBAAmB;CAC9B,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,iBAAiB,GAAiB;IACtC,IAAI,EAAE,YAAY;IAClB,aAAa,EAAE,8BAA8B;IAC7C,UAAU,EAAE,oDAAoD;IAChE,eAAe,EAAE,kBAAkB;CACpC,CAAC;AAEF,MAAM,OAAO,GAAuC;IAClD,IAAI,EAAE,WAAW;IACjB,UAAU,EAAE,iBAAiB;IAC7B,MAAM,EAAE,aAAa;IACrB,YAAY,EAAE,iBAAiB;CAChC,CAAC;AAEF,kEAAkE;AAClE,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAmB,CAAC;AAErE;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,OAAQ,OAAoD,CAAC,MAAM,CAAC,EAAE,UAAU,IAAI,IAAI,CAAC;AAC3F,CAAC;AAYD;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAO,GAAkB,EAAE;IAC7D,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;IACrF,IAAI,CAAC,MAAM;QAAE,OAAO,WAAW,CAAC;IAEhC,MAAM,MAAM,GAAI,OAAoD,CAAC,MAAM,CAAC,CAAC;IAC7E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,kCAAkC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,uBAAuB,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/H,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import type { RspackOptions } from '@rspack/core';\nimport { finalizeCloudflareBuild } from './cloudflare/build.js';\nimport type { DeployTarget } from './contract.js';\nimport { finalizeVercelBuild } from './vercel/build.js';\n\n/** What a preset's {@link DeployPreset.finalize} is told about the build it is arranging. */\nexport interface DeployBuildContext {\n /** The project root — where `rshono build` ran, and what a platform config file is written next to. */\n rootDir: string;\n /** `<root>/dist`. */\n distDir: string;\n /** The hashed client bundle, `<root>/dist/static` — served at `/_static`. */\n staticDir: string;\n /** The copy of the app's `public/`, or `null` when it has none. Served at the web root. */\n publicDir: string | null;\n /** Prerendered pages, `<root>/dist/ssg` — empty when the app has no `render: 'static'` routes. */\n ssgDir: string;\n}\n\n/**\n * The build-time half of a deploy target: which runtime module the bundle gets, how the server compiler has\n * to change to produce something the platform can run, and how the output is arranged once it exists.\n *\n * The runtime half is `DeployRuntime`, in its own file because it is compiled *into* the app bundle — this\n * side only ever runs in the CLI.\n */\nexport interface DeployPreset {\n readonly name: DeployTarget;\n /**\n * The module `@rshono/deploy` resolves to, as a path relative to the framework's own `dist/`.\n * Slash-separated and split on use, so it stays a valid path on Windows too.\n */\n readonly runtimeModule: string;\n /** How to run what was just built, completing the \"build complete —\" line. */\n readonly deployHint: string;\n /**\n * `true` where the platform hands per-request bindings to `app.fetch(request, env)` — Workers, and\n * nowhere else. It gates the `getRequestContext().env` merge, because every other target passes its own\n * private handles in that argument (`{ incoming, outgoing }` on Node, the whole invocation on Lambda) and\n * merging those would put a live socket, or a request's headers and cookies, behind a name typed\n * `string | undefined`.\n */\n readonly envBindings?: boolean;\n /**\n * Extra resolve conditions for the server bundle, most specific first — what picks the right build of React\n * and the RSC runtime, both of which ship one per runtime. Omit to accept whatever the Rspack target\n * implies, which is correct for Node.\n */\n readonly resolveConditions?: readonly string[];\n /** browserslist-style targets for the server bundle's swc pass. Defaults to Node. */\n readonly syntaxTargets?: readonly string[];\n /**\n * Adjusts the generated server Rspack config for this platform — target, externals policy, output shape.\n * Mutates in place, and runs before the user's `rspack` hook so that hook keeps the last word.\n */\n configureServer?(config: RspackOptions): void;\n /**\n * Arranges the finished build for the platform: assembles the directory layout it expects, emits its config\n * file, and prints how to deploy. Runs last, so everything it needs to move is already on disk.\n */\n finalize?(ctx: DeployBuildContext): Promise<void> | void;\n}\n\n/**\n * Node: a long-lived server process. The generated config is already this shape — `target: 'node'`, the\n * externals policy and ESM chunk output are all defaults — so the preset has nothing to contribute.\n */\nexport const NODE_PRESET: DeployPreset = {\n name: 'node',\n runtimeModule: 'deploy/node/runtime.js',\n deployHint: 'run `rshono start`',\n};\n\n/**\n * Cloudflare Workers: the host owns the process, the CDN owns the assets, and there is no filesystem.\n *\n * Every setting here follows from `workerd` not being Node. Dependencies are bundled because nothing resolves\n * `node_modules` at runtime; `node:` and `cloudflare:` imports stay external because the runtime provides\n * them under `nodejs_compat`; and async chunks are inlined because Wrangler's bundler cannot follow the\n * computed specifier Rspack's ESM chunk loader emits — a split bundle deploys and then fails on first render.\n */\nconst CLOUDFLARE_PRESET: DeployPreset = {\n name: 'cloudflare',\n runtimeModule: 'deploy/cloudflare/runtime.js',\n deployHint: 'deploy with `wrangler deploy`',\n envBindings: true,\n resolveConditions: ['workerd'],\n syntaxTargets: ['chrome 120'],\n configureServer(config) {\n config.target = 'webworker';\n config.externalsType = 'module-import';\n config.externals = [/^(?:node|cloudflare):/];\n config.output = { ...config.output, asyncChunks: false };\n },\n finalize: finalizeCloudflareBuild,\n};\n\n/**\n * Bundles the app's dependencies into the server output instead of importing them from `node_modules`.\n *\n * The generated config externalizes them, which is right where the bundle runs *inside* the project — but a\n * serverless function is uploaded as a directory rather than installed into one, so nothing resolves\n * `node_modules` at request time and an externalized `import 'some-package'` is a cold start that dies on\n * `ERR_MODULE_NOT_FOUND`. Node's own builtins stay external either way, through the `node` externals preset\n * that `target: 'node'` already enables.\n *\n * The cost is a dependency that cannot be bundled — a native addon, or one that reads its own files off disk\n * relative to `__dirname`. Those now fail the build rather than the deploy, which is the same constraint\n * `cloudflare` has always had; the `rspack` hook in `rshono.config.ts` is the way out.\n */\nfunction bundleDependencies(config: RspackOptions): void {\n config.externals = [];\n}\n\n/**\n * Vercel: one Node function behind the platform's CDN, which serves the assets and reaches the function only\n * for a page. `finalize` assembles the Build Output API layout the platform uploads — including the\n * `supportsResponseStreaming` flag, without which Vercel buffers the whole response and silently undoes\n * streamed SSR.\n *\n * Only `dist/server` is uploaded with the function, which is why dependencies are bundled: see\n * {@link bundleDependencies}.\n */\nconst VERCEL_PRESET: DeployPreset = {\n name: 'vercel',\n runtimeModule: 'deploy/vercel/runtime.js',\n deployHint: 'deploy with `vercel deploy --prebuilt`',\n configureServer: bundleDependencies,\n finalize: finalizeVercelBuild,\n};\n\n/**\n * AWS Lambda behind a Function URL in `RESPONSE_STREAM` mode — the AWS shape that keeps streaming. The\n * runtime wraps the app in `awslambda.streamifyResponse`; the buffered alternative would deploy fine and then\n * hold every page until its last byte rendered.\n *\n * The deployment package is `dist/` and nothing else, which is why dependencies are bundled: see\n * {@link bundleDependencies}.\n */\nconst AWS_LAMBDA_PRESET: DeployPreset = {\n name: 'aws-lambda',\n runtimeModule: 'deploy/aws-lambda/runtime.js',\n deployHint: 'zip dist/ with the handler at dist/server/main.mjs',\n configureServer: bundleDependencies,\n};\n\nconst PRESETS: Record<DeployTarget, DeployPreset> = {\n node: NODE_PRESET,\n cloudflare: CLOUDFLARE_PRESET,\n vercel: VERCEL_PRESET,\n 'aws-lambda': AWS_LAMBDA_PRESET,\n};\n\n/** Every target `deploy` accepts, for error messages and docs. */\nexport const DEPLOY_TARGETS = Object.keys(PRESETS) as DeployTarget[];\n\n/**\n * How to deploy what a given target built, or `null` for a name this rshono does not know — which a `dist/`\n * from a newer version can legitimately carry.\n */\nexport function deployHintFor(target: string): string | null {\n return (PRESETS as Record<string, DeployPreset | undefined>)[target]?.deployHint ?? null;\n}\n\n/** Where a deploy target can be named, in precedence order. */\nexport interface DeploySources {\n /** The `--deploy` flag. */\n flag?: string;\n /** The `RSHONO_DEPLOY` env var — for a CI job that deploys the same app to more than one place. */\n env?: string;\n /** The `deploy` field in `rshono.config.ts`. */\n config?: string;\n}\n\n/**\n * Resolves the preset to build with: the flag wins over the environment, which wins over the config file,\n * which wins over the `node` default. Blank values are ignored at every level, so an unset-but-present\n * `RSHONO_DEPLOY=` in CI falls through rather than failing the build.\n */\nexport function resolveDeployPreset(sources: DeploySources = {}): DeployPreset {\n const target = sources.flag?.trim() || sources.env?.trim() || sources.config?.trim();\n if (!target) return NODE_PRESET;\n\n const preset = (PRESETS as Record<string, DeployPreset | undefined>)[target];\n if (!preset) {\n throw new Error(`[rshono] unknown deploy target ${JSON.stringify(target)} — expected one of: ${DEPLOY_TARGETS.join(', ')}.`);\n }\n return preset;\n}\n"]}
1
+ {"version":3,"file":"presets.js","sourceRoot":"","sources":["../../src/deploy/presets.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAGhE,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AAkExD;;;GAGG;AACH,MAAM,CAAC,MAAM,WAAW,GAAiB;IACvC,IAAI,EAAE,MAAM;IACZ,aAAa,EAAE,wBAAwB;IACvC,UAAU,EAAE,oBAAoB;CACjC,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,iBAAiB,GAAiB;IACtC,IAAI,EAAE,YAAY;IAClB,aAAa,EAAE,8BAA8B;IAC7C,UAAU,EAAE,+BAA+B;IAC3C,WAAW,EAAE,IAAI;IACjB,iBAAiB,EAAE,CAAC,SAAS,CAAC;IAC9B,aAAa,EAAE,CAAC,YAAY,CAAC;IAC7B,eAAe,CAAC,MAAM;QACpB,MAAM,CAAC,MAAM,GAAG,WAAW,CAAC;QAC5B,MAAM,CAAC,aAAa,GAAG,eAAe,CAAC;QACvC,MAAM,CAAC,SAAS,GAAG,CAAC,uBAAuB,CAAC,CAAC;QAC7C,MAAM,CAAC,MAAM,GAAG,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC;IAC3D,CAAC;IACD,QAAQ,EAAE,uBAAuB;CAClC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,SAAS,kBAAkB,CAAC,MAAqB;IAC/C,MAAM,CAAC,SAAS,GAAG,EAAE,CAAC;AACxB,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,aAAa,GAAiB;IAClC,IAAI,EAAE,QAAQ;IACd,aAAa,EAAE,0BAA0B;IACzC,UAAU,EAAE,wCAAwC;IACpD,eAAe,EAAE,kBAAkB;IACnC,QAAQ,EAAE,mBAAmB;CAC9B,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,iBAAiB,GAAiB;IACtC,IAAI,EAAE,YAAY;IAClB,aAAa,EAAE,8BAA8B;IAC7C,UAAU,EAAE,oDAAoD;IAChE,eAAe,EAAE,kBAAkB;CACpC,CAAC;AAEF,MAAM,OAAO,GAAuC;IAClD,IAAI,EAAE,WAAW;IACjB,UAAU,EAAE,iBAAiB;IAC7B,MAAM,EAAE,aAAa;IACrB,YAAY,EAAE,iBAAiB;CAChC,CAAC;AAEF,kEAAkE;AAClE,MAAM,CAAC,MAAM,cAAc,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAmB,CAAC;AAErE;;;;;;;GAOG;AACH,SAAS,SAAS,CAAC,MAAc;IAC/B,OAAO,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,MAAsB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AACtF,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,MAAc;IAC1C,OAAO,SAAS,CAAC,MAAM,CAAC,EAAE,UAAU,IAAI,IAAI,CAAC;AAC/C,CAAC;AAYD;;;;GAIG;AACH,MAAM,UAAU,mBAAmB,CAAC,OAAO,GAAkB,EAAE;IAC7D,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,GAAG,EAAE,IAAI,EAAE,IAAI,OAAO,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC;IACrF,IAAI,CAAC,MAAM;QAAE,OAAO,WAAW,CAAC;IAEhC,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,kCAAkC,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,uBAAuB,cAAc,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IAC/H,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["import type { RspackOptions } from '@rspack/core';\nimport { finalizeCloudflareBuild } from './cloudflare/build.js';\nimport type { Route } from '../router.js';\nimport type { DeployTarget } from './contract.js';\nimport { finalizeVercelBuild } from './vercel/build.js';\n\n/** What a preset's {@link DeployPreset.finalize} is told about the build it is arranging. */\nexport interface DeployBuildContext {\n /** The project root — where `rshono build` ran, and what a platform config file is written next to. */\n rootDir: string;\n /** `<root>/dist`. */\n distDir: string;\n /** The hashed client bundle, `<root>/dist/static` — served at `/_static`. */\n staticDir: string;\n /** The copy of the app's `public/`, or `null` when it has none. Served at the web root. */\n publicDir: string | null;\n /** Prerendered pages, `<root>/dist/ssg` — empty when the app has no `render: 'static'` routes. */\n ssgDir: string;\n /**\n * The app's route table, as the bundle validated it. Read by the two CDN presets: their platform serves\n * the static output before the app is invoked, so a `public/` file that lands on a route's path answers\n * instead of it — see `publicRouteCollisions`.\n */\n routes: readonly Route[];\n}\n\n/**\n * The build-time half of a deploy target: which runtime module the bundle gets, how the server compiler has\n * to change to produce something the platform can run, and how the output is arranged once it exists.\n *\n * The runtime half is `DeployRuntime`, in its own file because it is compiled *into* the app bundle — this\n * side only ever runs in the CLI.\n */\nexport interface DeployPreset {\n readonly name: DeployTarget;\n /**\n * The module `@rshono/deploy` resolves to, as a path relative to the framework's own `dist/`.\n * Slash-separated and split on use, so it stays a valid path on Windows too.\n */\n readonly runtimeModule: string;\n /** How to run what was just built, completing the \"build complete —\" line. */\n readonly deployHint: string;\n /**\n * `true` where the platform hands per-request bindings to `app.fetch(request, env)` — Workers, and\n * nowhere else. It gates the `getRequestContext().env` merge, because every other target passes its own\n * private handles in that argument (`{ incoming, outgoing }` on Node, the whole invocation on Lambda) and\n * merging those would put a live socket, or a request's headers and cookies, behind a name typed\n * `string | undefined`.\n */\n readonly envBindings?: boolean;\n /**\n * Extra resolve conditions for the server bundle, most specific first — what picks the right build of React\n * and the RSC runtime, both of which ship one per runtime. Omit to accept whatever the Rspack target\n * implies, which is correct for Node.\n */\n readonly resolveConditions?: readonly string[];\n /** browserslist-style targets for the server bundle's swc pass. Defaults to Node. */\n readonly syntaxTargets?: readonly string[];\n /**\n * Adjusts the generated server Rspack config for this platform — target, externals policy, output shape.\n * Mutates in place, and runs before the user's `rspack` hook so that hook keeps the last word.\n */\n configureServer?(config: RspackOptions): void;\n /**\n * Arranges the finished build for the platform: assembles the directory layout it expects, emits its config\n * file, and prints how to deploy. Runs last, so everything it needs to move is already on disk.\n */\n finalize?(ctx: DeployBuildContext): Promise<void> | void;\n}\n\n/**\n * Node: a long-lived server process. The generated config is already this shape — `target: 'node'`, the\n * externals policy and ESM chunk output are all defaults — so the preset has nothing to contribute.\n */\nexport const NODE_PRESET: DeployPreset = {\n name: 'node',\n runtimeModule: 'deploy/node/runtime.js',\n deployHint: 'run `rshono start`',\n};\n\n/**\n * Cloudflare Workers: the host owns the process, the CDN owns the assets, and there is no filesystem.\n *\n * Every setting here follows from `workerd` not being Node. Dependencies are bundled because nothing resolves\n * `node_modules` at runtime; `node:` and `cloudflare:` imports stay external because the runtime provides\n * them under `nodejs_compat`; and async chunks are inlined because Wrangler's bundler cannot follow the\n * computed specifier Rspack's ESM chunk loader emits — a split bundle deploys and then fails on first render.\n */\nconst CLOUDFLARE_PRESET: DeployPreset = {\n name: 'cloudflare',\n runtimeModule: 'deploy/cloudflare/runtime.js',\n deployHint: 'deploy with `wrangler deploy`',\n envBindings: true,\n resolveConditions: ['workerd'],\n syntaxTargets: ['chrome 120'],\n configureServer(config) {\n config.target = 'webworker';\n config.externalsType = 'module-import';\n config.externals = [/^(?:node|cloudflare):/];\n config.output = { ...config.output, asyncChunks: false };\n },\n finalize: finalizeCloudflareBuild,\n};\n\n/**\n * Bundles the app's dependencies into the server output instead of importing them from `node_modules`.\n *\n * The generated config externalizes them, which is right where the bundle runs *inside* the project — but a\n * serverless function is uploaded as a directory rather than installed into one, so nothing resolves\n * `node_modules` at request time and an externalized `import 'some-package'` is a cold start that dies on\n * `ERR_MODULE_NOT_FOUND`. Node's own builtins stay external either way, through the `node` externals preset\n * that `target: 'node'` already enables.\n *\n * The cost is a dependency that cannot be bundled — a native addon, or one that reads its own files off disk\n * relative to `__dirname`. Those now fail the build rather than the deploy, which is the same constraint\n * `cloudflare` has always had; the `rspack` hook in `rshono.config.ts` is the way out.\n */\nfunction bundleDependencies(config: RspackOptions): void {\n config.externals = [];\n}\n\n/**\n * Vercel: one Node function behind the platform's CDN, which serves the assets and reaches the function only\n * for a page. `finalize` assembles the Build Output API layout the platform uploads — including the\n * `supportsResponseStreaming` flag, without which Vercel buffers the whole response and silently undoes\n * streamed SSR.\n *\n * Only `dist/server` is uploaded with the function, which is why dependencies are bundled: see\n * {@link bundleDependencies}.\n */\nconst VERCEL_PRESET: DeployPreset = {\n name: 'vercel',\n runtimeModule: 'deploy/vercel/runtime.js',\n deployHint: 'deploy with `vercel deploy --prebuilt`',\n configureServer: bundleDependencies,\n finalize: finalizeVercelBuild,\n};\n\n/**\n * AWS Lambda behind a Function URL in `RESPONSE_STREAM` mode — the AWS shape that keeps streaming. The\n * runtime wraps the app in `awslambda.streamifyResponse`; the buffered alternative would deploy fine and then\n * hold every page until its last byte rendered.\n *\n * The deployment package is `dist/` and nothing else, which is why dependencies are bundled: see\n * {@link bundleDependencies}.\n */\nconst AWS_LAMBDA_PRESET: DeployPreset = {\n name: 'aws-lambda',\n runtimeModule: 'deploy/aws-lambda/runtime.js',\n deployHint: 'zip dist/ with the handler at dist/server/main.mjs',\n configureServer: bundleDependencies,\n};\n\nconst PRESETS: Record<DeployTarget, DeployPreset> = {\n node: NODE_PRESET,\n cloudflare: CLOUDFLARE_PRESET,\n vercel: VERCEL_PRESET,\n 'aws-lambda': AWS_LAMBDA_PRESET,\n};\n\n/** Every target `deploy` accepts, for error messages and docs. */\nexport const DEPLOY_TARGETS = Object.keys(PRESETS) as DeployTarget[];\n\n/**\n * `PRESETS[target]` for a target that really is one.\n *\n * `Object.hasOwn` rather than a bare bracket access, which resolves every `Object.prototype` key —\n * `constructor`, `__proto__`, `toString` — to an inherited value that then passes a truthiness guard. A typo\n * that happens to be one of those used to reach the builder and die on `preset.runtimeModule.split('/')`\n * instead of getting the message written for an unknown target.\n */\nfunction presetFor(target: string): DeployPreset | undefined {\n return Object.hasOwn(PRESETS, target) ? PRESETS[target as DeployTarget] : undefined;\n}\n\n/**\n * How to deploy what a given target built, or `null` for a name this rshono does not know — which a `dist/`\n * from a newer version can legitimately carry.\n */\nexport function deployHintFor(target: string): string | null {\n return presetFor(target)?.deployHint ?? null;\n}\n\n/** Where a deploy target can be named, in precedence order. */\nexport interface DeploySources {\n /** The `--deploy` flag. */\n flag?: string;\n /** The `RSHONO_DEPLOY` env var — for a CI job that deploys the same app to more than one place. */\n env?: string;\n /** The `deploy` field in `rshono.config.ts`. */\n config?: string;\n}\n\n/**\n * Resolves the preset to build with: the flag wins over the environment, which wins over the config file,\n * which wins over the `node` default. Blank values are ignored at every level, so an unset-but-present\n * `RSHONO_DEPLOY=` in CI falls through rather than failing the build.\n */\nexport function resolveDeployPreset(sources: DeploySources = {}): DeployPreset {\n const target = sources.flag?.trim() || sources.env?.trim() || sources.config?.trim();\n if (!target) return NODE_PRESET;\n\n const preset = presetFor(target);\n if (!preset) {\n throw new Error(`[rshono] unknown deploy target ${JSON.stringify(target)} — expected one of: ${DEPLOY_TARGETS.join(', ')}.`);\n }\n return preset;\n}\n"]}
@@ -0,0 +1,42 @@
1
+ import { type Route } from '../router.js';
2
+ /**
3
+ * The request paths a `public/` tree answers, and the collisions with the route table that only show up
4
+ * after a deploy.
5
+ *
6
+ * `DeployRuntime.mountPublicFallback` registers `public/` **after** every route, so it answers only what no
7
+ * route claimed. That is the whole story on `node` and `aws-lambda`, where the app owns the URL surface. On
8
+ * the two targets with a CDN in front, `public/` is part of the static output and the platform answers from
9
+ * it **before** the app is invoked:
10
+ *
11
+ * - **vercel** — `{ handle: 'filesystem' }` sits ahead of the catch-all in `config.json`, by construction.
12
+ * - **cloudflare** — Workers Assets is checked first, which the generated `wrangler.jsonc` says out loud.
13
+ *
14
+ * So an app with `public/about.html` and a page route at `/about.html` renders the page under `rshono dev`
15
+ * and `rshono start`, and serves the file on those two — a behaviour difference discovered by deploying. A
16
+ * `public/index.html` beside a page route at `/` is the same collision, on the one path every app has.
17
+ *
18
+ * The framework cannot reorder either platform, so this is a build-time warning: the build knows both sides
19
+ * and comparing them is a set lookup.
20
+ */
21
+ /** `.html` files are also reachable without the extension where the platform's HTML handling does that. */
22
+ export interface PublicPathOptions {
23
+ /**
24
+ * Whether `public/about.html` also answers `/about`. True on Cloudflare, whose Workers Assets defaults to
25
+ * `auto-trailing-slash` HTML handling; false on Vercel, which serves the file at its own path unless the
26
+ * project turns `cleanUrls` on. Getting this wrong in the permissive direction is a warning about a
27
+ * collision the platform does not have, which is the noise that gets a check ignored.
28
+ */
29
+ htmlExtensionless: boolean;
30
+ }
31
+ /**
32
+ * The literal route paths a `public/` file would answer instead, on a target whose platform serves the
33
+ * static output first — `[]` when there is no `public/` directory or nothing collides.
34
+ *
35
+ * Literal paths only. A `public/` file landing inside a parameterised route's pattern is a real collision
36
+ * too, but deciding it needs the router rather than a set, and the paths every app actually writes files
37
+ * beside — `/`, `/about`, `/favicon.ico` — are literal. The limit is here rather than in the message
38
+ * because a reader of the message can do nothing with it.
39
+ */
40
+ export declare function publicRouteCollisions(publicDir: string | null, routes: readonly Route[], options: PublicPathOptions): string[];
41
+ /** The warning both CDN presets print, so the two say the same thing about the same problem. */
42
+ export declare function warnAboutPublicCollisions(collisions: string[], target: string): void;
@@ -0,0 +1,66 @@
1
+ import { readdirSync } from 'node:fs';
2
+ import { join, posix } from 'node:path';
3
+ import { isPageRoute } from '../router.js';
4
+ /** Every file in a directory tree, as a `/`-separated path relative to its root. */
5
+ function filesUnder(root, prefix = '') {
6
+ const found = [];
7
+ for (const entry of readdirSync(join(root, prefix), { withFileTypes: true })) {
8
+ const rel = prefix ? posix.join(prefix, entry.name) : entry.name;
9
+ if (entry.isDirectory())
10
+ found.push(...filesUnder(root, rel));
11
+ else if (entry.isFile())
12
+ found.push(rel);
13
+ }
14
+ return found;
15
+ }
16
+ /**
17
+ * The request paths one `public/` file is served at — its own, plus the extra ones a platform's HTML
18
+ * handling adds. The same normalisation `ssgFilePath` does for a prerendered page, in the other direction:
19
+ * a check keyed on raw filenames would miss `/` for `index.html` and would be wrong about the rest.
20
+ *
21
+ * Deliberately not exhaustive. It lists what a file is *certainly* reachable at, because a false warning
22
+ * about a route that works is worse than the collision it would have caught — the same asymmetry
23
+ * `assertNothingIsShadowed` keeps.
24
+ */
25
+ function pathsFor(file, options) {
26
+ const paths = [`/${file}`];
27
+ const index = file === 'index.html' ? '' : file.endsWith('/index.html') ? file.slice(0, -'/index.html'.length) : null;
28
+ if (index !== null)
29
+ paths.push(`/${index}`, `/${index}/`);
30
+ else if (options.htmlExtensionless && file.endsWith('.html'))
31
+ paths.push(`/${file.slice(0, -'.html'.length)}`);
32
+ return [...new Set(paths)];
33
+ }
34
+ /**
35
+ * The literal route paths a `public/` file would answer instead, on a target whose platform serves the
36
+ * static output first — `[]` when there is no `public/` directory or nothing collides.
37
+ *
38
+ * Literal paths only. A `public/` file landing inside a parameterised route's pattern is a real collision
39
+ * too, but deciding it needs the router rather than a set, and the paths every app actually writes files
40
+ * beside — `/`, `/about`, `/favicon.ico` — are literal. The limit is here rather than in the message
41
+ * because a reader of the message can do nothing with it.
42
+ */
43
+ export function publicRouteCollisions(publicDir, routes, options) {
44
+ if (publicDir === null)
45
+ return [];
46
+ const claimed = new Map(routes.filter((route) => !route.path.includes(':') && !route.path.includes('*')).map((route) => [route.path, route]));
47
+ const collisions = [];
48
+ for (const file of filesUnder(publicDir)) {
49
+ for (const path of pathsFor(file, options)) {
50
+ const route = claimed.get(path);
51
+ if (route)
52
+ collisions.push(`public/${file} answers ${path}, which ${isPageRoute(route) ? 'a page' : 'an endpoint'} route claims`);
53
+ }
54
+ }
55
+ return collisions;
56
+ }
57
+ /** The warning both CDN presets print, so the two say the same thing about the same problem. */
58
+ export function warnAboutPublicCollisions(collisions, target) {
59
+ if (collisions.length === 0)
60
+ return;
61
+ console.warn(` ⚠ ${collisions.length} public/ file(s) shadow a route on ${target}, where the CDN answers before the app is invoked:`);
62
+ for (const line of collisions)
63
+ console.warn(` ${line}`);
64
+ console.warn(` Under \`rshono dev\` and \`rshono start\` the route wins instead, so this only appears once deployed.`);
65
+ }
66
+ //# sourceMappingURL=public-paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"public-paths.js","sourceRoot":"","sources":["../../src/deploy/public-paths.ts"],"names":[],"mappings":"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","sourcesContent":["import { readdirSync } from 'node:fs';\nimport { join, posix } from 'node:path';\nimport { isPageRoute, type Route } from '../router.js';\n\n/**\n * The request paths a `public/` tree answers, and the collisions with the route table that only show up\n * after a deploy.\n *\n * `DeployRuntime.mountPublicFallback` registers `public/` **after** every route, so it answers only what no\n * route claimed. That is the whole story on `node` and `aws-lambda`, where the app owns the URL surface. On\n * the two targets with a CDN in front, `public/` is part of the static output and the platform answers from\n * it **before** the app is invoked:\n *\n * - **vercel** — `{ handle: 'filesystem' }` sits ahead of the catch-all in `config.json`, by construction.\n * - **cloudflare** — Workers Assets is checked first, which the generated `wrangler.jsonc` says out loud.\n *\n * So an app with `public/about.html` and a page route at `/about.html` renders the page under `rshono dev`\n * and `rshono start`, and serves the file on those two — a behaviour difference discovered by deploying. A\n * `public/index.html` beside a page route at `/` is the same collision, on the one path every app has.\n *\n * The framework cannot reorder either platform, so this is a build-time warning: the build knows both sides\n * and comparing them is a set lookup.\n */\n\n/** `.html` files are also reachable without the extension where the platform's HTML handling does that. */\nexport interface PublicPathOptions {\n /**\n * Whether `public/about.html` also answers `/about`. True on Cloudflare, whose Workers Assets defaults to\n * `auto-trailing-slash` HTML handling; false on Vercel, which serves the file at its own path unless the\n * project turns `cleanUrls` on. Getting this wrong in the permissive direction is a warning about a\n * collision the platform does not have, which is the noise that gets a check ignored.\n */\n htmlExtensionless: boolean;\n}\n\n/** Every file in a directory tree, as a `/`-separated path relative to its root. */\nfunction filesUnder(root: string, prefix = ''): string[] {\n const found: string[] = [];\n for (const entry of readdirSync(join(root, prefix), { withFileTypes: true })) {\n const rel = prefix ? posix.join(prefix, entry.name) : entry.name;\n if (entry.isDirectory()) found.push(...filesUnder(root, rel));\n else if (entry.isFile()) found.push(rel);\n }\n return found;\n}\n\n/**\n * The request paths one `public/` file is served at — its own, plus the extra ones a platform's HTML\n * handling adds. The same normalisation `ssgFilePath` does for a prerendered page, in the other direction:\n * a check keyed on raw filenames would miss `/` for `index.html` and would be wrong about the rest.\n *\n * Deliberately not exhaustive. It lists what a file is *certainly* reachable at, because a false warning\n * about a route that works is worse than the collision it would have caught — the same asymmetry\n * `assertNothingIsShadowed` keeps.\n */\nfunction pathsFor(file: string, options: PublicPathOptions): string[] {\n const paths = [`/${file}`];\n const index = file === 'index.html' ? '' : file.endsWith('/index.html') ? file.slice(0, -'/index.html'.length) : null;\n if (index !== null) paths.push(`/${index}`, `/${index}/`);\n else if (options.htmlExtensionless && file.endsWith('.html')) paths.push(`/${file.slice(0, -'.html'.length)}`);\n return [...new Set(paths)];\n}\n\n/**\n * The literal route paths a `public/` file would answer instead, on a target whose platform serves the\n * static output first — `[]` when there is no `public/` directory or nothing collides.\n *\n * Literal paths only. A `public/` file landing inside a parameterised route's pattern is a real collision\n * too, but deciding it needs the router rather than a set, and the paths every app actually writes files\n * beside — `/`, `/about`, `/favicon.ico` — are literal. The limit is here rather than in the message\n * because a reader of the message can do nothing with it.\n */\nexport function publicRouteCollisions(publicDir: string | null, routes: readonly Route[], options: PublicPathOptions): string[] {\n if (publicDir === null) return [];\n const claimed = new Map(routes.filter((route) => !route.path.includes(':') && !route.path.includes('*')).map((route) => [route.path, route]));\n const collisions: string[] = [];\n for (const file of filesUnder(publicDir)) {\n for (const path of pathsFor(file, options)) {\n const route = claimed.get(path);\n if (route) collisions.push(`public/${file} answers ${path}, which ${isPageRoute(route) ? 'a page' : 'an endpoint'} route claims`);\n }\n }\n return collisions;\n}\n\n/** The warning both CDN presets print, so the two say the same thing about the same problem. */\nexport function warnAboutPublicCollisions(collisions: string[], target: string): void {\n if (collisions.length === 0) return;\n console.warn(` ⚠ ${collisions.length} public/ file(s) shadow a route on ${target}, where the CDN answers before the app is invoked:`);\n for (const line of collisions) console.warn(` ${line}`);\n console.warn(` Under \\`rshono dev\\` and \\`rshono start\\` the route wins instead, so this only appears once deployed.`);\n}\n"]}
@@ -1,3 +1,2 @@
1
1
  import type { DeployBuildContext } from '../presets.js';
2
2
  export declare function finalizeVercelBuild(ctx: DeployBuildContext): Promise<void>;
3
- //# sourceMappingURL=build.d.ts.map
@@ -1,6 +1,7 @@
1
1
  import { cpSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { rm } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
+ import { publicRouteCollisions, warnAboutPublicCollisions } from '../public-paths.js';
4
5
  /**
5
6
  * The Build Output API v3 layout, which `vercel deploy --prebuilt` uploads verbatim — producing it directly
6
7
  * means Vercel runs no build of its own.
@@ -34,6 +35,11 @@ export async function finalizeVercelBuild(ctx) {
34
35
  cpSync(ctx.staticDir, join(staticOut, '_static'), { recursive: true });
35
36
  if (ctx.publicDir)
36
37
  cpSync(ctx.publicDir, staticOut, { recursive: true });
38
+ // `{ handle: 'filesystem' }` is ahead of the function by construction, so a `public/` file on a route's
39
+ // path answers instead of the route here — and only here, which is what makes it worth saying at build
40
+ // time. `htmlExtensionless: false`: Vercel serves `about.html` at `/about.html` and adds `/about` only
41
+ // with `cleanUrls`, which is the project's setting and not something this build can read.
42
+ warnAboutPublicCollisions(publicRouteCollisions(ctx.publicDir, ctx.routes, { htmlExtensionless: false }), 'vercel');
37
43
  // Shipped inside the function: the bundle, and the one thing it reads from disk at request time.
38
44
  //
39
45
  // `public/` is deliberately *not* among them, though it is on the filesystem targets. It went into the
@@ -1 +1 @@
1
- {"version":3,"file":"build.js","sourceRoot":"","sources":["../../../src/deploy/vercel/build.ts"],"names":[],"mappings":"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","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 * The Build Output API v3 layout, which `vercel deploy --prebuilt` uploads verbatim — producing it directly\n * means Vercel runs no build of its own.\n */\nconst OUTPUT_DIR = join('.vercel', 'output');\nconst FUNCTION_DIR = join('functions', 'index.func');\n\n/**\n * The bundle keeps this exact path inside the function because the runtime derives the project root from where\n * it sits (see `deploy/filesystem.ts`), so `dist/ssg` and `dist/public` land where it already looks — and no\n * runtime code needs to know it is on Vercel.\n */\nconst HANDLER = 'dist/server/main.mjs';\n\n/**\n * Routes, in the order Vercel evaluates them: hashed assets get the immutable header the CDN cannot infer,\n * then anything in the static output is served from it, and whatever is left is the app's to render.\n */\nconst ROUTES = [\n { src: '^/_static/(.*)$', headers: { 'cache-control': 'public, max-age=31536000, immutable' }, continue: true },\n { handle: 'filesystem' },\n { src: '/(.*)', dest: '/index' },\n];\n\nexport async function finalizeVercelBuild(ctx: DeployBuildContext): Promise<void> {\n const outputDir = join(ctx.rootDir, OUTPUT_DIR);\n const staticOut = join(outputDir, 'static');\n const functionDir = join(outputDir, FUNCTION_DIR);\n\n // From scratch, so a deleted page or asset cannot survive in the uploaded output.\n await rm(outputDir, { recursive: true, force: true });\n mkdirSync(staticOut, { recursive: true });\n mkdirSync(functionDir, { recursive: true });\n\n // Served by the CDN: the hashed bundle under its public path, and `public/` at the web root.\n cpSync(ctx.staticDir, join(staticOut, '_static'), { recursive: true });\n if (ctx.publicDir) cpSync(ctx.publicDir, staticOut, { recursive: true });\n\n // Shipped inside the function: the bundle, and the one thing it reads from disk at request time.\n //\n // `public/` is deliberately *not* among them, though it is on the filesystem targets. It went into the\n // static output above, and `{ handle: 'filesystem' }` sits ahead of the catch-all route, so the platform\n // answers those paths before the function is invoked — a copy here is bytes uploaded and unpacked on every\n // cold start that nothing can ever read. `vercel/runtime.ts` turns the matching mount off to say so.\n cpSync(join(ctx.distDir, 'server'), join(functionDir, 'dist', 'server'), { recursive: true });\n if (existsSync(ctx.ssgDir)) cpSync(ctx.ssgDir, join(functionDir, 'dist', 'ssg'), { recursive: true });\n\n writeFileSync(\n join(functionDir, '.vc-config.json'),\n `${JSON.stringify(\n {\n // The Node the build ran on, so the function runs the runtime the app was tested against — and so\n // this does not become a hard-coded version the platform retires on a date the framework does not\n // control. A major Vercel does not offer is an explicit error from the deploy rather than a mismatch.\n runtime: `nodejs${process.versions.node.split('.')[0]}.x`,\n handler: HANDLER,\n launcherType: 'Nodejs',\n // Without this the platform buffers the whole response, undoing streamed SSR.\n supportsResponseStreaming: true,\n },\n null,\n 2,\n )}\\n`,\n );\n\n writeFileSync(join(outputDir, 'config.json'), `${JSON.stringify({ version: 3, routes: ROUTES }, null, 2)}\\n`);\n\n console.log(` • assembled ${OUTPUT_DIR} (Build Output API v3)`);\n}\n"]}
1
+ {"version":3,"file":"build.js","sourceRoot":"","sources":["../../../src/deploy/vercel/build.ts"],"names":[],"mappings":"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","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';\nimport { publicRouteCollisions, warnAboutPublicCollisions } from '../public-paths.js';\n\n/**\n * The Build Output API v3 layout, which `vercel deploy --prebuilt` uploads verbatim — producing it directly\n * means Vercel runs no build of its own.\n */\nconst OUTPUT_DIR = join('.vercel', 'output');\nconst FUNCTION_DIR = join('functions', 'index.func');\n\n/**\n * The bundle keeps this exact path inside the function because the runtime derives the project root from where\n * it sits (see `deploy/filesystem.ts`), so `dist/ssg` and `dist/public` land where it already looks — and no\n * runtime code needs to know it is on Vercel.\n */\nconst HANDLER = 'dist/server/main.mjs';\n\n/**\n * Routes, in the order Vercel evaluates them: hashed assets get the immutable header the CDN cannot infer,\n * then anything in the static output is served from it, and whatever is left is the app's to render.\n */\nconst ROUTES = [\n { src: '^/_static/(.*)$', headers: { 'cache-control': 'public, max-age=31536000, immutable' }, continue: true },\n { handle: 'filesystem' },\n { src: '/(.*)', dest: '/index' },\n];\n\nexport async function finalizeVercelBuild(ctx: DeployBuildContext): Promise<void> {\n const outputDir = join(ctx.rootDir, OUTPUT_DIR);\n const staticOut = join(outputDir, 'static');\n const functionDir = join(outputDir, FUNCTION_DIR);\n\n // From scratch, so a deleted page or asset cannot survive in the uploaded output.\n await rm(outputDir, { recursive: true, force: true });\n mkdirSync(staticOut, { recursive: true });\n mkdirSync(functionDir, { recursive: true });\n\n // Served by the CDN: the hashed bundle under its public path, and `public/` at the web root.\n cpSync(ctx.staticDir, join(staticOut, '_static'), { recursive: true });\n if (ctx.publicDir) cpSync(ctx.publicDir, staticOut, { recursive: true });\n\n // `{ handle: 'filesystem' }` is ahead of the function by construction, so a `public/` file on a route's\n // path answers instead of the route here — and only here, which is what makes it worth saying at build\n // time. `htmlExtensionless: false`: Vercel serves `about.html` at `/about.html` and adds `/about` only\n // with `cleanUrls`, which is the project's setting and not something this build can read.\n warnAboutPublicCollisions(publicRouteCollisions(ctx.publicDir, ctx.routes, { htmlExtensionless: false }), 'vercel');\n\n // Shipped inside the function: the bundle, and the one thing it reads from disk at request time.\n //\n // `public/` is deliberately *not* among them, though it is on the filesystem targets. It went into the\n // static output above, and `{ handle: 'filesystem' }` sits ahead of the catch-all route, so the platform\n // answers those paths before the function is invoked — a copy here is bytes uploaded and unpacked on every\n // cold start that nothing can ever read. `vercel/runtime.ts` turns the matching mount off to say so.\n cpSync(join(ctx.distDir, 'server'), join(functionDir, 'dist', 'server'), { recursive: true });\n if (existsSync(ctx.ssgDir)) cpSync(ctx.ssgDir, join(functionDir, 'dist', 'ssg'), { recursive: true });\n\n writeFileSync(\n join(functionDir, '.vc-config.json'),\n `${JSON.stringify(\n {\n // The Node the build ran on, so the function runs the runtime the app was tested against — and so\n // this does not become a hard-coded version the platform retires on a date the framework does not\n // control. A major Vercel does not offer is an explicit error from the deploy rather than a mismatch.\n runtime: `nodejs${process.versions.node.split('.')[0]}.x`,\n handler: HANDLER,\n launcherType: 'Nodejs',\n // Without this the platform buffers the whole response, undoing streamed SSR.\n supportsResponseStreaming: true,\n },\n null,\n 2,\n )}\\n`,\n );\n\n writeFileSync(join(outputDir, 'config.json'), `${JSON.stringify({ version: 3, routes: ROUTES }, null, 2)}\\n`);\n\n console.log(` • assembled ${OUTPUT_DIR} (Build Output API v3)`);\n}\n"]}
@@ -15,4 +15,3 @@ import type { DeployRuntime } from '../contract.js';
15
15
  * they ship inside the function and are read from its read-only disk, exactly as on a server.
16
16
  */
17
17
  export declare const runtime: DeployRuntime;
18
- //# sourceMappingURL=runtime.d.ts.map
package/dist/index.d.ts CHANGED
@@ -23,4 +23,3 @@
23
23
  export { defineRoutes, type EndpointRoute, type EndpointServerModule, type ErrorPageInfo, type ErrorPageProps, type FallbackPage, type HTTPMethod, type PageComponent, type PageProps, type PageRoute, type PathParams, type Route, type RouteConfig, } from './router.js';
24
24
  export { defineConfig, type RshonoConfig, type RspackHookContext } from './config.js';
25
25
  export type { DeployTarget } from './deploy/contract.js';
26
- //# sourceMappingURL=index.d.ts.map
package/dist/router.d.ts CHANGED
@@ -54,9 +54,12 @@ export interface PageProps<Path extends string = string, E extends Env = Env> {
54
54
  * `-Proto`). A fresh instance per request, so mutating it is local to the page; it is not
55
55
  * serializable, so hand a `'use client'` component `url.href` rather than `url`.
56
56
  *
57
- * On a `render: 'static'` route this is the build-time URL — rendered once against `siteUrl`, so
58
- * `url.searchParams` is always empty. Read the query from `useNavigation().url` in a `'use client'`
59
- * component instead, or mark the route `render: 'dynamic'`.
57
+ * On a `render: 'static'` route this is the build-time URL — rendered once against `siteUrl`, so the
58
+ * origin is `siteUrl`'s and `url.searchParams` is always empty, on first paint and after a soft
59
+ * navigation alike. **`useNavigation().url` is the same frozen URL, not a way around it**: the payload
60
+ * carries one `href` and both readings come from it. Mark the route `render: 'dynamic'` if the page
61
+ * depends on the query; a `'use client'` component that only wants it after hydration can read
62
+ * `location.search` in an effect.
60
63
  */
61
64
  url: URL;
62
65
  /** Matched route params for this request, e.g. `{ id: '42' }` for `/profile/:id`. */
@@ -174,6 +177,12 @@ export interface PageRoute {
174
177
  * A parameterised static route without this falls back to rendering per request, with a build
175
178
  * warning. Wildcard (`*`), optional and regex params cannot be prerendered.
176
179
  *
180
+ * **Every value has to be one portable file name**, since that is what a prerendered page is stored as,
181
+ * and the build fails naming the value rather than writing a page nothing will serve. So: no
182
+ * `\ / : * ? " < > |` or control characters, no trailing `.` or space, and not a reserved Windows device
183
+ * name (`CON`, `NUL`, `COM1`, …) — the last two enforced everywhere, so a build that works on macOS is
184
+ * not one that fails in CI on Windows.
185
+ *
177
186
  * @example
178
187
  * ```ts
179
188
  * {
@@ -310,7 +319,14 @@ export interface RouteConfig<TRoutes extends readonly Route[] = readonly Route[]
310
319
  routes: TRoutes;
311
320
  /** Page rendered with a 404 status for unmatched paths and for `notFound()` calls. */
312
321
  notFound?: FallbackPage;
313
- /** Page rendered with a 500 status when a request throws. Receives {@link ErrorPageProps}. */
322
+ /**
323
+ * Page rendered with a 500 status when a request throws — a page component, a page module that will not
324
+ * load, an endpoint, a server action, or middleware. Receives {@link ErrorPageProps}.
325
+ *
326
+ * It is a *fresh* render, with its own flight payload, so it hydrates and behaves like any other page.
327
+ * An app that declares none gets the framework's plain 500 document instead. If the `error` page itself
328
+ * throws, that is reported too and the framework's document answers.
329
+ */
314
330
  error?: FallbackPage;
315
331
  }
316
332
  /**
@@ -411,4 +427,3 @@ type RoutesOf<T> = T extends readonly Route[] ? T : T extends {
411
427
  */
412
428
  export declare function defineRoutes<const T extends readonly Route[] | RouteConfig<readonly Route[]>>(input: T & ValidateInput<T>): RouteConfig<RoutesOf<T>>;
413
429
  export {};
414
- //# sourceMappingURL=router.d.ts.map