@jk2908/solas 0.1.0
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/LICENSE +21 -0
- package/README.md +333 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +219 -0
- package/dist/error-boundary.d.ts +1 -0
- package/dist/error-boundary.js +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +235 -0
- package/dist/internal/build.d.ts +104 -0
- package/dist/internal/build.js +633 -0
- package/dist/internal/codegen/config.d.ts +5 -0
- package/dist/internal/codegen/config.js +19 -0
- package/dist/internal/codegen/environments.d.ts +12 -0
- package/dist/internal/codegen/environments.js +42 -0
- package/dist/internal/codegen/manifest.d.ts +5 -0
- package/dist/internal/codegen/manifest.js +15 -0
- package/dist/internal/codegen/maps.d.ts +5 -0
- package/dist/internal/codegen/maps.js +75 -0
- package/dist/internal/codegen/utils.d.ts +1 -0
- package/dist/internal/codegen/utils.js +2 -0
- package/dist/internal/env/browser.d.ts +4 -0
- package/dist/internal/env/browser.js +58 -0
- package/dist/internal/env/request-context.d.ts +19 -0
- package/dist/internal/env/request-context.js +2 -0
- package/dist/internal/env/rsc.d.ts +39 -0
- package/dist/internal/env/rsc.js +368 -0
- package/dist/internal/env/ssr.d.ts +42 -0
- package/dist/internal/env/ssr.js +149 -0
- package/dist/internal/env/utils.d.ts +2 -0
- package/dist/internal/env/utils.js +28 -0
- package/dist/internal/metadata.d.ts +81 -0
- package/dist/internal/metadata.js +185 -0
- package/dist/internal/navigation/http-exception-boundary.d.ts +12 -0
- package/dist/internal/navigation/http-exception-boundary.js +48 -0
- package/dist/internal/navigation/http-exception.d.ts +33 -0
- package/dist/internal/navigation/http-exception.js +45 -0
- package/dist/internal/navigation/link.d.ts +13 -0
- package/dist/internal/navigation/link.js +63 -0
- package/dist/internal/navigation/redirect-boundary.d.ts +12 -0
- package/dist/internal/navigation/redirect-boundary.js +39 -0
- package/dist/internal/navigation/redirect.d.ts +21 -0
- package/dist/internal/navigation/redirect.js +63 -0
- package/dist/internal/navigation/use-search-params.d.ts +1 -0
- package/dist/internal/navigation/use-search-params.js +13 -0
- package/dist/internal/prerender.d.ts +151 -0
- package/dist/internal/prerender.js +422 -0
- package/dist/internal/render/head.d.ts +4 -0
- package/dist/internal/render/head.js +38 -0
- package/dist/internal/render/tree.d.ts +47 -0
- package/dist/internal/render/tree.js +108 -0
- package/dist/internal/router/create-router.d.ts +6 -0
- package/dist/internal/router/create-router.js +95 -0
- package/dist/internal/router/pattern.d.ts +8 -0
- package/dist/internal/router/pattern.js +31 -0
- package/dist/internal/router/prefetcher.d.ts +47 -0
- package/dist/internal/router/prefetcher.js +90 -0
- package/dist/internal/router/resolver.d.ts +174 -0
- package/dist/internal/router/resolver.js +356 -0
- package/dist/internal/router/router-context.d.ts +11 -0
- package/dist/internal/router/router-context.js +7 -0
- package/dist/internal/router/router-provider.d.ts +6 -0
- package/dist/internal/router/router-provider.js +131 -0
- package/dist/internal/router/router.d.ts +79 -0
- package/dist/internal/router/router.js +417 -0
- package/dist/internal/router/use-router.d.ts +5 -0
- package/dist/internal/router/use-router.js +5 -0
- package/dist/internal/server/cookies.d.ts +6 -0
- package/dist/internal/server/cookies.js +17 -0
- package/dist/internal/server/dynamic.d.ts +9 -0
- package/dist/internal/server/dynamic.js +22 -0
- package/dist/internal/server/headers.d.ts +5 -0
- package/dist/internal/server/headers.js +19 -0
- package/dist/internal/server/url.d.ts +5 -0
- package/dist/internal/server/url.js +16 -0
- package/dist/internal/ui/defaults/error.d.ts +4 -0
- package/dist/internal/ui/defaults/error.js +6 -0
- package/dist/internal/ui/error-boundary.d.ts +26 -0
- package/dist/internal/ui/error-boundary.js +41 -0
- package/dist/navigation.d.ts +6 -0
- package/dist/navigation.js +6 -0
- package/dist/prerender.d.ts +1 -0
- package/dist/prerender.js +1 -0
- package/dist/router.d.ts +4 -0
- package/dist/router.js +4 -0
- package/dist/server.d.ts +4 -0
- package/dist/server.js +4 -0
- package/dist/solas.d.ts +32 -0
- package/dist/solas.js +125 -0
- package/dist/types.d.ts +93 -0
- package/dist/types.js +1 -0
- package/dist/utils/compress.d.ts +11 -0
- package/dist/utils/compress.js +76 -0
- package/dist/utils/context.d.ts +6 -0
- package/dist/utils/context.js +25 -0
- package/dist/utils/cookies.d.ts +3 -0
- package/dist/utils/cookies.js +35 -0
- package/dist/utils/export-reader.d.ts +29 -0
- package/dist/utils/export-reader.js +117 -0
- package/dist/utils/format.d.ts +6 -0
- package/dist/utils/format.js +72 -0
- package/dist/utils/logger.d.ts +52 -0
- package/dist/utils/logger.js +105 -0
- package/dist/utils/time.d.ts +4 -0
- package/dist/utils/time.js +29 -0
- package/package.json +111 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jerome Kenway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Solas
|
|
2
|
+
|
|
3
|
+
Solas is a minimal React meta-framework powered by Vite, created for experimenting with routing, streaming, and prerendering with React Server Components.
|
|
4
|
+
|
|
5
|
+
It has not been rigorously tested yet (there are currently no automated tests) ... and broken behaviour should be expected.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```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
|
+
```
|
|
13
|
+
|
|
14
|
+
## Use
|
|
15
|
+
|
|
16
|
+
Create a Vite config that registers Solas.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import { defineConfig } from 'vite'
|
|
20
|
+
|
|
21
|
+
import solas from '@jk2908/solas'
|
|
22
|
+
import react from '@vitejs/plugin-react'
|
|
23
|
+
|
|
24
|
+
export default defineConfig({
|
|
25
|
+
plugins: [solas(), react()],
|
|
26
|
+
})
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Structure
|
|
30
|
+
|
|
31
|
+
Put your routes in `app/`.
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
app/
|
|
35
|
+
+layout.tsx
|
|
36
|
+
+page.tsx
|
|
37
|
+
+middleware.ts
|
|
38
|
+
+loading.tsx
|
|
39
|
+
+401.tsx
|
|
40
|
+
+403.tsx
|
|
41
|
+
+404.tsx
|
|
42
|
+
+500.tsx
|
|
43
|
+
about/
|
|
44
|
+
+layout.tsx
|
|
45
|
+
+page.tsx
|
|
46
|
+
api/
|
|
47
|
+
+endpoint.ts
|
|
48
|
+
posts/
|
|
49
|
+
[id]/
|
|
50
|
+
+page.tsx
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Use these filename conventions:
|
|
54
|
+
|
|
55
|
+
- `+layout.tsx`: shared layout for a route branch.
|
|
56
|
+
- `+page.tsx`: page component for a route.
|
|
57
|
+
- `+endpoint.ts`: request handler for non-page routes.
|
|
58
|
+
- `+middleware.ts`: middleware that runs for the current route branch and is inherited by child routes. Parent and child middleware stack together.
|
|
59
|
+
- `+loading.tsx`: loading fallback inherited by child routes.
|
|
60
|
+
- `+401.tsx`: boundary for unauthorised responses in the current route branch and its children.
|
|
61
|
+
- `+403.tsx`: boundary for forbidden responses in the current route branch and its children.
|
|
62
|
+
- `+404.tsx`: boundary for not found responses in the current route branch and its children.
|
|
63
|
+
- `+500.tsx`: boundary for server errors in the current route branch and its children.
|
|
64
|
+
|
|
65
|
+
Nested folders create nested routes. Dynamic segments use `[param]`, and catch-all segments use `[...param]`.
|
|
66
|
+
|
|
67
|
+
Status boundaries follow the same override pattern as layouts: a child route uses the nearest matching boundary file above it, and a more specific boundary replaces a parent one.
|
|
68
|
+
|
|
69
|
+
## Config
|
|
70
|
+
|
|
71
|
+
All Solas options are passed to `solas()` inside `defineConfig`.
|
|
72
|
+
|
|
73
|
+
### `url`
|
|
74
|
+
|
|
75
|
+
Use `url` to tell Solas what the public application origin is.
|
|
76
|
+
|
|
77
|
+
`url` is optional.
|
|
78
|
+
|
|
79
|
+
This happens inside `solas()` when the plugin builds its validated config object. Solas assigns `config.url` with this lookup order:
|
|
80
|
+
|
|
81
|
+
- `config.url`
|
|
82
|
+
- `VITE_APP_URL`
|
|
83
|
+
- `APP_URL`
|
|
84
|
+
|
|
85
|
+
Current behaviour:
|
|
86
|
+
|
|
87
|
+
- Solas reads that value during plugin configuration.
|
|
88
|
+
- Solas injects `APP_URL` and `VITE_APP_URL` into `import.meta.env`.
|
|
89
|
+
- The current Solas runtime does not otherwise require `config.url` for routing, build, or prerender to work.
|
|
90
|
+
|
|
91
|
+
In practice, that means you do not have to pass `url` unless your application code wants a canonical origin value, or you want to standardise that value in config rather than relying on environment variables.
|
|
92
|
+
|
|
93
|
+
If you do want to set it explicitly, this is the shape:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
export default defineConfig(({ mode }) => ({
|
|
97
|
+
plugins: [
|
|
98
|
+
solas({
|
|
99
|
+
url: mode === 'production' ? 'https://example.com' : 'http://localhost:8787',
|
|
100
|
+
}),
|
|
101
|
+
],
|
|
102
|
+
}))
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
If you prefer environment variables, set one of these instead:
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
APP_URL=https://example.com
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
```sh
|
|
112
|
+
VITE_APP_URL=https://example.com
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### `port`
|
|
116
|
+
|
|
117
|
+
Use `port` to change the development server port.
|
|
118
|
+
|
|
119
|
+
Default: `8787`
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
export default defineConfig({
|
|
123
|
+
plugins: [
|
|
124
|
+
solas({
|
|
125
|
+
port: 4000,
|
|
126
|
+
}),
|
|
127
|
+
],
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### `precompress`
|
|
132
|
+
|
|
133
|
+
Use `precompress` to control whether Solas writes compressed build assets.
|
|
134
|
+
|
|
135
|
+
Default: `true`
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
export default defineConfig({
|
|
139
|
+
plugins: [
|
|
140
|
+
solas({
|
|
141
|
+
precompress: false,
|
|
142
|
+
}),
|
|
143
|
+
],
|
|
144
|
+
})
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### `prerender`
|
|
148
|
+
|
|
149
|
+
Use `prerender` to set the default prerender mode for the app. Valid values are `full`, `ppr`, and `false`.
|
|
150
|
+
|
|
151
|
+
Default: `false`
|
|
152
|
+
|
|
153
|
+
- `false`: do not prerender the route.
|
|
154
|
+
- `full`: render the full route to HTML at build time.
|
|
155
|
+
- `ppr`: prerender a static shell and defer dynamic regions to request time.
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
export default defineConfig({
|
|
159
|
+
plugins: [
|
|
160
|
+
solas({
|
|
161
|
+
prerender: 'ppr',
|
|
162
|
+
}),
|
|
163
|
+
],
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
This value is only the default. Route files can override it with `export const prerender = ...`, and the nearest explicit export wins.
|
|
168
|
+
|
|
169
|
+
```ts
|
|
170
|
+
// vite.config.ts
|
|
171
|
+
export default defineConfig({
|
|
172
|
+
plugins: [solas({ prerender: 'full' })],
|
|
173
|
+
})
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
// app/about/+layout.tsx
|
|
178
|
+
export const prerender = 'ppr'
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
```tsx
|
|
182
|
+
// app/about/team/+page.tsx
|
|
183
|
+
export const prerender = false
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
In that example, the app default is `full`, the `about` layout overrides it to `ppr`, and the page overrides it again to `false`.
|
|
187
|
+
|
|
188
|
+
For dynamic routes, prerendering uses the params you export from the page:
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
export const params = () => [{ id: 'post-1' }, { id: 'post-2' }]
|
|
192
|
+
export const prerender = 'full'
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
In `ppr` mode, Solas prerenders the shell and lets you defer parts of the tree to request time.
|
|
196
|
+
|
|
197
|
+
Use `dynamic()` inside a Suspense boundary to mark a subtree as request-time only:
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import { Suspense } from 'react'
|
|
201
|
+
|
|
202
|
+
import { dynamic } from '@jk2908/solas/server'
|
|
203
|
+
|
|
204
|
+
export const prerender = 'ppr'
|
|
205
|
+
|
|
206
|
+
export default function Page() {
|
|
207
|
+
return (
|
|
208
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
209
|
+
<Ts />
|
|
210
|
+
</Suspense>
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
async function Ts() {
|
|
215
|
+
dynamic()
|
|
216
|
+
return <div>{Date.now()}</div>
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
During prerender, `dynamic()` suspends so the nearest Suspense fallback is written into the static shell. At request time, the deferred content resolves normally.
|
|
221
|
+
|
|
222
|
+
If you call `dynamic()` outside `ppr` mode, Solas does not defer that subtree. In `full` mode it logs a warning and the component still renders at build time.
|
|
223
|
+
|
|
224
|
+
`headers()`, `cookies()`, and `url()` also mark the current render path as dynamic, so they should be treated the same way when you are building a `ppr` shell.
|
|
225
|
+
|
|
226
|
+
### `metadata`
|
|
227
|
+
|
|
228
|
+
Use `metadata` to set default document metadata.
|
|
229
|
+
|
|
230
|
+
```ts
|
|
231
|
+
export default defineConfig({
|
|
232
|
+
plugins: [
|
|
233
|
+
solas({
|
|
234
|
+
metadata: {
|
|
235
|
+
title: '%s - Solas',
|
|
236
|
+
meta: [
|
|
237
|
+
{
|
|
238
|
+
name: 'description',
|
|
239
|
+
content: 'My Solas app',
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
},
|
|
243
|
+
}),
|
|
244
|
+
],
|
|
245
|
+
})
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
This is also only the default. Route metadata is merged in order, so config metadata can be extended or overridden by the shell, layouts, page, and status boundaries. The later, more specific route metadata wins for titles and duplicate tags.
|
|
249
|
+
|
|
250
|
+
```tsx
|
|
251
|
+
// vite.config.ts
|
|
252
|
+
solas({
|
|
253
|
+
metadata: {
|
|
254
|
+
title: '%s - Solas',
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
// app/+layout.tsx
|
|
259
|
+
export const metadata = {
|
|
260
|
+
title: 'Docs',
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// app/guides/+page.tsx
|
|
264
|
+
export const metadata = {
|
|
265
|
+
title: 'Routing',
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
In that example, the final page title becomes `Routing - Solas`.
|
|
270
|
+
|
|
271
|
+
### `trailingSlash`
|
|
272
|
+
|
|
273
|
+
Use `trailingSlash` when you want generated routes to end with `/`.
|
|
274
|
+
|
|
275
|
+
Default: `false`
|
|
276
|
+
|
|
277
|
+
```ts
|
|
278
|
+
export default defineConfig({
|
|
279
|
+
plugins: [
|
|
280
|
+
solas({
|
|
281
|
+
trailingSlash: true,
|
|
282
|
+
}),
|
|
283
|
+
],
|
|
284
|
+
})
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### `logger.level`
|
|
288
|
+
|
|
289
|
+
Use `logger.level` to control internal Solas logging.
|
|
290
|
+
|
|
291
|
+
Default: `info`
|
|
292
|
+
|
|
293
|
+
Valid values are `debug`, `info`, `warn`, `error`, and `fatal`.
|
|
294
|
+
|
|
295
|
+
- `debug`: show everything
|
|
296
|
+
- `info`: the default
|
|
297
|
+
- `warn`: only warnings and errors
|
|
298
|
+
- `error`: only errors
|
|
299
|
+
- `fatal`: only fatal errors
|
|
300
|
+
|
|
301
|
+
This is mainly useful when debugging framework behaviour such as routing and prerendering. It is for Solas internals, not your app's general-purpose logging, and it does not control top-level CLI status output such as build and preview progress messages.
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
export default defineConfig({
|
|
305
|
+
plugins: [
|
|
306
|
+
solas({
|
|
307
|
+
logger: {
|
|
308
|
+
level: process.env.NODE_ENV === 'production' ? 'fatal' : 'info',
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
],
|
|
312
|
+
})
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Scripts
|
|
316
|
+
|
|
317
|
+
Add scripts to your app:
|
|
318
|
+
|
|
319
|
+
```json
|
|
320
|
+
{
|
|
321
|
+
"scripts": {
|
|
322
|
+
"dev": "solas dev",
|
|
323
|
+
"build": "solas build",
|
|
324
|
+
"preview": "solas preview"
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
```
|
|
328
|
+
|
|
329
|
+
## Commands
|
|
330
|
+
|
|
331
|
+
- `solas dev` starts the development server.
|
|
332
|
+
- `solas build` creates a production build, prerenders configured routes, and writes compressed assets when enabled.
|
|
333
|
+
- `solas preview` serves the built app for local verification.
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { Solas } from './solas';
|
|
5
|
+
import { Compress } from './utils/compress';
|
|
6
|
+
import { Logger } from './utils/logger';
|
|
7
|
+
import { Prerender } from './internal/prerender';
|
|
8
|
+
const logger = new Logger();
|
|
9
|
+
const DEFAULT_PREVIEW_PORT = 4173;
|
|
10
|
+
async function build() {
|
|
11
|
+
process.env.NODE_ENV = 'production';
|
|
12
|
+
const cwd = process.cwd();
|
|
13
|
+
const manifestPath = path.join(cwd, Solas.Config.GENERATED_DIR, 'build.json');
|
|
14
|
+
// run vite build
|
|
15
|
+
logger.info('[build]', 'running vite build...');
|
|
16
|
+
const vite = Bun.spawnSync(['bunx', '--bun', 'vite', 'build', '--mode', 'production'], {
|
|
17
|
+
cwd,
|
|
18
|
+
stdout: 'inherit',
|
|
19
|
+
stderr: 'inherit',
|
|
20
|
+
env: { ...process.env, NODE_ENV: 'production' },
|
|
21
|
+
});
|
|
22
|
+
if (vite.exitCode !== 0) {
|
|
23
|
+
logger.error('[build] vite build failed');
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
// read build manifest
|
|
27
|
+
let manifest;
|
|
28
|
+
try {
|
|
29
|
+
const raw = await fs.readFile(manifestPath, 'utf-8');
|
|
30
|
+
manifest = JSON.parse(raw);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
logger.error('[build] failed to read build manifest');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
const outDir = path.resolve(cwd, Solas.Config.OUT_DIR);
|
|
37
|
+
const rscDir = path.join(outDir, 'rsc');
|
|
38
|
+
const artifactRoot = Prerender.Artifact.getRootPath(outDir);
|
|
39
|
+
// clear old prerender artifacts so routes that have switched modes
|
|
40
|
+
// do not keep stale metadata from a previous build
|
|
41
|
+
await fs.rm(artifactRoot, { recursive: true, force: true });
|
|
42
|
+
// prerender routes
|
|
43
|
+
if (manifest.prerenderedRoutes.length > 0) {
|
|
44
|
+
const timeout = Prerender.Build.getTimeout();
|
|
45
|
+
const concurrency = Prerender.Build.getConcurrency();
|
|
46
|
+
const artifactManifestRoutes = {};
|
|
47
|
+
logger.info('[prerender]', `prerendering ${manifest.prerenderedRoutes.length} routes (timeout: ${timeout}ms, concurrency: ${concurrency})...`);
|
|
48
|
+
// ensure production mode for React
|
|
49
|
+
process.env.NODE_ENV = 'production';
|
|
50
|
+
const rscEntry = path.join(rscDir, 'index.js');
|
|
51
|
+
const { default: app } = await import(/* @vite-ignore */ rscEntry);
|
|
52
|
+
for await (const result of Prerender.Build.run(app, manifest.prerenderedRoutes, {
|
|
53
|
+
timeout,
|
|
54
|
+
concurrency,
|
|
55
|
+
})) {
|
|
56
|
+
const route = result.route;
|
|
57
|
+
try {
|
|
58
|
+
const routeDir = route === '/' ? '' : route.replace(/^\//, '');
|
|
59
|
+
// folder for this route's build notes/files
|
|
60
|
+
const artifactDir = Prerender.Artifact.getPath(outDir, route);
|
|
61
|
+
if ('error' in result)
|
|
62
|
+
throw result.error;
|
|
63
|
+
if ('status' in result) {
|
|
64
|
+
logger.warn('[prerender]', `skipped ${route}: ${result.status}`);
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
const artifact = result.artifact;
|
|
68
|
+
if (artifact.mode === 'ppr') {
|
|
69
|
+
// for ppr we save the shell now, and the delayed part for later
|
|
70
|
+
await fs.mkdir(artifactDir, { recursive: true });
|
|
71
|
+
const writes = [
|
|
72
|
+
Bun.write(path.join(artifactDir, 'prelude.html'), artifact.html),
|
|
73
|
+
Bun.write(path.join(artifactDir, 'metadata.json'), JSON.stringify({
|
|
74
|
+
schema: artifact.schema,
|
|
75
|
+
route: artifact.route,
|
|
76
|
+
createdAt: artifact.createdAt,
|
|
77
|
+
mode: artifact.mode,
|
|
78
|
+
})),
|
|
79
|
+
];
|
|
80
|
+
if (artifact.postponed !== undefined) {
|
|
81
|
+
writes.push(Bun.write(path.join(artifactDir, 'postponed.json'), JSON.stringify(artifact.postponed)));
|
|
82
|
+
}
|
|
83
|
+
await Promise.all(writes);
|
|
84
|
+
artifactManifestRoutes[route] = {
|
|
85
|
+
mode: artifact.mode,
|
|
86
|
+
createdAt: artifact.createdAt,
|
|
87
|
+
files: artifact.postponed !== undefined
|
|
88
|
+
? ['metadata', 'prelude', 'postponed']
|
|
89
|
+
: ['metadata', 'prelude'],
|
|
90
|
+
};
|
|
91
|
+
logger.info('[prerender:artifacts]', JSON.stringify({
|
|
92
|
+
route,
|
|
93
|
+
prelude: artifact.html,
|
|
94
|
+
postponed: artifact.postponed ?? null,
|
|
95
|
+
metadata: {
|
|
96
|
+
schema: artifact.schema,
|
|
97
|
+
route: artifact.route,
|
|
98
|
+
createdAt: artifact.createdAt,
|
|
99
|
+
mode: artifact.mode,
|
|
100
|
+
},
|
|
101
|
+
}));
|
|
102
|
+
logger.info('[prerender]', `${route} (ppr)`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// @todo: hash files
|
|
106
|
+
// even for full pages, write metadata so preview/runtime knows to serve built html
|
|
107
|
+
await fs.mkdir(artifactDir, { recursive: true });
|
|
108
|
+
await Bun.write(path.join(artifactDir, 'metadata.json'), JSON.stringify({
|
|
109
|
+
schema: artifact.schema,
|
|
110
|
+
route: artifact.route,
|
|
111
|
+
createdAt: artifact.createdAt,
|
|
112
|
+
mode: artifact.mode,
|
|
113
|
+
}));
|
|
114
|
+
const outPath = route === '/'
|
|
115
|
+
? path.join(outDir, 'index.html')
|
|
116
|
+
: path.join(outDir, routeDir, 'index.html');
|
|
117
|
+
await fs.mkdir(path.dirname(outPath), { recursive: true });
|
|
118
|
+
await Bun.write(outPath, artifact.html);
|
|
119
|
+
artifactManifestRoutes[route] = {
|
|
120
|
+
mode: artifact.mode,
|
|
121
|
+
createdAt: artifact.createdAt,
|
|
122
|
+
files: ['metadata', 'html'],
|
|
123
|
+
};
|
|
124
|
+
logger.info('[prerender]', `${route} (full)`);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
logger.error('[prerender]', `failed ${route}: ${err}. This often means unresolved async work (for example external fetches or dynamic rendering in full mode).`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
await fs.mkdir(artifactRoot, { recursive: true });
|
|
131
|
+
await Bun.write(Prerender.Artifact.getManifestPath(outDir), JSON.stringify({
|
|
132
|
+
generatedAt: Date.now(),
|
|
133
|
+
routes: artifactManifestRoutes,
|
|
134
|
+
}));
|
|
135
|
+
}
|
|
136
|
+
// precompress
|
|
137
|
+
if (manifest.precompress) {
|
|
138
|
+
logger.info('[precompress]', 'compressing assets...');
|
|
139
|
+
for await (const { input, compressed } of Compress.run(outDir, {
|
|
140
|
+
filter: f => /\.(js|css|html|svg|json|txt)$/.test(f),
|
|
141
|
+
})) {
|
|
142
|
+
await Bun.write(`${input}.br`, compressed);
|
|
143
|
+
logger.info('[precompress]', `${path.basename(input)}.br`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
// cleanup
|
|
147
|
+
await fs.unlink(manifestPath).catch(() => { });
|
|
148
|
+
logger.info('[build]', 'done');
|
|
149
|
+
}
|
|
150
|
+
async function dev() {
|
|
151
|
+
const proc = Bun.spawn(['bunx', '--bun', 'vite', 'dev'], {
|
|
152
|
+
cwd: process.cwd(),
|
|
153
|
+
stdout: 'inherit',
|
|
154
|
+
stderr: 'inherit',
|
|
155
|
+
stdin: 'inherit',
|
|
156
|
+
env: { ...process.env, NODE_ENV: 'development' },
|
|
157
|
+
});
|
|
158
|
+
await proc.exited;
|
|
159
|
+
}
|
|
160
|
+
async function preview() {
|
|
161
|
+
process.env.NODE_ENV = 'production';
|
|
162
|
+
const cwd = process.cwd();
|
|
163
|
+
const outDir = path.resolve(cwd, Solas.Config.OUT_DIR);
|
|
164
|
+
const rscDir = path.join(outDir, 'rsc');
|
|
165
|
+
const rscEntry = path.join(rscDir, 'index.js');
|
|
166
|
+
const portFlagIndex = args.findIndex(arg => arg === '--port' || arg === '-p');
|
|
167
|
+
const parsedPort = portFlagIndex >= 0 && args[portFlagIndex + 1]
|
|
168
|
+
? Number(args[portFlagIndex + 1])
|
|
169
|
+
: DEFAULT_PREVIEW_PORT;
|
|
170
|
+
if (!Number.isInteger(parsedPort) || parsedPort <= 0 || parsedPort > 65535) {
|
|
171
|
+
logger.error(`[preview] invalid port: ${args[portFlagIndex + 1] ?? 'undefined'}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
// import RSC server (handles prerendered HTML, static assets, and ssr)
|
|
175
|
+
try {
|
|
176
|
+
await fs.access(rscEntry);
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
logger.error(`[preview] missing ${path.relative(cwd, rscEntry)} - run \`${Solas.Config.SLUG} build\` first`);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
const { default: app } = await import(/* @vite-ignore */ rscEntry);
|
|
183
|
+
try {
|
|
184
|
+
Bun.serve({
|
|
185
|
+
port: parsedPort,
|
|
186
|
+
fetch: app.fetch,
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
logger.error(`[preview] failed to start on port ${parsedPort}: ${err}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
logger.info('[preview]', `server running at http://localhost:${parsedPort}`);
|
|
194
|
+
// keep alive
|
|
195
|
+
await new Promise(() => { });
|
|
196
|
+
}
|
|
197
|
+
// cli entry point
|
|
198
|
+
const [, , command, ...args] = process.argv;
|
|
199
|
+
switch (command) {
|
|
200
|
+
case 'build':
|
|
201
|
+
await build();
|
|
202
|
+
break;
|
|
203
|
+
case 'dev':
|
|
204
|
+
await dev();
|
|
205
|
+
break;
|
|
206
|
+
case 'preview':
|
|
207
|
+
await preview();
|
|
208
|
+
break;
|
|
209
|
+
default:
|
|
210
|
+
console.log(`
|
|
211
|
+
${Solas.Config.NAME} - cli
|
|
212
|
+
|
|
213
|
+
Commands:
|
|
214
|
+
build Build for production (vite build + prerender + compress)
|
|
215
|
+
dev Start development server
|
|
216
|
+
preview Preview production build (serves prerendered HTML with SSR fallback)
|
|
217
|
+
`);
|
|
218
|
+
process.exit(command ? 1 : 0);
|
|
219
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ErrorBoundary } from './internal/ui/error-boundary';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ErrorBoundary } from './internal/ui/error-boundary';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { PluginOption } from 'vite';
|
|
2
|
+
import type { PluginConfig } from './types';
|
|
3
|
+
declare function solas(c: PluginConfig): PluginOption[];
|
|
4
|
+
export default solas;
|
|
5
|
+
export { Solas } from './solas';
|
|
6
|
+
export type * from './solas.d.ts';
|
|
7
|
+
export type * from './types';
|