@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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +333 -0
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +219 -0
  5. package/dist/error-boundary.d.ts +1 -0
  6. package/dist/error-boundary.js +1 -0
  7. package/dist/index.d.ts +7 -0
  8. package/dist/index.js +235 -0
  9. package/dist/internal/build.d.ts +104 -0
  10. package/dist/internal/build.js +633 -0
  11. package/dist/internal/codegen/config.d.ts +5 -0
  12. package/dist/internal/codegen/config.js +19 -0
  13. package/dist/internal/codegen/environments.d.ts +12 -0
  14. package/dist/internal/codegen/environments.js +42 -0
  15. package/dist/internal/codegen/manifest.d.ts +5 -0
  16. package/dist/internal/codegen/manifest.js +15 -0
  17. package/dist/internal/codegen/maps.d.ts +5 -0
  18. package/dist/internal/codegen/maps.js +75 -0
  19. package/dist/internal/codegen/utils.d.ts +1 -0
  20. package/dist/internal/codegen/utils.js +2 -0
  21. package/dist/internal/env/browser.d.ts +4 -0
  22. package/dist/internal/env/browser.js +58 -0
  23. package/dist/internal/env/request-context.d.ts +19 -0
  24. package/dist/internal/env/request-context.js +2 -0
  25. package/dist/internal/env/rsc.d.ts +39 -0
  26. package/dist/internal/env/rsc.js +368 -0
  27. package/dist/internal/env/ssr.d.ts +42 -0
  28. package/dist/internal/env/ssr.js +149 -0
  29. package/dist/internal/env/utils.d.ts +2 -0
  30. package/dist/internal/env/utils.js +28 -0
  31. package/dist/internal/metadata.d.ts +81 -0
  32. package/dist/internal/metadata.js +185 -0
  33. package/dist/internal/navigation/http-exception-boundary.d.ts +12 -0
  34. package/dist/internal/navigation/http-exception-boundary.js +48 -0
  35. package/dist/internal/navigation/http-exception.d.ts +33 -0
  36. package/dist/internal/navigation/http-exception.js +45 -0
  37. package/dist/internal/navigation/link.d.ts +13 -0
  38. package/dist/internal/navigation/link.js +63 -0
  39. package/dist/internal/navigation/redirect-boundary.d.ts +12 -0
  40. package/dist/internal/navigation/redirect-boundary.js +39 -0
  41. package/dist/internal/navigation/redirect.d.ts +21 -0
  42. package/dist/internal/navigation/redirect.js +63 -0
  43. package/dist/internal/navigation/use-search-params.d.ts +1 -0
  44. package/dist/internal/navigation/use-search-params.js +13 -0
  45. package/dist/internal/prerender.d.ts +151 -0
  46. package/dist/internal/prerender.js +422 -0
  47. package/dist/internal/render/head.d.ts +4 -0
  48. package/dist/internal/render/head.js +38 -0
  49. package/dist/internal/render/tree.d.ts +47 -0
  50. package/dist/internal/render/tree.js +108 -0
  51. package/dist/internal/router/create-router.d.ts +6 -0
  52. package/dist/internal/router/create-router.js +95 -0
  53. package/dist/internal/router/pattern.d.ts +8 -0
  54. package/dist/internal/router/pattern.js +31 -0
  55. package/dist/internal/router/prefetcher.d.ts +47 -0
  56. package/dist/internal/router/prefetcher.js +90 -0
  57. package/dist/internal/router/resolver.d.ts +174 -0
  58. package/dist/internal/router/resolver.js +356 -0
  59. package/dist/internal/router/router-context.d.ts +11 -0
  60. package/dist/internal/router/router-context.js +7 -0
  61. package/dist/internal/router/router-provider.d.ts +6 -0
  62. package/dist/internal/router/router-provider.js +131 -0
  63. package/dist/internal/router/router.d.ts +79 -0
  64. package/dist/internal/router/router.js +417 -0
  65. package/dist/internal/router/use-router.d.ts +5 -0
  66. package/dist/internal/router/use-router.js +5 -0
  67. package/dist/internal/server/cookies.d.ts +6 -0
  68. package/dist/internal/server/cookies.js +17 -0
  69. package/dist/internal/server/dynamic.d.ts +9 -0
  70. package/dist/internal/server/dynamic.js +22 -0
  71. package/dist/internal/server/headers.d.ts +5 -0
  72. package/dist/internal/server/headers.js +19 -0
  73. package/dist/internal/server/url.d.ts +5 -0
  74. package/dist/internal/server/url.js +16 -0
  75. package/dist/internal/ui/defaults/error.d.ts +4 -0
  76. package/dist/internal/ui/defaults/error.js +6 -0
  77. package/dist/internal/ui/error-boundary.d.ts +26 -0
  78. package/dist/internal/ui/error-boundary.js +41 -0
  79. package/dist/navigation.d.ts +6 -0
  80. package/dist/navigation.js +6 -0
  81. package/dist/prerender.d.ts +1 -0
  82. package/dist/prerender.js +1 -0
  83. package/dist/router.d.ts +4 -0
  84. package/dist/router.js +4 -0
  85. package/dist/server.d.ts +4 -0
  86. package/dist/server.js +4 -0
  87. package/dist/solas.d.ts +32 -0
  88. package/dist/solas.js +125 -0
  89. package/dist/types.d.ts +93 -0
  90. package/dist/types.js +1 -0
  91. package/dist/utils/compress.d.ts +11 -0
  92. package/dist/utils/compress.js +76 -0
  93. package/dist/utils/context.d.ts +6 -0
  94. package/dist/utils/context.js +25 -0
  95. package/dist/utils/cookies.d.ts +3 -0
  96. package/dist/utils/cookies.js +35 -0
  97. package/dist/utils/export-reader.d.ts +29 -0
  98. package/dist/utils/export-reader.js +117 -0
  99. package/dist/utils/format.d.ts +6 -0
  100. package/dist/utils/format.js +72 -0
  101. package/dist/utils/logger.d.ts +52 -0
  102. package/dist/utils/logger.js +105 -0
  103. package/dist/utils/time.d.ts +4 -0
  104. package/dist/utils/time.js +29 -0
  105. 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ export {};
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';
@@ -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';