@jk2908/solas 0.7.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 (105) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +156 -3
  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 -3
  14. package/dist/config.js +12 -3
  15. package/dist/index.js +36 -45
  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 +4 -2
  28. package/dist/internal/codegen/environments.d.ts +11 -3
  29. package/dist/internal/codegen/environments.js +26 -6
  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 -3
  45. package/dist/internal/env/rsc.js +37 -34
  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 +11 -0
  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/runtime.d.ts +0 -15
  105. package/dist/internal/runtimes/runtime.js +0 -37
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
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
+
3
24
  ## 0.7.0
4
25
 
5
26
  ### 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/`.
@@ -507,9 +525,144 @@ export default defineConfig({
507
525
  })
508
526
  ```
509
527
 
528
+ ## Cloudflare
529
+
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.
531
+
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):
533
+
534
+ ```sh
535
+ npm install -D @cloudflare/vite-plugin@beta cf
536
+ ```
537
+
538
+ ```ts
539
+ // vite.config.ts
540
+ import { defineConfig } from 'vite'
541
+
542
+ import solas from '@jk2908/solas'
543
+ import { cloudflare } from '@jk2908/solas/cloudflare/vite'
544
+ import react from '@vitejs/plugin-react'
545
+
546
+ export default defineConfig({
547
+ plugins: [solas({ prerender: 'full', adapter: cloudflare() }), react()],
548
+ })
549
+ ```
550
+
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() })`).
552
+
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
600
+ {
601
+ "include": ["app", ".solas/*", ".cloudflare/types"],
602
+ }
603
+ ```
604
+
605
+ The adapter:
606
+
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`
612
+
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.
614
+
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:
616
+
617
+ ```ts
618
+ import { createCloudflareAssets } from '@jk2908/solas/cloudflare'
619
+
620
+ const assets = createCloudflareAssets(env)
621
+ ```
622
+
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
+ ```
633
+
634
+ ```json
635
+ {
636
+ "scripts": {
637
+ "build": "vite build",
638
+ "start": "node server.ts"
639
+ }
640
+ }
641
+ ```
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
+
510
663
  ## Runtime
511
664
 
512
- This is not a deployment or packaging adapter. Platform adapters are not available yet.
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.
513
666
 
514
667
  ## Scripts
515
668
 
@@ -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
+ }>;