@jk2908/solas 0.4.4 → 0.5.1

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 CHANGED
@@ -1,5 +1,24 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.1 - 2026-06-06
4
+
5
+ - Fixed production app bundling by removing the `mime-types` package dependency from Solas runtime paths, preventing client bundles from resolving `/node_modules/mime-types/*` imports.
6
+ - Added an internal `getMimeTypeFromPath(...)` helper for Node runtime MIME resolution with a safe `application/octet-stream` fallback for unknown extensions.
7
+ - Removed obsolete `src/adapters/*` runtime adapter files so runtime selection now consistently uses the internal runtime implementations.
8
+
9
+ ## 0.5.0 - 2026-06-06
10
+
11
+ - Added runtime selection via `runtime: 'auto' | 'node' | 'bun'`, with `auto` choosing Bun when available and falling back to Node otherwise.
12
+ - Removed the default Bun runtime requirement from the documented workflow. Standard `vite dev`, `vite build`, and `vite preview` commands now work on the default Node path, while Bun-backed Vite commands remain available when you want to run Solas in Bun.
13
+ - Added a dedicated `@jk2908/solas/$` runtime-safe export for generated/runtime code so preview and production server bundles no longer need to pull through the package root plugin entry.
14
+ - Fixed prerender static param resolution to stop depending on host support for `Promise.try`, so build-time route processing now works correctly under Node-based Vite runs.
15
+
16
+ ## 0.4.5 - 2026-05-29
17
+
18
+ - Documented concrete `href` and `router.go(...)` usage in the README, including how explicit `query` values merge with an existing query string and take precedence for duplicate keys.
19
+ - Clarified in the README that `router.go(...)` and `router.refresh()` are awaitable, and that `router.refresh()` always refreshes the current browser location at call time.
20
+ - Fixed browser-router typing so `refresh` is exposed as a promise-returning method, matching the runtime implementation.
21
+
3
22
  ## 0.4.4 - 2026-05-29
4
23
 
5
24
  - Added `router.refresh()` to the browser router, and made it clear that it clears the current route cache before fetching a fresh RSC payload.
package/README.md CHANGED
@@ -4,8 +4,6 @@ Solas is a minimal React meta-framework powered by Vite, created for experimenti
4
4
 
5
5
  Solas is experimental and currently has no automated test suite, so expect rough edges.
6
6
 
7
- Solas currently requires Bun 1.2+ on your `PATH`. You can still manage dependencies with `npm`, `pnpm`, or `yarn`, but the Solas CLI and Vite plugin runtime use Bun APIs directly.
8
-
9
7
  ## Install
10
8
 
11
9
  ```sh
@@ -100,6 +98,24 @@ That gives you:
100
98
  - rejected params for static routes that do not accept them
101
99
  - typed query and navigation options on `router.go(...)`
102
100
 
101
+ If you already have a concrete path, you can pass that directly instead of using route params:
102
+
103
+ ```tsx
104
+ ;<Link href={`/p/${post.id}`} />
105
+
106
+ await router.go(`/p/${post.id}`)
107
+ ```
108
+
109
+ In that form the path is already resolved, so `params` are not used. Typed `Link` and `router.go(...)` only allow `params` when you pass a route pattern like `/p/:id`.
110
+
111
+ If the target already contains a query string and you also pass `query`, Solas merges them. Existing query entries are kept unless you override the same key in `query`, and explicit `query` values win:
112
+
113
+ ```tsx
114
+ <Link href={`/p/${post.id}?tab=summary`} query={{ draft: true, tab: 'full' }} />
115
+ ```
116
+
117
+ That resolves to `/p/${post.id}?tab=full&draft=true`.
118
+
103
119
  Use `Link` for same-origin app navigation. Prefetching is opt-in:
104
120
 
105
121
  ```tsx
@@ -123,7 +139,7 @@ export function Controls() {
123
139
 
124
140
  return (
125
141
  <>
126
- <button type="button" onClick={() => void router.go('/posts')}>
142
+ <button type="button" onClick={() => router.go('/posts')}>
127
143
  Go to posts
128
144
  </button>
129
145
 
@@ -131,7 +147,7 @@ export function Controls() {
131
147
  Prefetch posts
132
148
  </button>
133
149
 
134
- <button type="button" onClick={() => void router.refresh()}>
150
+ <button type="button" onClick={() => router.refresh()}>
135
151
  Refresh current route
136
152
  </button>
137
153
  </>
@@ -139,6 +155,18 @@ export function Controls() {
139
155
  }
140
156
  ```
141
157
 
158
+ `router.go(...)` and `router.refresh()` both return promises, so you can `await` either of them when you need to sequence work after navigation completes:
159
+
160
+ ```tsx
161
+ const finalPath = await router.go('/posts')
162
+ ```
163
+
164
+ ```tsx
165
+ await router.refresh()
166
+ ```
167
+
168
+ `router.refresh()` always refreshes the current browser location at the moment you call it. If you call it after `await router.go('/posts')`, it refreshes `/posts` (or the final redirected path).
169
+
142
170
  `router.go(...)` accepts route params and query values using the same typed route rules as `Link`:
143
171
 
144
172
  ```tsx
@@ -173,6 +201,74 @@ That keeps your route params aligned across links, imperative navigation, metada
173
201
 
174
202
  All Solas options are passed to `solas()` inside `defineConfig`.
175
203
 
204
+ ## Runtime
205
+
206
+ Solas uses a runtime for filesystem access, mime lookup, and hashing.
207
+
208
+ This is not a deployment or packaging adapter. Platform adapters are not available yet.
209
+
210
+ Use the `runtime` config key to select the runtime used by Solas server code.
211
+
212
+ Supported values are `'auto'`, `'node'`, and `'bun'`.
213
+
214
+ `solas()` defaults to `runtime: 'auto'`. In a Bun process, that selects Bun. Otherwise it falls back to Node.
215
+
216
+ If you already run Vite through Bun, `runtime: 'auto'` is usually enough and Solas will detect Bun automatically.
217
+
218
+ ### Node runtime
219
+
220
+ If you want to pin Node explicitly, set `runtime: 'node'`:
221
+
222
+ ```ts
223
+ import { defineConfig } from 'vite'
224
+
225
+ import solas from '@jk2908/solas'
226
+ import react from '@vitejs/plugin-react'
227
+
228
+ export default defineConfig({
229
+ plugins: [solas({ runtime: 'node' }), react()],
230
+ })
231
+ ```
232
+
233
+ Use the normal Vite commands with the Node runtime:
234
+
235
+ ```json
236
+ {
237
+ "scripts": {
238
+ "dev": "vite dev",
239
+ "build": "vite build",
240
+ "preview": "vite preview"
241
+ }
242
+ }
243
+ ```
244
+
245
+ ### Bun runtime
246
+
247
+ If you want Solas runtime code to execute in Bun, set `runtime: 'bun'`:
248
+
249
+ ```ts
250
+ import { defineConfig } from 'vite'
251
+
252
+ import solas from '@jk2908/solas'
253
+ import react from '@vitejs/plugin-react'
254
+
255
+ export default defineConfig({
256
+ plugins: [solas({ runtime: 'bun' }), react()],
257
+ })
258
+ ```
259
+
260
+ When you use the Bun runtime, run Vite through Bun so the server/runtime code executes in a Bun process:
261
+
262
+ ```json
263
+ {
264
+ "scripts": {
265
+ "dev": "bunx --bun vite dev",
266
+ "build": "bunx --bun vite build",
267
+ "preview": "bunx --bun vite preview"
268
+ }
269
+ }
270
+ ```
271
+
176
272
  ### `url`
177
273
 
178
274
  `url` is optional. If you set it, Solas treats it as the public origin for your app.
@@ -492,18 +588,20 @@ Add scripts to your app:
492
588
  ```json
493
589
  {
494
590
  "scripts": {
495
- "dev": "bunx --bun vite dev",
496
- "build": "bunx --bun vite build",
497
- "preview": "bunx --bun vite preview"
591
+ "dev": "vite dev",
592
+ "build": "vite build",
593
+ "preview": "vite preview"
498
594
  }
499
595
  }
500
596
  ```
501
597
 
598
+ These defaults assume either `runtime: 'node'` or `runtime: 'auto'` while running Vite under Node. If you set `runtime: 'bun'`, or let `runtime: 'auto'` resolve to Bun by running Vite through Bun, use the Bun-backed commands shown in the runtime section above.
599
+
502
600
  ## Commands
503
601
 
504
- - `bunx --bun vite dev` starts the development server.
505
- - `bunx --bun vite build` creates a production build. Solas finalizes that build by prerendering configured routes, writing the runtime manifest, generating `sitemap.xml` when enabled, and precompressing output when enabled.
506
- - `bunx --bun vite preview` serves the built app for local verification.
602
+ - `vite dev` starts the development server.
603
+ - `vite build` creates a production build. Solas finalizes that build by prerendering configured routes, writing the runtime manifest, generating `sitemap.xml` when enabled, and precompressing output when enabled.
604
+ - `vite preview` serves the built app for local verification.
507
605
 
508
606
  ## Security
509
607
 
package/dist/index.js CHANGED
@@ -14,19 +14,26 @@ import { writeMaps } from './internal/codegen/maps.js';
14
14
  import { writeTypes } from './internal/codegen/types.js';
15
15
  import { postbuild } from './internal/postbuild.js';
16
16
  import { collect as collectPublicFiles } from './internal/public-files.js';
17
+ import { Runtime } from './internal/runtimes/runtime.js';
17
18
  import { Solas } from './solas.js';
18
19
  const DEFAULT_CONFIG = {
20
+ runtime: 'auto',
19
21
  precompress: false,
20
22
  prerender: false,
21
23
  trustedOrigins: [],
22
24
  trailingSlash: 'never',
23
25
  };
24
26
  function solas(c) {
25
- const config = Solas.Config.validate({
26
- ...DEFAULT_CONFIG,
27
+ const validatedConfig = Solas.Config.validate({
27
28
  ...c,
28
29
  url: c?.url ?? process.env.VITE_APP_URL?.toString(),
29
30
  });
31
+ const config = {
32
+ ...DEFAULT_CONFIG,
33
+ ...validatedConfig,
34
+ runtime: validatedConfig.runtime ?? DEFAULT_CONFIG.runtime,
35
+ };
36
+ Runtime.runtime = Solas.Runtime.create(config.runtime);
30
37
  if (config.logger?.level)
31
38
  Logger.defaultLevel = config.logger.level;
32
39
  const logger = new Logger();
@@ -43,11 +50,11 @@ function solas(c) {
43
50
  const cached = fileCache.get(filePath);
44
51
  if (cached === content) {
45
52
  // if content is unchanged and file exists, skip write
46
- if (await Bun.file(filePath).exists())
53
+ if (await Runtime.exists(filePath))
47
54
  return null;
48
55
  // else, file is missing but cached content is the same as
49
56
  // last time we saw it, write it
50
- await Bun.write(filePath, content);
57
+ await Runtime.write(filePath, content);
51
58
  fileCache.set(filePath, content);
52
59
  return path.relative(process.cwd(), filePath);
53
60
  }
@@ -57,7 +64,7 @@ function solas(c) {
57
64
  if (curr === content)
58
65
  return null;
59
66
  try {
60
- await Bun.write(filePath, content);
67
+ await Runtime.write(filePath, content);
61
68
  fileCache.set(filePath, content);
62
69
  return path.relative(process.cwd(), filePath);
63
70
  }
@@ -70,7 +77,7 @@ function solas(c) {
70
77
  // file doesn't exist, write it
71
78
  if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
72
79
  try {
73
- await Bun.write(filePath, content);
80
+ await Runtime.write(filePath, content);
74
81
  fileCache.set(filePath, content);
75
82
  return path.relative(process.cwd(), filePath);
76
83
  }
@@ -100,7 +107,7 @@ function solas(c) {
100
107
  ['manifest.ts', writeManifest(manifest)],
101
108
  ['maps.ts', writeMaps(imports, modules)],
102
109
  [`${Solas.Config.SLUG}.d.ts`, writeTypes(manifest)],
103
- [Solas.Config.ENTRY_RSC, writeRSCEntry()],
110
+ [Solas.Config.ENTRY_RSC, writeRSCEntry(config)],
104
111
  [Solas.Config.ENTRY_SSR, writeSSREntry()],
105
112
  [Solas.Config.ENTRY_BROWSER, writeBrowserEntry()],
106
113
  ];
@@ -299,7 +306,7 @@ function solas(c) {
299
306
  }
300
307
  // write build manifest
301
308
  const generatedDir = path.join(process.cwd(), Solas.Config.GENERATED_DIR);
302
- await Bun.write(path.join(generatedDir, 'build.json'), JSON.stringify({
309
+ await Runtime.write(path.join(generatedDir, 'build.json'), JSON.stringify({
303
310
  base: resolvedViteConfig?.base ?? '/',
304
311
  publicFiles: await collectPublicFiles(resolvedViteConfig?.publicDir),
305
312
  prerenderRoutes: Array.from(buildContext.prerenderRoutes),
@@ -4,7 +4,7 @@ export { BrowserRouter } from './shared.js';
4
4
  export declare const BrowserRouterContext: import("react").Context<{
5
5
  go: BrowserRouter.Go;
6
6
  prefetch: (path: string) => void;
7
- refresh: () => void;
7
+ refresh: BrowserRouter.Refresh;
8
8
  isNavigating: boolean;
9
9
  url: {
10
10
  pathname?: string | undefined;
@@ -10,7 +10,7 @@ export { BrowserRouter } from './shared.js';
10
10
  export const BrowserRouterContext = createContext({
11
11
  go: async () => '',
12
12
  prefetch: () => { },
13
- refresh: () => { },
13
+ refresh: async () => '',
14
14
  isNavigating: false,
15
15
  url: {},
16
16
  });
@@ -130,7 +130,7 @@ export function BrowserRouterProvider({ children, setPayload, isNavigating = fal
130
130
  const currentPath = window.location.pathname + window.location.search;
131
131
  const key = ResponseCache.toCacheKey(currentPath, window.location.origin);
132
132
  if (!key)
133
- return;
133
+ return Promise.resolve(currentPath);
134
134
  if (responseCache.has(key))
135
135
  responseCache.remove(key);
136
136
  return go(currentPath, {
@@ -156,6 +156,7 @@ export declare namespace BrowserRouter {
156
156
  <TTo extends Target>(to: TTo, opts?: TargetConfig & Replace): Promise<string>;
157
157
  <TTo extends string>(to: string extends TTo ? TTo : never, opts?: GoOptions): Promise<string>;
158
158
  };
159
+ export type Refresh = () => Promise<string>;
159
160
  /**
160
161
  * Convert a route pattern and params into a real path string. This is used internally
161
162
  * to implement <Link /> and router.go
@@ -1,7 +1,7 @@
1
1
  export declare function useRouter(): {
2
2
  go: import("./shared.js").BrowserRouter.Go;
3
3
  prefetch: (path: string) => void;
4
- refresh: () => void;
4
+ refresh: import("./shared.js").BrowserRouter.Refresh;
5
5
  isNavigating: boolean;
6
6
  url: {
7
7
  pathname?: string | undefined;
@@ -4,6 +4,7 @@ import { Logger } from '../utils/logger.js';
4
4
  import { Solas } from '../solas.js';
5
5
  import { normalisePathname } from './http-router/utils.js';
6
6
  import { Prerender } from './prerender.js';
7
+ import { Runtime } from './runtimes/runtime.js';
7
8
  export { Build };
8
9
  /**
9
10
  * Types, constants, and the Finder class for route discovery and manifest generation.
@@ -345,7 +346,7 @@ var Build;
345
346
  currentPrerenderMode = flag;
346
347
  }
347
348
  const shellImport = Finder.getImportPath(shellPath);
348
- const shellId = `${Build.EntryKind.SHELL}${Bun.hash(shellImport)}`;
349
+ const shellId = `${Build.EntryKind.SHELL}${Runtime.hash(shellImport)}`;
349
350
  const layoutIds = [];
350
351
  const unauthorisedIds = [];
351
352
  const forbiddenIds = [];
@@ -366,7 +367,7 @@ var Build;
366
367
  continue;
367
368
  }
368
369
  const layoutImport = Finder.getImportPath(layoutPath);
369
- const layoutId = `${Build.EntryKind.LAYOUT}${Bun.hash(layoutImport)}`;
370
+ const layoutId = `${Build.EntryKind.LAYOUT}${Runtime.hash(layoutImport)}`;
370
371
  if (!processed.has(layoutPath)) {
371
372
  prerenderCache.set(layoutPath, await Prerender.Build.getStaticFlag(layoutPath, this.buildContext));
372
373
  imports.components.dynamic.set(layoutId, layoutImport);
@@ -381,7 +382,7 @@ var Build;
381
382
  continue;
382
383
  }
383
384
  const unauthorisedImport = Finder.getImportPath(unauthorisedPath);
384
- const unauthorisedId = `${Build.EntryKind['401']}${Bun.hash(unauthorisedImport)}`;
385
+ const unauthorisedId = `${Build.EntryKind['401']}${Runtime.hash(unauthorisedImport)}`;
385
386
  unauthorisedIds.push(unauthorisedId);
386
387
  if (!processed.has(unauthorisedPath)) {
387
388
  imports.components.dynamic.set(unauthorisedId, unauthorisedImport);
@@ -394,7 +395,7 @@ var Build;
394
395
  continue;
395
396
  }
396
397
  const forbiddenImport = Finder.getImportPath(forbiddenPath);
397
- const forbiddenId = `${Build.EntryKind['403']}${Bun.hash(forbiddenImport)}`;
398
+ const forbiddenId = `${Build.EntryKind['403']}${Runtime.hash(forbiddenImport)}`;
398
399
  forbiddenIds.push(forbiddenId);
399
400
  if (!processed.has(forbiddenPath)) {
400
401
  imports.components.dynamic.set(forbiddenId, forbiddenImport);
@@ -409,7 +410,7 @@ var Build;
409
410
  continue;
410
411
  }
411
412
  const notFoundImport = Finder.getImportPath(notFoundPath);
412
- const notFoundId = `${Build.EntryKind['404']}${Bun.hash(notFoundImport)}`;
413
+ const notFoundId = `${Build.EntryKind['404']}${Runtime.hash(notFoundImport)}`;
413
414
  notFoundIds.push(notFoundId);
414
415
  // dedupe imports but still assign the slot for this route
415
416
  if (!processed.has(notFoundPath)) {
@@ -423,7 +424,7 @@ var Build;
423
424
  continue;
424
425
  }
425
426
  const serverErrorImport = Finder.getImportPath(serverErrorPath);
426
- const serverErrorId = `${Build.EntryKind['500']}${Bun.hash(serverErrorImport)}`;
427
+ const serverErrorId = `${Build.EntryKind['500']}${Runtime.hash(serverErrorImport)}`;
427
428
  serverErrorIds.push(serverErrorId);
428
429
  if (!processed.has(serverErrorPath)) {
429
430
  imports.components.dynamic.set(serverErrorId, serverErrorImport);
@@ -438,7 +439,7 @@ var Build;
438
439
  continue;
439
440
  }
440
441
  const loaderImport = Finder.getImportPath(loaderPath);
441
- const loaderId = `${Build.EntryKind.LOADING}${Bun.hash(loaderImport)}`;
442
+ const loaderId = `${Build.EntryKind.LOADING}${Runtime.hash(loaderImport)}`;
442
443
  loadingIds.push(loaderId);
443
444
  // dedupe imports but still assign the slot for this route
444
445
  if (!processed.has(loaderPath)) {
@@ -452,7 +453,7 @@ var Build;
452
453
  continue;
453
454
  }
454
455
  const middlewareImport = Finder.getImportPath(middlewarePath);
455
- const middlewareId = `${Build.EntryKind.MIDDLEWARE}${Bun.hash(middlewareImport)}`;
456
+ const middlewareId = `${Build.EntryKind.MIDDLEWARE}${Runtime.hash(middlewareImport)}`;
456
457
  middlewareIds.push(middlewareId);
457
458
  if (!processed.has(middlewarePath)) {
458
459
  // route scanning only tells us this is a +middleware file path
@@ -466,8 +467,8 @@ var Build;
466
467
  }
467
468
  // generate entry id based on page if exists, otherwise dir
468
469
  const entryId = pagePath
469
- ? `${Build.EntryKind.PAGE}${Bun.hash(Finder.getImportPath(pagePath))}`
470
- : `${Build.EntryKind.PAGE}${Bun.hash(route)}`;
470
+ ? `${Build.EntryKind.PAGE}${Runtime.hash(Finder.getImportPath(pagePath))}`
471
+ : `${Build.EntryKind.PAGE}${Runtime.hash(route)}`;
471
472
  if (pagePath) {
472
473
  const pagePrerender = await Prerender.Build.getStaticFlag(pagePath, this.buildContext);
473
474
  applyPrerenderMode(pagePrerender);
@@ -568,12 +569,12 @@ var Build;
568
569
  continue;
569
570
  }
570
571
  const m = method.toLowerCase();
571
- const endpointId = `${Build.EntryKind.ENDPOINT}${Bun.hash(Finder.getImportPath(endpointFilePath))}_${m}`;
572
+ const endpointId = `${Build.EntryKind.ENDPOINT}${Runtime.hash(Finder.getImportPath(endpointFilePath))}_${m}`;
572
573
  const middlewareIds = await Promise.all(endpointMiddlewarePaths.map(async middlewarePath => {
573
574
  if (!middlewarePath)
574
575
  return null;
575
576
  const middlewareImport = Finder.getImportPath(middlewarePath);
576
- const middlewareId = `${Build.EntryKind.MIDDLEWARE}${Bun.hash(middlewareImport)}`;
577
+ const middlewareId = `${Build.EntryKind.MIDDLEWARE}${Runtime.hash(middlewareImport)}`;
577
578
  if (!processed.has(middlewarePath)) {
578
579
  // endpoint middleware discovery gives us file paths, not proof
579
580
  // of the export so check the module shape first
@@ -605,7 +606,7 @@ var Build;
605
606
  modules[route] = {
606
607
  ...(modules[route] ?? {}),
607
608
  middlewareIds: endpointMiddlewarePaths.map(middlewarePath => middlewarePath
608
- ? `${Build.EntryKind.MIDDLEWARE}${Bun.hash(Finder.getImportPath(middlewarePath))}`
609
+ ? `${Build.EntryKind.MIDDLEWARE}${Runtime.hash(Finder.getImportPath(middlewarePath))}`
609
610
  : null),
610
611
  };
611
612
  }
@@ -4,14 +4,15 @@ import { AUTOGEN_MSG, source, toSourceLiteral } from './utils.js';
4
4
  * Generates the code to create an exported config object
5
5
  */
6
6
  export function writeConfig(config) {
7
+ const { runtime: _runtime, ...runtimeConfig } = config;
7
8
  const loggerLevel = config.logger?.level;
8
9
  const importLines = [
9
- `import type { PluginConfig } from '${Solas.Config.PKG_NAME}'`,
10
+ `import type { RuntimeConfig } from '${Solas.Config.PKG_NAME}'`,
10
11
  loggerLevel ? `import { Logger } from '${Solas.Config.PKG_NAME}/utils/logger'` : '',
11
12
  ]
12
13
  .filter(Boolean)
13
14
  .join('\n');
14
- const configStatement = `const config = ${toSourceLiteral(config)} as const satisfies PluginConfig`;
15
+ const configStatement = `const config = ${toSourceLiteral(runtimeConfig)} as const satisfies RuntimeConfig`;
15
16
  const loggerStatement = loggerLevel
16
17
  ? `Logger.defaultLevel = ${toSourceLiteral(loggerLevel)}`
17
18
  : '';
@@ -1,7 +1,8 @@
1
+ import type { ConfiguredPluginConfig } from '../../types.js';
1
2
  /**
2
3
  * Generates the RSC entry code
3
4
  */
4
- export declare function writeRSCEntry(): string;
5
+ export declare function writeRSCEntry(config: ConfiguredPluginConfig): string;
5
6
  /**
6
7
  * Generates the SSR entry code
7
8
  */
@@ -1,19 +1,21 @@
1
1
  import { Solas } from '../../solas.js';
2
- import { AUTOGEN_MSG, source } from './utils.js';
2
+ import { AUTOGEN_MSG, source, toStringLiteral } from './utils.js';
3
3
  /**
4
4
  * Generates the RSC entry code
5
5
  */
6
- export function writeRSCEntry() {
6
+ export function writeRSCEntry(config) {
7
+ const runtime = toStringLiteral(config.runtime);
7
8
  return source `
8
9
  ${AUTOGEN_MSG}
9
10
 
10
- import { createHandler } from '${Solas.Config.PKG_NAME}/env/rsc'
11
- import { Solas } from '${Solas.Config.PKG_NAME}'
11
+ import { createHandler, Runtime } from '${Solas.Config.PKG_NAME}/env/rsc'
12
+ import { Solas } from '${Solas.Config.PKG_NAME}/$'
12
13
 
13
14
  import { manifest } from './manifest.js'
14
15
  import { importMap } from './maps.js'
15
16
  import { config } from './config.js'
16
17
 
18
+ Runtime.runtime = Solas.Runtime.create(${runtime})
17
19
  const runtimeManifest = await Solas.Runtime.loadManifest(Solas.Config.OUT_DIR)
18
20
 
19
21
  export default createHandler(config, manifest, importMap, runtimeManifest)
@@ -2,6 +2,7 @@ import type { ReactFormState } from 'react-dom/client';
2
2
  import type { ImportMap, Manifest, RuntimeConfig } from '../../types.js';
3
3
  import { Solas } from '../../solas.js';
4
4
  import { Metadata } from '../metadata.js';
5
+ export { Runtime } from '../runtimes/runtime.js';
5
6
  export type RscPayload = {
6
7
  returnValue?: {
7
8
  ok: boolean;
@@ -17,6 +17,7 @@ import { processActionRequest } from '../server/actions.js';
17
17
  import DefaultErr from '../ui/defaults/error.js';
18
18
  import { RequestContext } from './request-context.js';
19
19
  import { getKnownDigest, isKnownError } from './utils.js';
20
+ export { Runtime } from '../runtimes/runtime.js';
20
21
  const logger = new Logger();
21
22
  const BASE_PATH = BasePath.normalise(import.meta.env.BASE_URL);
22
23
  function resolveFilePath(root, relativePath) {
@@ -2,6 +2,7 @@ import { match as createMatch } from 'path-to-regexp';
2
2
  import { BasePath } from '../../utils/base-path.js';
3
3
  import { Solas } from '../../solas.js';
4
4
  import { HttpException } from '../navigation/http-exception.js';
5
+ import { Runtime } from '../runtimes/runtime.js';
5
6
  import { maybeAction } from '../server/actions.js';
6
7
  import { enforce } from '../server/csrf.js';
7
8
  import { getAlternatePathname, normalisePathname, toPathPattern } from './utils.js';
@@ -253,24 +254,24 @@ export class HttpRouter {
253
254
  */
254
255
  static async serveStatic(filePath, req, precompress = false, headers = {}) {
255
256
  const accept = req.headers.get('accept-encoding') ?? '';
256
- let file = Bun.file(filePath);
257
+ let resolvedPath = filePath;
257
258
  let encoding = null;
258
259
  if (precompress) {
259
260
  // prefer a precompressed variant when the client accepts it and one was emitted
260
261
  if (accept.includes('br')) {
261
- const brotli = Bun.file(`${filePath}.br`);
262
- if (await brotli.exists()) {
263
- file = brotli;
262
+ const brotliPath = `${filePath}.br`;
263
+ if (await Runtime.exists(brotliPath)) {
264
+ resolvedPath = brotliPath;
264
265
  encoding = 'br';
265
266
  }
266
267
  }
267
268
  }
268
- if (!(await file.exists())) {
269
+ if (!(await Runtime.exists(resolvedPath))) {
269
270
  return new Response('Not found', { status: 404 });
270
271
  }
271
272
  // get mime type from original path, not compressed variant
272
- const mimeType = Bun.file(filePath).type;
273
- const res = new Response(file, {
273
+ const mimeType = Runtime.mimeType(filePath);
274
+ const res = new Response(await Runtime.readBuffer(resolvedPath), {
274
275
  headers: {
275
276
  'Content-Type': headers['Content-Type'] ?? mimeType,
276
277
  },
@@ -4,6 +4,7 @@ import { Compress } from '../utils/compress.js';
4
4
  import { Logger } from '../utils/logger.js';
5
5
  import { Solas } from '../solas.js';
6
6
  import { Prerender } from './prerender.js';
7
+ import { Runtime } from './runtimes/runtime.js';
7
8
  const logger = new Logger();
8
9
  export async function postbuild(cwd = process.cwd()) {
9
10
  const manifestPath = path.join(cwd, Solas.Config.GENERATED_DIR, 'build.json');
@@ -59,8 +60,8 @@ export async function postbuild(cwd = process.cwd()) {
59
60
  if (artifact.mode === 'ppr') {
60
61
  await fs.mkdir(artifactDir, { recursive: true });
61
62
  const writes = [
62
- Bun.write(path.join(artifactDir, 'prelude.html'), artifact.html),
63
- Bun.write(path.join(artifactDir, 'metadata.json'), JSON.stringify({
63
+ Runtime.write(path.join(artifactDir, 'prelude.html'), artifact.html),
64
+ Runtime.write(path.join(artifactDir, 'metadata.json'), JSON.stringify({
64
65
  schema: artifact.schema,
65
66
  route: artifact.route,
66
67
  createdAt: artifact.createdAt,
@@ -68,7 +69,7 @@ export async function postbuild(cwd = process.cwd()) {
68
69
  })),
69
70
  ];
70
71
  if (artifact.postponed !== undefined) {
71
- writes.push(Bun.write(path.join(artifactDir, 'postponed.json'), JSON.stringify(artifact.postponed)));
72
+ writes.push(Runtime.write(path.join(artifactDir, 'postponed.json'), JSON.stringify(artifact.postponed)));
72
73
  }
73
74
  await Promise.all(writes);
74
75
  artifactManifest[route] = {
@@ -82,13 +83,13 @@ export async function postbuild(cwd = process.cwd()) {
82
83
  }
83
84
  await fs.mkdir(artifactDir, { recursive: true });
84
85
  await Promise.all([
85
- Bun.write(path.join(artifactDir, 'metadata.json'), JSON.stringify({
86
+ Runtime.write(path.join(artifactDir, 'metadata.json'), JSON.stringify({
86
87
  schema: artifact.schema,
87
88
  route: artifact.route,
88
89
  createdAt: artifact.createdAt,
89
90
  mode: artifact.mode,
90
91
  })),
91
- Bun.write(Prerender.Artifact.getFilePath(outDir, route, Prerender.Artifact.FULL_PRERENDER_FILENAME), artifact.html),
92
+ Runtime.write(Prerender.Artifact.getFilePath(outDir, route, Prerender.Artifact.FULL_PRERENDER_FILENAME), artifact.html),
92
93
  ]);
93
94
  artifactManifest[route] = {
94
95
  mode: artifact.mode,
@@ -108,7 +109,7 @@ export async function postbuild(cwd = process.cwd()) {
108
109
  artifacts: artifactManifest,
109
110
  publicFiles: manifest.publicFiles,
110
111
  };
111
- await Bun.write(Solas.Runtime.getManifestPath(outDir), JSON.stringify(runtimeManifest));
112
+ await Runtime.write(Solas.Runtime.getManifestPath(outDir), JSON.stringify(runtimeManifest));
112
113
  if (manifest.sitemapRoutes.length > 0 && manifest.url) {
113
114
  const origin = manifest.url.replace(/\/$/, '');
114
115
  const urls = manifest.sitemapRoutes
@@ -120,7 +121,7 @@ export async function postbuild(cwd = process.cwd()) {
120
121
  urls,
121
122
  '</urlset>',
122
123
  ].join('\n');
123
- await Bun.write(path.join(outDir, 'sitemap.xml'), sitemap);
124
+ await Runtime.write(path.join(outDir, 'sitemap.xml'), sitemap);
124
125
  logger.info('[sitemap]', `generated ${manifest.sitemapRoutes.length} urls`);
125
126
  }
126
127
  if (manifest.precompress) {
@@ -144,7 +145,7 @@ export async function postbuild(cwd = process.cwd()) {
144
145
  normalisedPath.endsWith(`/${Prerender.Artifact.FULL_PRERENDER_FILENAME}`));
145
146
  },
146
147
  })) {
147
- await Bun.write(`${input}.br`, compressed);
148
+ await Runtime.write(`${input}.br`, compressed);
148
149
  logger.info('[precompress]', `${path.basename(input)}.br`);
149
150
  }
150
151
  }