@jk2908/solas 0.7.0 → 0.8.0-canary.1791495022678
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +157 -3
- package/dist/adapters/cloudflare/index.d.ts +2 -0
- package/dist/adapters/cloudflare/index.js +2 -0
- package/dist/adapters/cloudflare/runtime.d.ts +34 -0
- package/dist/adapters/cloudflare/runtime.js +75 -0
- package/dist/adapters/cloudflare/vite.d.ts +21 -0
- package/dist/adapters/cloudflare/vite.js +151 -0
- package/dist/adapters/cloudflare/worker.d.ts +18 -0
- package/dist/adapters/cloudflare/worker.js +46 -0
- package/dist/adapters/node.d.ts +43 -0
- package/dist/adapters/node.js +144 -0
- package/dist/config.d.ts +3 -3
- package/dist/config.js +12 -3
- package/dist/index.js +36 -45
- package/dist/internal/browser-router/history.d.ts +9 -9
- package/dist/internal/browser-router/history.js +9 -9
- package/dist/internal/browser-router/link.d.ts +5 -5
- package/dist/internal/browser-router/response-cache.d.ts +8 -8
- package/dist/internal/browser-router/response-cache.js +8 -8
- package/dist/internal/browser-router/router.js +9 -9
- package/dist/internal/browser-router/shared.d.ts +11 -11
- package/dist/internal/browser-router/shared.js +2 -2
- package/dist/internal/build.d.ts +10 -10
- package/dist/internal/build.js +23 -23
- package/dist/internal/codegen/config.d.ts +1 -1
- package/dist/internal/codegen/config.js +4 -2
- package/dist/internal/codegen/environments.d.ts +11 -3
- package/dist/internal/codegen/environments.js +26 -6
- package/dist/internal/codegen/format.d.ts +1 -1
- package/dist/internal/codegen/format.js +1 -1
- package/dist/internal/codegen/manifest.d.ts +1 -1
- package/dist/internal/codegen/manifest.js +1 -1
- package/dist/internal/codegen/maps.d.ts +1 -1
- package/dist/internal/codegen/maps.js +1 -1
- package/dist/internal/codegen/types.d.ts +1 -1
- package/dist/internal/codegen/types.js +1 -1
- package/dist/internal/codegen/utils.d.ts +7 -7
- package/dist/internal/codegen/utils.js +10 -10
- package/dist/internal/env/browser.d.ts +1 -1
- package/dist/internal/env/browser.js +1 -1
- package/dist/internal/env/flight.d.ts +3 -3
- package/dist/internal/env/flight.js +5 -5
- package/dist/internal/env/rsc.d.ts +15 -3
- package/dist/internal/env/rsc.js +37 -34
- package/dist/internal/env/ssr.d.ts +4 -4
- package/dist/internal/env/ssr.js +4 -4
- package/dist/internal/http-router/create-http-router.d.ts +1 -1
- package/dist/internal/http-router/create-http-router.js +1 -1
- package/dist/internal/http-router/router.d.ts +8 -7
- package/dist/internal/http-router/router.js +23 -24
- package/dist/internal/http-router/utils.d.ts +3 -3
- package/dist/internal/http-router/utils.js +4 -4
- package/dist/internal/metadata.d.ts +5 -5
- package/dist/internal/metadata.js +15 -15
- package/dist/internal/navigation/http-exception.d.ts +5 -5
- package/dist/internal/navigation/http-exception.js +6 -6
- package/dist/internal/navigation/redirect-boundary.d.ts +1 -1
- package/dist/internal/navigation/redirect-boundary.js +1 -1
- package/dist/internal/navigation/redirect.d.ts +5 -5
- package/dist/internal/navigation/redirect.js +6 -6
- package/dist/internal/postbuild.d.ts +1 -0
- package/dist/internal/postbuild.js +37 -28
- package/dist/internal/prerender.d.ts +25 -22
- package/dist/internal/prerender.js +61 -37
- package/dist/internal/public-files.d.ts +1 -1
- package/dist/internal/public-files.js +1 -1
- package/dist/internal/render/head.d.ts +1 -1
- package/dist/internal/render/head.js +3 -3
- package/dist/internal/render/tree.d.ts +1 -1
- package/dist/internal/render/tree.js +1 -1
- package/dist/internal/resolver.d.ts +6 -6
- package/dist/internal/resolver.js +12 -12
- package/dist/internal/runtimes/assets.d.ts +35 -0
- package/dist/internal/runtimes/assets.js +37 -0
- package/dist/internal/server/actions.d.ts +4 -4
- package/dist/internal/server/actions.js +4 -4
- package/dist/internal/server/cookies.d.ts +3 -3
- package/dist/internal/server/cookies.js +3 -3
- package/dist/internal/server/csrf.d.ts +3 -3
- package/dist/internal/server/csrf.js +4 -4
- package/dist/internal/server/dynamic.d.ts +3 -3
- package/dist/internal/server/dynamic.js +3 -3
- package/dist/internal/server/headers.d.ts +2 -2
- package/dist/internal/server/headers.js +2 -2
- package/dist/internal/server/url.d.ts +2 -2
- package/dist/internal/server/url.js +2 -2
- package/dist/internal/ui/error-boundary.d.ts +6 -6
- package/dist/internal/ui/error-boundary.js +6 -6
- package/dist/manifest.d.ts +2 -1
- package/dist/manifest.js +13 -37
- package/dist/types.d.ts +11 -0
- package/dist/utils/base-path.d.ts +3 -3
- package/dist/utils/base-path.js +3 -3
- package/dist/utils/compress.d.ts +1 -1
- package/dist/utils/compress.js +2 -3
- package/dist/utils/export-reader.d.ts +11 -10
- package/dist/utils/export-reader.js +10 -10
- package/dist/utils/hash.d.ts +4 -0
- package/dist/utils/hash.js +7 -0
- package/dist/utils/logger.d.ts +9 -9
- package/dist/utils/logger.js +9 -9
- package/package.json +21 -4
- package/dist/internal/runtimes/runtime.d.ts +0 -15
- package/dist/internal/runtimes/runtime.js +0 -37
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
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. The Cloudflare adapter copies them under `/_solas-artifacts` so the Worker serves them.
|
|
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
|
+
- The Cloudflare adapter sets `htmlHandling: none` and stores prerenders under `/_solas-artifacts`, so the asset layer no longer serves a prerendered document at its route path. Previously an RSC navigation (`Accept: text/x-component`) to a fully-prerendered route received HTML and failed to parse ("Connection closed").
|
|
24
|
+
|
|
3
25
|
## 0.7.0
|
|
4
26
|
|
|
5
27
|
### 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
|
|
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,145 @@ 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 `htmlHandling: none` / `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) and `dist/.solas` (runtime manifest + ppr artifacts) into the asset output under `/_solas-artifacts`
|
|
611
|
+
|
|
612
|
+
`htmlHandling: none` stops the asset layer serving a prerendered document at its route path. That matters because the asset layer ignores `Accept`, so an RSC navigation (`Accept: text/x-component`) to a fully-prerendered route would otherwise receive HTML and fail to parse. Routing documents through the Worker lets Solas serve the prerender for `text/html` and render RSC otherwise, while non-HTML assets (JS, CSS, fonts, images) are still served straight from the asset layer.
|
|
613
|
+
|
|
614
|
+
Because workers have no filesystem, the Worker reads its own output through the `ASSETS` binding. Reads use logical namespaces rather than filesystem paths: `client` maps onto asset-root URLs, and `static` (prerenders) and `artifact` (manifest, ppr) map onto the artifact prefix. 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.
|
|
615
|
+
|
|
616
|
+
`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:
|
|
617
|
+
|
|
618
|
+
```ts
|
|
619
|
+
import { createCloudflareAssets } from '@jk2908/solas/cloudflare'
|
|
620
|
+
|
|
621
|
+
const assets = createCloudflareAssets(env)
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
## Node
|
|
625
|
+
|
|
626
|
+
`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`:
|
|
627
|
+
|
|
628
|
+
```ts
|
|
629
|
+
// server.ts
|
|
630
|
+
import { serve } from '@jk2908/solas/node'
|
|
631
|
+
|
|
632
|
+
await serve()
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
```json
|
|
636
|
+
{
|
|
637
|
+
"scripts": {
|
|
638
|
+
"build": "vite build",
|
|
639
|
+
"start": "node server.ts"
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
`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`.
|
|
645
|
+
|
|
646
|
+
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:
|
|
647
|
+
|
|
648
|
+
```ts
|
|
649
|
+
// Bun
|
|
650
|
+
import handler from './dist/rsc/index.js'
|
|
651
|
+
|
|
652
|
+
Bun.serve({ fetch: request => handler.fetch(request) })
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
```ts
|
|
656
|
+
// Deno
|
|
657
|
+
import handler from './dist/rsc/index.js'
|
|
658
|
+
|
|
659
|
+
Deno.serve(request => handler.fetch(request))
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
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`.
|
|
663
|
+
|
|
510
664
|
## Runtime
|
|
511
665
|
|
|
512
|
-
|
|
666
|
+
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
667
|
|
|
514
668
|
## Scripts
|
|
515
669
|
|
|
@@ -0,0 +1,34 @@
|
|
|
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
|
+
* Prerendered documents live under the artifact prefix rather than the asset
|
|
22
|
+
* root. If they sat at their route paths the asset layer would serve them for
|
|
23
|
+
* every request to that path, so an RSC navigation (`Accept:
|
|
24
|
+
* text/x-component`) would receive HTML instead of reaching the Worker.
|
|
25
|
+
*/
|
|
26
|
+
export declare const PRERENDER_PREFIX = "_solas-artifacts/static";
|
|
27
|
+
/**
|
|
28
|
+
* Create a Solas asset store backed by the Cloudflare assets binding.
|
|
29
|
+
*
|
|
30
|
+
* Workers have no writable filesystem, so this is read-only. `client` files are
|
|
31
|
+
* served from the asset root; `static` and `artifact` files are served from the
|
|
32
|
+
* adapter's artifact prefix.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createCloudflareAssets(env: CloudflareEnv): Assets;
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
/**
|
|
8
|
+
* Prerendered documents live under the artifact prefix rather than the asset
|
|
9
|
+
* root. If they sat at their route paths the asset layer would serve them for
|
|
10
|
+
* every request to that path, so an RSC navigation (`Accept:
|
|
11
|
+
* text/x-component`) would receive HTML instead of reaching the Worker.
|
|
12
|
+
*/
|
|
13
|
+
export const PRERENDER_PREFIX = `${ARTIFACT_PREFIX}/static`;
|
|
14
|
+
// The assets binding matches on pathname only, so this is only used to satisfy
|
|
15
|
+
// the URL constructor
|
|
16
|
+
const ASSET_ORIGIN = 'https://solas-assets.invalid';
|
|
17
|
+
/**
|
|
18
|
+
* Create a Solas asset store backed by the Cloudflare assets binding.
|
|
19
|
+
*
|
|
20
|
+
* Workers have no writable filesystem, so this is read-only. `client` files are
|
|
21
|
+
* served from the asset root; `static` and `artifact` files are served from the
|
|
22
|
+
* adapter's artifact prefix.
|
|
23
|
+
*/
|
|
24
|
+
export function createCloudflareAssets(env) {
|
|
25
|
+
const assets = env.ASSETS;
|
|
26
|
+
if (!assets || typeof assets.fetch !== 'function') {
|
|
27
|
+
throw new Error('[solas] createCloudflareAssets requires an `ASSETS` binding. Add an assets binding to your Worker.');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Where each namespace lives in the asset output: `client` files sit at the
|
|
31
|
+
* asset root, prerenders and artifacts under the artifact prefix.
|
|
32
|
+
*/
|
|
33
|
+
function toAssetPath(asset) {
|
|
34
|
+
switch (asset.namespace) {
|
|
35
|
+
case 'artifact':
|
|
36
|
+
return `${ARTIFACT_PREFIX}/${asset.path}`;
|
|
37
|
+
case 'static':
|
|
38
|
+
return `${PRERENDER_PREFIX}/${asset.path}`;
|
|
39
|
+
default:
|
|
40
|
+
return asset.path;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function toUrl(asset) {
|
|
44
|
+
return new URL(`/${toAssetPath(asset)}`, ASSET_ORIGIN).toString();
|
|
45
|
+
}
|
|
46
|
+
async function read(asset) {
|
|
47
|
+
const res = await assets.fetch(toUrl(asset), {
|
|
48
|
+
// asset responses should be passed through verbatim
|
|
49
|
+
headers: { 'accept-encoding': 'identity' },
|
|
50
|
+
});
|
|
51
|
+
return res.ok ? res : null;
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
async exists(asset) {
|
|
55
|
+
return (await read(asset)) !== null;
|
|
56
|
+
},
|
|
57
|
+
async readText(asset) {
|
|
58
|
+
const res = await read(asset);
|
|
59
|
+
if (!res) {
|
|
60
|
+
throw new Error(`[solas] asset not found: ${asset.namespace}/${asset.path}`);
|
|
61
|
+
}
|
|
62
|
+
return res.text();
|
|
63
|
+
},
|
|
64
|
+
async readBuffer(asset) {
|
|
65
|
+
const res = await read(asset);
|
|
66
|
+
if (!res) {
|
|
67
|
+
throw new Error(`[solas] asset not found: ${asset.namespace}/${asset.path}`);
|
|
68
|
+
}
|
|
69
|
+
return res.arrayBuffer();
|
|
70
|
+
},
|
|
71
|
+
mimeType(filePath) {
|
|
72
|
+
return getMimeTypeFromPath(filePath);
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -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,151 @@
|
|
|
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, PRERENDER_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, all under the
|
|
45
|
+
* artifact prefix so the asset layer never answers a document request itself:
|
|
46
|
+
* - `dist/static/**` (full prerenders) at `<prefix>/static`, read back through
|
|
47
|
+
* the `ASSETS` binding so the Worker serves the HTML for `text/html` and
|
|
48
|
+
* renders RSC otherwise
|
|
49
|
+
* - `dist/.solas/**` (runtime manifest + ppr artifacts) at `<prefix>`, read back
|
|
50
|
+
* through the `ASSETS` binding
|
|
51
|
+
*/
|
|
52
|
+
async function copyOutput(cwd) {
|
|
53
|
+
const outDir = path.join(cwd, Config.OUT_DIR);
|
|
54
|
+
const artifactSource = path.join(outDir, Config.GENERATED_DIR);
|
|
55
|
+
const staticSource = path.join(outDir, 'static');
|
|
56
|
+
const assetDirs = await findAssetDirs(path.join(cwd, '.cloudflare', 'output'));
|
|
57
|
+
for (const assetDir of assetDirs) {
|
|
58
|
+
if (await pathExists(artifactSource)) {
|
|
59
|
+
await fs.cp(artifactSource, path.join(assetDir, ARTIFACT_PREFIX), {
|
|
60
|
+
recursive: true,
|
|
61
|
+
force: true,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (await pathExists(staticSource)) {
|
|
65
|
+
await fs.cp(staticSource, path.join(assetDir, PRERENDER_PREFIX), {
|
|
66
|
+
recursive: true,
|
|
67
|
+
force: true,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function withAdapterConfig(config) {
|
|
73
|
+
return (workerConfig) => {
|
|
74
|
+
const base = typeof config === 'function' ? (config(workerConfig) ?? {}) : (config ?? {});
|
|
75
|
+
return {
|
|
76
|
+
...base,
|
|
77
|
+
entrypoint: VIRTUAL_ENTRY,
|
|
78
|
+
assets: { htmlHandling: 'none', notFoundHandling: 'none', ...base.assets },
|
|
79
|
+
env: { ...base.env, ASSETS: { type: 'assets' } },
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Cloudflare adapter for Solas.
|
|
85
|
+
*
|
|
86
|
+
* Wraps `@cloudflare/vite-plugin`, generates the Worker entry that installs the
|
|
87
|
+
* asset-backed store, points Solas's prerender step at the Cloudflare Worker
|
|
88
|
+
* bundle, and copies prerender artifacts into the asset output.
|
|
89
|
+
*/
|
|
90
|
+
export function cloudflare(options = {}) {
|
|
91
|
+
const cwd = process.cwd();
|
|
92
|
+
// back the virtual entry id with a path inside the generated dir so the
|
|
93
|
+
// generated entry's relative imports (`./manifest.js`, ...) resolve
|
|
94
|
+
const virtualEntryPath = path.join(cwd, Config.GENERATED_DIR, 'entry.cloudflare.tsx');
|
|
95
|
+
const cfPlugins = cloudflareVitePlugin({
|
|
96
|
+
// Solas runs its server in the `rsc` environment; Cloudflare's worker
|
|
97
|
+
// environment must match so the RSC runtime (and its `ssr` child) is
|
|
98
|
+
// bundled into the Worker
|
|
99
|
+
viteEnvironment: { name: 'rsc', childEnvironments: ['ssr'] },
|
|
100
|
+
...options.cloudflare,
|
|
101
|
+
config: withAdapterConfig(options.cloudflare?.config),
|
|
102
|
+
});
|
|
103
|
+
const adapter = {
|
|
104
|
+
name: 'solas-cloudflare',
|
|
105
|
+
enforce: 'post',
|
|
106
|
+
configResolved(config) {
|
|
107
|
+
// the Cloudflare plugin points the `rsc` environment's build output at
|
|
108
|
+
// the Worker bundle during its `config` hook, so by now we know where
|
|
109
|
+
// Solas's prerender step should import the Worker from
|
|
110
|
+
const rscOutDir = config.environments.rsc?.build.outDir;
|
|
111
|
+
if (rscOutDir) {
|
|
112
|
+
setRscEntry(path.resolve(cwd, rscOutDir, 'index.js'));
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
configEnvironment(name, environment) {
|
|
116
|
+
// Pre-bundle the Worker environments so workerd never has to re-optimize
|
|
117
|
+
// dependencies in the middle of a request. Scanning the app directory
|
|
118
|
+
// surfaces any additional request-time dependencies too
|
|
119
|
+
if (name !== 'rsc' && name !== 'ssr')
|
|
120
|
+
return;
|
|
121
|
+
environment.optimizeDeps ??= {};
|
|
122
|
+
environment.optimizeDeps.include = [
|
|
123
|
+
...new Set([
|
|
124
|
+
...(environment.optimizeDeps.include ?? []),
|
|
125
|
+
...WORKER_OPTIMIZE_DEPS,
|
|
126
|
+
]),
|
|
127
|
+
];
|
|
128
|
+
environment.optimizeDeps.entries = [
|
|
129
|
+
...new Set([
|
|
130
|
+
...(environment.optimizeDeps.entries ?? []),
|
|
131
|
+
`${Config.APP_DIR}/**/*.{tsx,ts,jsx,js}`,
|
|
132
|
+
]),
|
|
133
|
+
];
|
|
134
|
+
},
|
|
135
|
+
resolveId(source) {
|
|
136
|
+
return source === VIRTUAL_ENTRY ? virtualEntryPath : null;
|
|
137
|
+
},
|
|
138
|
+
load(id) {
|
|
139
|
+
return id === virtualEntryPath ? writeCloudflareEntry() : null;
|
|
140
|
+
},
|
|
141
|
+
buildApp: {
|
|
142
|
+
order: 'post',
|
|
143
|
+
async handler() {
|
|
144
|
+
// runs after Solas's own `post` buildApp (adapter comes later in the
|
|
145
|
+
// plugin array), so `dist/.solas` artifacts already exist
|
|
146
|
+
await copyOutput(cwd);
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
return { name: 'cloudflare', plugins: [adapter, ...cfPlugins] };
|
|
151
|
+
}
|
|
@@ -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
|
+
}>;
|