@jk2908/solas 0.6.0 → 0.8.0-canary.1791491149398

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 (111) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +137 -40
  3. package/dist/adapters/cloudflare/index.d.ts +2 -0
  4. package/dist/adapters/cloudflare/index.js +2 -0
  5. package/dist/adapters/cloudflare/runtime.d.ts +27 -0
  6. package/dist/adapters/cloudflare/runtime.js +57 -0
  7. package/dist/adapters/cloudflare/vite.d.ts +21 -0
  8. package/dist/adapters/cloudflare/vite.js +146 -0
  9. package/dist/adapters/cloudflare/worker.d.ts +18 -0
  10. package/dist/adapters/cloudflare/worker.js +46 -0
  11. package/dist/adapters/node.d.ts +43 -0
  12. package/dist/adapters/node.js +144 -0
  13. package/dist/config.d.ts +3 -4
  14. package/dist/config.js +12 -12
  15. package/dist/index.js +36 -49
  16. package/dist/internal/browser-router/history.d.ts +9 -9
  17. package/dist/internal/browser-router/history.js +9 -9
  18. package/dist/internal/browser-router/link.d.ts +5 -5
  19. package/dist/internal/browser-router/response-cache.d.ts +8 -8
  20. package/dist/internal/browser-router/response-cache.js +8 -8
  21. package/dist/internal/browser-router/router.js +9 -9
  22. package/dist/internal/browser-router/shared.d.ts +11 -11
  23. package/dist/internal/browser-router/shared.js +2 -2
  24. package/dist/internal/build.d.ts +10 -10
  25. package/dist/internal/build.js +23 -23
  26. package/dist/internal/codegen/config.d.ts +1 -1
  27. package/dist/internal/codegen/config.js +3 -2
  28. package/dist/internal/codegen/environments.d.ts +12 -4
  29. package/dist/internal/codegen/environments.js +28 -10
  30. package/dist/internal/codegen/format.d.ts +1 -1
  31. package/dist/internal/codegen/format.js +1 -1
  32. package/dist/internal/codegen/manifest.d.ts +1 -1
  33. package/dist/internal/codegen/manifest.js +1 -1
  34. package/dist/internal/codegen/maps.d.ts +1 -1
  35. package/dist/internal/codegen/maps.js +1 -1
  36. package/dist/internal/codegen/types.d.ts +1 -1
  37. package/dist/internal/codegen/types.js +1 -1
  38. package/dist/internal/codegen/utils.d.ts +7 -7
  39. package/dist/internal/codegen/utils.js +10 -10
  40. package/dist/internal/env/browser.d.ts +1 -1
  41. package/dist/internal/env/browser.js +1 -1
  42. package/dist/internal/env/flight.d.ts +3 -3
  43. package/dist/internal/env/flight.js +5 -5
  44. package/dist/internal/env/rsc.d.ts +15 -4
  45. package/dist/internal/env/rsc.js +37 -35
  46. package/dist/internal/env/ssr.d.ts +4 -4
  47. package/dist/internal/env/ssr.js +4 -4
  48. package/dist/internal/http-router/create-http-router.d.ts +1 -1
  49. package/dist/internal/http-router/create-http-router.js +1 -1
  50. package/dist/internal/http-router/router.d.ts +8 -7
  51. package/dist/internal/http-router/router.js +23 -24
  52. package/dist/internal/http-router/utils.d.ts +3 -3
  53. package/dist/internal/http-router/utils.js +4 -4
  54. package/dist/internal/metadata.d.ts +5 -5
  55. package/dist/internal/metadata.js +15 -15
  56. package/dist/internal/navigation/http-exception.d.ts +5 -5
  57. package/dist/internal/navigation/http-exception.js +6 -6
  58. package/dist/internal/navigation/redirect-boundary.d.ts +1 -1
  59. package/dist/internal/navigation/redirect-boundary.js +1 -1
  60. package/dist/internal/navigation/redirect.d.ts +5 -5
  61. package/dist/internal/navigation/redirect.js +6 -6
  62. package/dist/internal/postbuild.d.ts +1 -0
  63. package/dist/internal/postbuild.js +37 -28
  64. package/dist/internal/prerender.d.ts +25 -22
  65. package/dist/internal/prerender.js +61 -37
  66. package/dist/internal/public-files.d.ts +1 -1
  67. package/dist/internal/public-files.js +1 -1
  68. package/dist/internal/render/head.d.ts +1 -1
  69. package/dist/internal/render/head.js +3 -3
  70. package/dist/internal/render/tree.d.ts +1 -1
  71. package/dist/internal/render/tree.js +1 -1
  72. package/dist/internal/resolver.d.ts +6 -6
  73. package/dist/internal/resolver.js +12 -12
  74. package/dist/internal/runtimes/assets.d.ts +35 -0
  75. package/dist/internal/runtimes/assets.js +37 -0
  76. package/dist/internal/server/actions.d.ts +4 -4
  77. package/dist/internal/server/actions.js +4 -4
  78. package/dist/internal/server/cookies.d.ts +3 -3
  79. package/dist/internal/server/cookies.js +3 -3
  80. package/dist/internal/server/csrf.d.ts +3 -3
  81. package/dist/internal/server/csrf.js +4 -4
  82. package/dist/internal/server/dynamic.d.ts +3 -3
  83. package/dist/internal/server/dynamic.js +3 -3
  84. package/dist/internal/server/headers.d.ts +2 -2
  85. package/dist/internal/server/headers.js +2 -2
  86. package/dist/internal/server/url.d.ts +2 -2
  87. package/dist/internal/server/url.js +2 -2
  88. package/dist/internal/ui/error-boundary.d.ts +6 -6
  89. package/dist/internal/ui/error-boundary.js +6 -6
  90. package/dist/manifest.d.ts +2 -1
  91. package/dist/manifest.js +13 -37
  92. package/dist/types.d.ts +13 -6
  93. package/dist/utils/base-path.d.ts +3 -3
  94. package/dist/utils/base-path.js +3 -3
  95. package/dist/utils/compress.d.ts +1 -1
  96. package/dist/utils/compress.js +2 -3
  97. package/dist/utils/export-reader.d.ts +11 -10
  98. package/dist/utils/export-reader.js +10 -10
  99. package/dist/utils/hash.d.ts +4 -0
  100. package/dist/utils/hash.js +7 -0
  101. package/dist/utils/logger.d.ts +9 -9
  102. package/dist/utils/logger.js +9 -9
  103. package/package.json +21 -4
  104. package/dist/internal/runtimes/bun.d.ts +0 -9
  105. package/dist/internal/runtimes/bun.js +0 -33
  106. package/dist/internal/runtimes/create.d.ts +0 -4
  107. package/dist/internal/runtimes/create.js +0 -9
  108. package/dist/internal/runtimes/node.d.ts +0 -9
  109. package/dist/internal/runtimes/node.js +0 -31
  110. package/dist/internal/runtimes/runtime.d.ts +0 -29
  111. package/dist/internal/runtimes/runtime.js +0 -32
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ### Features
6
+
7
+ - Added a platform `adapter` option to `solas()`. `solas({ adapter: cloudflare() })` registers the adapter's plugins alongside Solas's own instead of spreading them into the Vite plugins array.
8
+ - Added a Cloudflare adapter for the Cloudflare Vite plugin beta and the `cf` CLI (`cloudflare.config.ts`). `@jk2908/solas/cloudflare/vite` exports `cloudflare()`, and `@jk2908/solas/cloudflare` exports `createWorker()` and `createCloudflareAssets()`.
9
+ - Fully prerendered routes are now written to `dist/static` at their route paths, so a static host can serve them without invoking the server. The Cloudflare adapter copies them to the asset root.
10
+ - Added `@jk2908/solas/node`, a `serve()` helper that runs the built handler on a Node or Bun HTTP server.
11
+ - Request-time reads use an injected `Assets` store with logical `{ namespace, path }` references (`client`, `static`, `artifact`) instead of filesystem paths.
12
+
13
+ ### Breaking
14
+
15
+ - Requires Vite 8 (Rolldown). The `vite` peer is now `^8.0.0`. Use Vite's `resolve.tsconfigPaths` instead of `vite-tsconfig-paths`, which breaks Rolldown's handling of the `file://` imports `@vitejs/plugin-rsc` emits.
16
+ - Removed the `Runtime` facade (`Runtime.set`/`Runtime.reset`) and the `Runtime` export from `@jk2908/solas/env/rsc`.
17
+ - `createHandler` now takes an options object as its fourth argument: `createHandler(config, manifest, importMap, { assets, runtimeManifest })`.
18
+
19
+ ### Fixes
20
+
21
+ - Route modules are now executed in a standalone Vite environment resolved for `serve`, instead of a second Vite server that reloaded the config and cloned the plugin graph.
22
+ - The Cloudflare adapter pre-bundles the Worker environments so workerd never re-optimises dependencies mid-request, which previously split React across two instances in `vite dev`.
23
+
24
+ ## 0.7.0
25
+
26
+ ### Breaking
27
+
28
+ - Removed the `runtime` config option and the Bun/Node runtime adapters. Solas now uses the Node standard library directly, which Bun runs natively, so there is no runtime selection to configure. Generated entries no longer call `createRuntime`.
29
+
3
30
  ## 0.6.0
4
31
 
5
32
  ### Breaking
package/README.md CHANGED
@@ -6,11 +6,15 @@ Solas is experimental and under active development, so expect rough edges.
6
6
 
7
7
  ## Install
8
8
 
9
+ Solas requires **Vite 8** (which uses Rolldown as the bundler).
10
+
9
11
  ```sh
10
- npm install @jk2908/solas react react-dom react-server-dom-webpack vite
11
- npm install -D @vitejs/plugin-react typescript vite-tsconfig-paths
12
+ npm install @jk2908/solas react react-dom react-server-dom-webpack vite@^8
13
+ npm install -D @vitejs/plugin-react@^6 typescript
12
14
  ```
13
15
 
16
+ Use Vite's built-in TypeScript path resolution (`resolve.tsconfigPaths: true`) rather than `vite-tsconfig-paths`, which is not compatible with Rolldown's handling of the `file://` runtime imports the RSC plugin emits.
17
+
14
18
  ## Use
15
19
 
16
20
  Create a Vite config that registers Solas.
@@ -26,6 +30,20 @@ export default defineConfig({
26
30
  })
27
31
  ```
28
32
 
33
+ Platform deployment is configured with the `adapter` option, which registers the platform's plugins alongside Solas's own instead of adding them to the plugins array:
34
+
35
+ ```ts
36
+ import { defineConfig } from 'vite'
37
+
38
+ import solas from '@jk2908/solas'
39
+ import { cloudflare } from '@jk2908/solas/cloudflare/vite'
40
+ import react from '@vitejs/plugin-react'
41
+
42
+ export default defineConfig({
43
+ plugins: [solas({ adapter: cloudflare() }), react()],
44
+ })
45
+ ```
46
+
29
47
  ## Structure
30
48
 
31
49
  Put your routes in `app/`.
@@ -201,16 +219,6 @@ That keeps your route params aligned across links, imperative navigation, metada
201
219
 
202
220
  All Solas options are passed to `solas()` inside `defineConfig`.
203
221
 
204
- ### `runtime`
205
-
206
- Use `runtime` to select the runtime used by Solas server code.
207
-
208
- Supported values are `'auto'`, `'node'`, and `'bun'`.
209
-
210
- `solas()` defaults to `runtime: 'auto'`. In a Bun process, that selects Bun. Otherwise it falls back to Node.
211
-
212
- If you already run Vite through Bun, `runtime: 'auto'` is usually enough and Solas will detect Bun automatically.
213
-
214
222
  ### `url`
215
223
 
216
224
  Use `url` to set the public origin for your app. It is optional.
@@ -517,66 +525,145 @@ export default defineConfig({
517
525
  })
518
526
  ```
519
527
 
520
- ## Runtime
521
-
522
- Solas uses a runtime for filesystem access, mime lookup, and hashing.
528
+ ## Cloudflare
523
529
 
524
- This is not a deployment or packaging adapter. Platform adapters are not available yet.
530
+ Solas ships a Cloudflare adapter that runs the RSC server as a Worker and serves prerender artifacts and client assets through the Cloudflare assets binding.
525
531
 
526
- ### Node runtime
532
+ It targets the **Cloudflare Vite plugin beta** and the **`cf` CLI** workflow, where the Worker is configured by `cloudflare.config.ts`. Install the beta plugin (and `cf` for deploys):
527
533
 
528
- If you want to pin Node explicitly, set `runtime: 'node'`:
534
+ ```sh
535
+ npm install -D @cloudflare/vite-plugin@beta cf
536
+ ```
529
537
 
530
538
  ```ts
539
+ // vite.config.ts
531
540
  import { defineConfig } from 'vite'
532
541
 
533
542
  import solas from '@jk2908/solas'
543
+ import { cloudflare } from '@jk2908/solas/cloudflare/vite'
534
544
  import react from '@vitejs/plugin-react'
535
545
 
536
546
  export default defineConfig({
537
- plugins: [solas({ runtime: 'node' }), react()],
547
+ plugins: [solas({ prerender: 'full', adapter: cloudflare() }), react()],
538
548
  })
539
549
  ```
540
550
 
541
- Use the normal Vite commands with the Node runtime:
551
+ The adapter is passed to `solas()` rather than added to the plugins array directly, so platform wiring stays in one place (the same shape as SvelteKit's `sveltekit({ adapter: adapter() })`).
542
552
 
543
- ```json
553
+ Give the Worker a name and a compatibility date that enables `nodejs_compat`:
554
+
555
+ ```ts
556
+ // cloudflare.config.ts
557
+ import { defineConfig, defineWorker } from 'cf/config'
558
+
559
+ export default defineConfig({
560
+ worker: defineWorker({
561
+ name: 'my-solas-app',
562
+ compatibilityDate: '2026-10-02',
563
+ compatibilityFlags: ['nodejs_compat'],
564
+ }),
565
+ })
566
+ ```
567
+
568
+ > `cloudflare.config.ts` is an open beta and must be loaded with Node 22.18+ (Bun is not supported), so the Cloudflare build and deploy commands run under Node. See the [`cf` documentation](https://developers.cloudflare.com/cf/) for the format and the `cf dev` / `cf build` / `cf deploy` commands.
569
+
570
+ ### Bindings
571
+
572
+ Bindings are declared in `cloudflare.config.ts` and read with the standard Cloudflare global. The adapter does not manage them:
573
+
574
+ ```ts
575
+ // cloudflare.config.ts
576
+ export default defineConfig({
577
+ worker: defineWorker({
578
+ name: 'my-solas-app',
579
+ compatibilityDate: '2026-10-02',
580
+ compatibilityFlags: ['nodejs_compat'],
581
+ env: {
582
+ MY_KV: bindings.kv({ id: '...' }),
583
+ },
584
+ }),
585
+ })
586
+ ```
587
+
588
+ ```tsx
589
+ // server component
590
+ import { env } from 'cloudflare:workers'
591
+
592
+ const value = await env.MY_KV.get('key')
593
+ ```
594
+
595
+ The Cloudflare Vite plugin emulates bindings in `cf dev` and `vite preview`, so there is nothing extra to configure locally.
596
+
597
+ For types, the plugin writes `.cloudflare/types/index.d.ts` (bindings inferred from `cloudflare.config.ts`). Include it in `tsconfig.json` so `cloudflare:workers`'s `env` is typed:
598
+
599
+ ```jsonc
544
600
  {
545
- "scripts": {
546
- "dev": "vite dev",
547
- "build": "vite build",
548
- "preview": "vite preview"
549
- }
601
+ "include": ["app", ".solas/*", ".cloudflare/types"],
550
602
  }
551
603
  ```
552
604
 
553
- ### Bun runtime
605
+ The adapter:
554
606
 
555
- If you want Solas runtime code to execute in Bun, set `runtime: 'bun'`:
607
+ - configures `@cloudflare/vite-plugin` to build the `rsc` environment as the Worker, and injects the generated entrypoint, the `ASSETS` binding, and `notFoundHandling: none` into the Worker config
608
+ - generates the Worker entry (a virtual module) that delegates to the typed `createWorker` helper
609
+ - points Solas's prerender step at the Cloudflare Worker bundle
610
+ - copies `dist/static` (full prerenders at their route paths) to the asset root, so the platform serves them directly, with no Worker invocation
611
+ - copies `dist/.solas` (runtime manifest + ppr artifacts) into the asset output at `/_solas-artifacts`
556
612
 
557
- ```ts
558
- import { defineConfig } from 'vite'
613
+ Because workers have no filesystem, the Worker reads its own output through the `ASSETS` binding. Reads use logical namespaces rather than filesystem paths: `client` and `static` map onto asset-root URLs, and `artifact` maps onto the artifact prefix. Full prerenders are served by the platform; the Worker only handles dynamic routes, RSC navigation, endpoints, and the request-time `artifact` reads (manifest, ppr prelude/postponed). During the build-time prerender pass the Worker is invoked without bindings, so the Node asset store is used and reads/writes artifacts on disk.
559
614
 
560
- import solas from '@jk2908/solas'
561
- import react from '@vitejs/plugin-react'
615
+ `cloudflare()` forwards its `cloudflare` option to `@cloudflare/vite-plugin`. To build a Worker manually, `createWorker` composes the entry behaviour, and `createCloudflareAssets` builds the store directly:
562
616
 
563
- export default defineConfig({
564
- plugins: [solas({ runtime: 'bun' }), react()],
565
- })
617
+ ```ts
618
+ import { createCloudflareAssets } from '@jk2908/solas/cloudflare'
619
+
620
+ const assets = createCloudflareAssets(env)
566
621
  ```
567
622
 
568
- When you use the Bun runtime, run Vite through Bun so the server/runtime code executes in a Bun process:
623
+ ## Node
624
+
625
+ `vite dev` and `vite preview` already run the built handler in Node (via `@vitejs/plugin-rsc`'s default server handler), so nothing extra is needed locally. For a production Node server, use `@jk2908/solas/node`:
626
+
627
+ ```ts
628
+ // server.ts
629
+ import { serve } from '@jk2908/solas/node'
630
+
631
+ await serve()
632
+ ```
569
633
 
570
634
  ```json
571
635
  {
572
636
  "scripts": {
573
- "dev": "bunx --bun vite dev",
574
- "build": "bunx --bun vite build",
575
- "preview": "bunx --bun vite preview"
637
+ "build": "vite build",
638
+ "start": "node server.ts"
576
639
  }
577
640
  }
578
641
  ```
579
642
 
643
+ `serve()` loads the built RSC entry (`dist/rsc/index.js` by default) and forwards requests to its fetch handler, which already serves prerendered routes, client assets, and RSC/SSR responses. Options: `port` (defaults to `PORT` or `3000`), `host`, `outDir`, and `entry`. Run from the project root so the handler can resolve `dist`.
644
+
645
+ Bun and Deno do not need the helper. Their native servers pass the handler a `Request` directly and expect a `Response`, so the Node-specific HTTP-to-Fetch bridging (streams, `duplex`, backpressure) is not needed:
646
+
647
+ ```ts
648
+ // Bun
649
+ import handler from './dist/rsc/index.js'
650
+
651
+ Bun.serve({ fetch: request => handler.fetch(request) })
652
+ ```
653
+
654
+ ```ts
655
+ // Deno
656
+ import handler from './dist/rsc/index.js'
657
+
658
+ Deno.serve(request => handler.fetch(request))
659
+ ```
660
+
661
+ The handler reads `dist` through `node:fs`, which Bun and Deno (2.x, via Node compat) provide. Run from the project root. Bun can also run `@jk2908/solas/node` as-is, since it implements `node:http`.
662
+
663
+ ## Runtime
664
+
665
+ At request time Solas reads its output through an `Assets` interface (`exists`/`readText`/`readBuffer`/`mimeType`) injected into `createHandler`, defaulting to the Node standard library (which Bun runs natively). References are logical (`{ namespace, path }`), where the namespaces are `client` (Vite client output), `static` (full prerenders), and `artifact` (runtime manifest and ppr files). Platforms without a filesystem provide their own store; `@jk2908/solas/cloudflare` builds one around the `ASSETS` binding via `createWorker`. Build-time reads and writes use `node:fs` directly and are not abstracted.
666
+
580
667
  ## Scripts
581
668
 
582
669
  Add scripts to your app:
@@ -591,7 +678,17 @@ Add scripts to your app:
591
678
  }
592
679
  ```
593
680
 
594
- These defaults assume either `runtime: 'node'` or `runtime: 'auto'` while running Vite under Node. If you set `runtime: 'bun'`, or let `runtime: 'auto'` resolve to Bun by running Vite through Bun, use the Bun-backed commands shown in the runtime section above.
681
+ If you run Vite through Bun, use the Bun-backed commands instead:
682
+
683
+ ```json
684
+ {
685
+ "scripts": {
686
+ "dev": "bunx --bun vite dev",
687
+ "build": "bunx --bun vite build",
688
+ "preview": "bunx --bun vite preview"
689
+ }
690
+ }
691
+ ```
595
692
 
596
693
  ## Commands
597
694
 
@@ -0,0 +1,2 @@
1
+ export { ARTIFACT_PREFIX, createCloudflareAssets, type CloudflareAssets, type CloudflareEnv, } from './runtime.js';
2
+ export { createWorker, type CreateWorkerOptions } from './worker.js';
@@ -0,0 +1,2 @@
1
+ export { ARTIFACT_PREFIX, createCloudflareAssets, } from './runtime.js';
2
+ export { createWorker } from './worker.js';
@@ -0,0 +1,27 @@
1
+ import type { Assets } from '../../internal/runtimes/assets.js';
2
+ /**
3
+ * The subset of the Cloudflare `assets` Fetcher binding that Solas needs.
4
+ */
5
+ export type CloudflareAssets = {
6
+ fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
7
+ };
8
+ /**
9
+ * The bindings shape the Cloudflare asset store reads from.
10
+ */
11
+ export type CloudflareEnv = {
12
+ ASSETS?: CloudflareAssets;
13
+ [key: string]: unknown;
14
+ };
15
+ /**
16
+ * Path prefix the adapter copies `dist/.solas` prerender artifacts under in the
17
+ * asset output.
18
+ */
19
+ export declare const ARTIFACT_PREFIX = "_solas-artifacts";
20
+ /**
21
+ * Create a Solas asset store backed by the Cloudflare assets binding.
22
+ *
23
+ * Workers have no writable filesystem, so this is read-only. `client` and
24
+ * `static` files are served from the asset root; `artifact` files are served
25
+ * from the adapter's artifact prefix.
26
+ */
27
+ export declare function createCloudflareAssets(env: CloudflareEnv): Assets;
@@ -0,0 +1,57 @@
1
+ import { getMimeTypeFromPath } from '../../internal/runtimes/mime.js';
2
+ /**
3
+ * Path prefix the adapter copies `dist/.solas` prerender artifacts under in the
4
+ * asset output.
5
+ */
6
+ export const ARTIFACT_PREFIX = '_solas-artifacts';
7
+ // The assets binding matches on pathname only, so this is only used to satisfy
8
+ // the URL constructor
9
+ const ASSET_ORIGIN = 'https://solas-assets.invalid';
10
+ /**
11
+ * Create a Solas asset store backed by the Cloudflare assets binding.
12
+ *
13
+ * Workers have no writable filesystem, so this is read-only. `client` and
14
+ * `static` files are served from the asset root; `artifact` files are served
15
+ * from the adapter's artifact prefix.
16
+ */
17
+ export function createCloudflareAssets(env) {
18
+ const assets = env.ASSETS;
19
+ if (!assets || typeof assets.fetch !== 'function') {
20
+ throw new Error('[solas] createCloudflareAssets requires an `ASSETS` binding. Add an assets binding to your Worker.');
21
+ }
22
+ function toUrl(asset) {
23
+ const path = asset.namespace === 'artifact'
24
+ ? `/${ARTIFACT_PREFIX}/${asset.path}`
25
+ : `/${asset.path}`;
26
+ return new URL(path, ASSET_ORIGIN).toString();
27
+ }
28
+ async function read(asset) {
29
+ const res = await assets.fetch(toUrl(asset), {
30
+ // asset responses should be passed through verbatim
31
+ headers: { 'accept-encoding': 'identity' },
32
+ });
33
+ return res.ok ? res : null;
34
+ }
35
+ return {
36
+ async exists(asset) {
37
+ return (await read(asset)) !== null;
38
+ },
39
+ async readText(asset) {
40
+ const res = await read(asset);
41
+ if (!res) {
42
+ throw new Error(`[solas] asset not found: ${asset.namespace}/${asset.path}`);
43
+ }
44
+ return res.text();
45
+ },
46
+ async readBuffer(asset) {
47
+ const res = await read(asset);
48
+ if (!res) {
49
+ throw new Error(`[solas] asset not found: ${asset.namespace}/${asset.path}`);
50
+ }
51
+ return res.arrayBuffer();
52
+ },
53
+ mimeType(filePath) {
54
+ return getMimeTypeFromPath(filePath);
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,21 @@
1
+ import { cloudflare as cloudflareVitePlugin } from '@cloudflare/vite-plugin';
2
+ import type { Adapter } from '../../types.js';
3
+ /**
4
+ * @see {@link https://developers.cloudflare.com/workers/vite-plugin/}
5
+ */
6
+ type CloudflarePluginConfig = Parameters<typeof cloudflareVitePlugin>[0];
7
+ export type SolasCloudflareOptions = {
8
+ /**
9
+ * Options forwarded to `@cloudflare/vite-plugin`.
10
+ */
11
+ cloudflare?: CloudflarePluginConfig;
12
+ };
13
+ /**
14
+ * Cloudflare adapter for Solas.
15
+ *
16
+ * Wraps `@cloudflare/vite-plugin`, generates the Worker entry that installs the
17
+ * asset-backed store, points Solas's prerender step at the Cloudflare Worker
18
+ * bundle, and copies prerender artifacts into the asset output.
19
+ */
20
+ export declare function cloudflare(options?: SolasCloudflareOptions): Adapter;
21
+ export {};
@@ -0,0 +1,146 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { cloudflare as cloudflareVitePlugin } from '@cloudflare/vite-plugin';
4
+ import * as Config from '../../config.js';
5
+ import { writeCloudflareEntry } from '../../internal/codegen/environments.js';
6
+ import { setRscEntry } from '../../internal/postbuild.js';
7
+ import { ARTIFACT_PREFIX } from './runtime.js';
8
+ const VIRTUAL_ENTRY = 'virtual:solas-cloudflare-entry';
9
+ /**
10
+ * Solas dependencies that are imported at request time from the Worker
11
+ * environment. They must be pre-bundled up front: the Cloudflare dev Worker
12
+ * runs in workerd and does not survive a mid-request dependency
13
+ * re-optimization, which otherwise leaves React split across two instances
14
+ * ("Invalid hook call").
15
+ */
16
+ const WORKER_OPTIMIZE_DEPS = ['path-to-regexp'];
17
+ async function pathExists(filePath) {
18
+ try {
19
+ await fs.access(filePath);
20
+ return true;
21
+ }
22
+ catch {
23
+ return false;
24
+ }
25
+ }
26
+ /**
27
+ * Recursively find directories named `assets` beneath Cloudflare's build output.
28
+ */
29
+ async function findAssetDirs(root, found = []) {
30
+ const entries = await fs.readdir(root, { withFileTypes: true }).catch(() => []);
31
+ for (const entry of entries) {
32
+ if (!entry.isDirectory())
33
+ continue;
34
+ const full = path.join(root, entry.name);
35
+ if (entry.name === 'assets') {
36
+ found.push(full);
37
+ continue;
38
+ }
39
+ await findAssetDirs(full, found);
40
+ }
41
+ return found;
42
+ }
43
+ /**
44
+ * Copy Solas's output into every Cloudflare asset directory:
45
+ * - `dist/static/**` (full prerenders) at the asset root, so the platform serves
46
+ * them at their route paths before the Worker runs
47
+ * - `dist/.solas/**` (runtime manifest + ppr artifacts) under the artifact
48
+ * prefix, read back through the `ASSETS` binding
49
+ */
50
+ async function copyOutput(cwd) {
51
+ const outDir = path.join(cwd, Config.OUT_DIR);
52
+ const artifactSource = path.join(outDir, Config.GENERATED_DIR);
53
+ const staticSource = path.join(outDir, 'static');
54
+ const assetDirs = await findAssetDirs(path.join(cwd, '.cloudflare', 'output'));
55
+ for (const assetDir of assetDirs) {
56
+ if (await pathExists(artifactSource)) {
57
+ await fs.cp(artifactSource, path.join(assetDir, ARTIFACT_PREFIX), {
58
+ recursive: true,
59
+ force: true,
60
+ });
61
+ }
62
+ if (await pathExists(staticSource)) {
63
+ await fs.cp(staticSource, assetDir, { recursive: true, force: true });
64
+ }
65
+ }
66
+ }
67
+ function withAdapterConfig(config) {
68
+ return (workerConfig) => {
69
+ const base = typeof config === 'function' ? (config(workerConfig) ?? {}) : (config ?? {});
70
+ return {
71
+ ...base,
72
+ entrypoint: VIRTUAL_ENTRY,
73
+ assets: { notFoundHandling: 'none', ...base.assets },
74
+ env: { ...base.env, ASSETS: { type: 'assets' } },
75
+ };
76
+ };
77
+ }
78
+ /**
79
+ * Cloudflare adapter for Solas.
80
+ *
81
+ * Wraps `@cloudflare/vite-plugin`, generates the Worker entry that installs the
82
+ * asset-backed store, points Solas's prerender step at the Cloudflare Worker
83
+ * bundle, and copies prerender artifacts into the asset output.
84
+ */
85
+ export function cloudflare(options = {}) {
86
+ const cwd = process.cwd();
87
+ // back the virtual entry id with a path inside the generated dir so the
88
+ // generated entry's relative imports (`./manifest.js`, ...) resolve
89
+ const virtualEntryPath = path.join(cwd, Config.GENERATED_DIR, 'entry.cloudflare.tsx');
90
+ const cfPlugins = cloudflareVitePlugin({
91
+ // Solas runs its server in the `rsc` environment; Cloudflare's worker
92
+ // environment must match so the RSC runtime (and its `ssr` child) is
93
+ // bundled into the Worker
94
+ viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] },
95
+ ...options.cloudflare,
96
+ config: withAdapterConfig(options.cloudflare?.config),
97
+ });
98
+ const adapter = {
99
+ name: 'solas-cloudflare',
100
+ enforce: 'post',
101
+ configResolved(config) {
102
+ // the Cloudflare plugin points the `rsc` environment's build output at
103
+ // the Worker bundle during its `config` hook, so by now we know where
104
+ // Solas's prerender step should import the Worker from
105
+ const rscOutDir = config.environments.rsc?.build.outDir;
106
+ if (rscOutDir) {
107
+ setRscEntry(path.resolve(cwd, rscOutDir, 'index.js'));
108
+ }
109
+ },
110
+ configEnvironment(name, environment) {
111
+ // Pre-bundle the Worker environments so workerd never has to re-optimize
112
+ // dependencies in the middle of a request. Scanning the app directory
113
+ // surfaces any additional request-time dependencies too
114
+ if (name !== 'rsc' && name !== 'ssr')
115
+ return;
116
+ environment.optimizeDeps ??= {};
117
+ environment.optimizeDeps.include = [
118
+ ...new Set([
119
+ ...(environment.optimizeDeps.include ?? []),
120
+ ...WORKER_OPTIMIZE_DEPS,
121
+ ]),
122
+ ];
123
+ environment.optimizeDeps.entries = [
124
+ ...new Set([
125
+ ...(environment.optimizeDeps.entries ?? []),
126
+ `${Config.APP_DIR}/**/*.{tsx,ts,jsx,js}`,
127
+ ]),
128
+ ];
129
+ },
130
+ resolveId(source) {
131
+ return source === VIRTUAL_ENTRY ? virtualEntryPath : null;
132
+ },
133
+ load(id) {
134
+ return id === virtualEntryPath ? writeCloudflareEntry() : null;
135
+ },
136
+ buildApp: {
137
+ order: 'post',
138
+ async handler() {
139
+ // runs after Solas's own `post` buildApp (adapter comes later in the
140
+ // plugin array), so `dist/.solas` artifacts already exist
141
+ await copyOutput(cwd);
142
+ },
143
+ },
144
+ };
145
+ return { name: 'cloudflare', plugins: [adapter, ...cfPlugins] };
146
+ }
@@ -0,0 +1,18 @@
1
+ import type { ImportMap, Manifest, RuntimeConfig } from '../../types.js';
2
+ import { type CloudflareEnv } from './runtime.js';
3
+ export type CreateWorkerOptions = {
4
+ config: RuntimeConfig;
5
+ manifest: Manifest;
6
+ importMap: ImportMap;
7
+ };
8
+ /**
9
+ * Build a Cloudflare Worker around Solas's RSC handler.
10
+ *
11
+ * The asset store and handler depend on `env`, so handlers are cached per `env`
12
+ * (not once per isolate). A failed build is evicted so a transient error does
13
+ * not poison the Worker. During build-time prerender the Worker is invoked
14
+ * without bindings, in which case the Node asset store is used.
15
+ */
16
+ export declare function createWorker(options: CreateWorkerOptions): {
17
+ fetch(request: Request, env?: CloudflareEnv | undefined, _ctx?: unknown): Promise<Response>;
18
+ };
@@ -0,0 +1,46 @@
1
+ import { createHandler, loadManifest } from '../../internal/env/rsc.js';
2
+ import { nodeAssets } from '../../internal/runtimes/assets.js';
3
+ import { createCloudflareAssets } from './runtime.js';
4
+ /**
5
+ * Build a Cloudflare Worker around Solas's RSC handler.
6
+ *
7
+ * The asset store and handler depend on `env`, so handlers are cached per `env`
8
+ * (not once per isolate). A failed build is evicted so a transient error does
9
+ * not poison the Worker. During build-time prerender the Worker is invoked
10
+ * without bindings, in which case the Node asset store is used.
11
+ */
12
+ export function createWorker(options) {
13
+ const handlers = new WeakMap();
14
+ let nodeHandler;
15
+ async function build(assets) {
16
+ const runtimeManifest = await loadManifest(assets);
17
+ return createHandler(options.config, options.manifest, options.importMap, {
18
+ assets,
19
+ runtimeManifest,
20
+ });
21
+ }
22
+ function getHandler(env) {
23
+ if (!env) {
24
+ nodeHandler ??= build(nodeAssets).catch(err => {
25
+ nodeHandler = undefined;
26
+ throw err;
27
+ });
28
+ return nodeHandler;
29
+ }
30
+ let promise = handlers.get(env);
31
+ if (!promise) {
32
+ promise = build(createCloudflareAssets(env)).catch(err => {
33
+ handlers.delete(env);
34
+ throw err;
35
+ });
36
+ handlers.set(env, promise);
37
+ }
38
+ return promise;
39
+ }
40
+ return {
41
+ async fetch(request, env, _ctx) {
42
+ const handler = await getHandler(env);
43
+ return handler.fetch(request);
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,43 @@
1
+ import http from 'node:http';
2
+ export type ServeOptions = {
3
+ /**
4
+ * Port to listen on. Defaults to `process.env.PORT` or `3000`.
5
+ */
6
+ port?: number;
7
+ /**
8
+ * Host to bind to. Defaults to `0.0.0.0`.
9
+ */
10
+ host?: string;
11
+ /**
12
+ * Build output directory the handler reads from. Defaults to `dist`, resolved
13
+ * relative to the current working directory.
14
+ */
15
+ outDir?: string;
16
+ /**
17
+ * Explicit path to the built RSC entry. Defaults to `<outDir>/rsc/index.js`.
18
+ */
19
+ entry?: string;
20
+ /**
21
+ * Called once the server is listening.
22
+ */
23
+ onListen?: (info: {
24
+ port: number;
25
+ host: string;
26
+ }) => void;
27
+ };
28
+ export type Server = http.Server & AsyncDisposable;
29
+ /**
30
+ * Start a production HTTP server for a Node (or Bun) deployment.
31
+ *
32
+ * Loads the built RSC entry (`dist/rsc/index.js` by default) and forwards
33
+ * requests to its fetch handler, which already serves prerendered routes,
34
+ * client assets, and RSC/SSR responses. Run it from the project root so the
35
+ * handler can resolve `dist`. The returned server is `AsyncDisposable`, so it
36
+ * can be released with `await using`.
37
+ *
38
+ * Deno and Bun do not need this helper: use `Deno.serve(handler.fetch)` or
39
+ * `Bun.serve({ fetch: handler.fetch })` directly.
40
+ */
41
+ export declare function serve(options?: ServeOptions): Promise<http.Server<typeof http.IncomingMessage, typeof http.ServerResponse> & {
42
+ [Symbol.asyncDispose](): Promise<void>;
43
+ }>;