@jk2908/solas 0.5.4 → 0.6.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 (66) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +81 -85
  3. package/dist/config.d.ts +25 -0
  4. package/dist/config.js +152 -0
  5. package/dist/events.d.ts +4 -0
  6. package/dist/events.js +5 -0
  7. package/dist/index.d.ts +1 -2
  8. package/dist/index.js +59 -37
  9. package/dist/internal/boundary-error.d.ts +4 -0
  10. package/dist/internal/boundary-error.js +8 -0
  11. package/dist/internal/browser-router/history.d.ts +1 -1
  12. package/dist/internal/browser-router/link.d.ts +1 -1
  13. package/dist/internal/browser-router/link.js +1 -1
  14. package/dist/internal/browser-router/router.d.ts +2 -2
  15. package/dist/internal/browser-router/router.js +5 -5
  16. package/dist/internal/browser-router/shared.d.ts +182 -184
  17. package/dist/internal/browser-router/shared.js +49 -56
  18. package/dist/internal/browser-router/use-router.d.ts +1 -1
  19. package/dist/internal/browser-router/use-search-params.js +3 -3
  20. package/dist/internal/build.d.ts +108 -110
  21. package/dist/internal/build.js +569 -581
  22. package/dist/internal/codegen/config.js +3 -3
  23. package/dist/internal/codegen/environments.js +5 -6
  24. package/dist/internal/codegen/format.d.ts +6 -0
  25. package/dist/internal/codegen/format.js +24 -0
  26. package/dist/internal/codegen/manifest.js +2 -2
  27. package/dist/internal/codegen/maps.d.ts +1 -1
  28. package/dist/internal/codegen/maps.js +2 -2
  29. package/dist/internal/codegen/types.js +4 -4
  30. package/dist/internal/codegen/utils.js +2 -2
  31. package/dist/internal/env/request-context.js +1 -1
  32. package/dist/internal/env/rsc.d.ts +3 -2
  33. package/dist/internal/env/rsc.js +26 -25
  34. package/dist/internal/env/ssr.js +4 -4
  35. package/dist/internal/http-router/router.js +8 -8
  36. package/dist/internal/metadata.d.ts +1 -1
  37. package/dist/internal/metadata.js +1 -1
  38. package/dist/internal/navigation/http-exception-boundary.d.ts +1 -1
  39. package/dist/internal/navigation/redirect-boundary.d.ts +1 -1
  40. package/dist/internal/navigation/redirect.js +6 -6
  41. package/dist/internal/postbuild.js +18 -17
  42. package/dist/internal/prerender.d.ts +130 -138
  43. package/dist/internal/prerender.js +326 -354
  44. package/dist/internal/public-files.js +2 -4
  45. package/dist/internal/render/head.js +2 -2
  46. package/dist/internal/resolver.js +1 -1
  47. package/dist/internal/server/actions.js +2 -2
  48. package/dist/internal/ui/error-boundary.d.ts +1 -1
  49. package/dist/manifest.d.ts +7 -0
  50. package/dist/manifest.js +85 -0
  51. package/dist/prerender.d.ts +1 -1
  52. package/dist/prerender.js +1 -1
  53. package/dist/solas.d.ts +2 -46
  54. package/dist/solas.js +6 -266
  55. package/dist/types.d.ts +14 -21
  56. package/dist/types.js +1 -1
  57. package/dist/utils/base-path.d.ts +12 -14
  58. package/dist/utils/base-path.js +75 -82
  59. package/dist/utils/compress.d.ts +9 -11
  60. package/dist/utils/compress.js +59 -64
  61. package/dist/utils/context.d.ts +4 -6
  62. package/dist/utils/context.js +17 -22
  63. package/dist/utils/logger.js +2 -2
  64. package/dist/utils/time.d.ts +2 -4
  65. package/dist/utils/time.js +23 -29
  66. package/package.json +18 -8
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Breaking
6
+
7
+ - Removed the `Solas` runtime namespace exports (`Solas.Config`, `Solas.Runtime`, `Solas.Events`) from the `@jk2908/solas` entry. Runtime constants now live in dedicated internal modules, and generated code no longer reaches through the namespace.
8
+ - Removed the `@jk2908/solas/$` export path. Generated environment entries now import directly from `@jk2908/solas/env/rsc`.
9
+ - Removed the `port` config option from `solas()`. The development port now comes from Vite's `server.port`. `url` remains available as an optional public origin and now resolves from the `url` option → `VITE_APP_URL` → the Vite dev server `host`/`port`.
10
+
11
+ ### Features
12
+
13
+ - Generated `.solas` files are formatted with oxfmt before writing, and stale generated files are pruned when the codegen stops emitting them.
14
+ - Added a Vitest test suite covering unit, integration, and browser (React Testing Library) tests.
15
+
16
+ ### Fixes
17
+
18
+ - Generated `entry.rsc.tsx` now emits valid code (`loadManifest` from `env/rsc`) instead of referencing an undefined `Config` import.
19
+ - Fixed the release pipeline by committing the postbuild measurement script.
20
+
21
+ ### Changed
22
+
23
+ - Pinned `@vitejs/plugin-rsc` to `0.5.34`.
24
+ - Removed the Playwright e2e suite, fixtures, and scripts for now; unit, integration, and browser tests run through Vitest.
25
+ - Declared `engines.node` (`^20.19.0 || >=22.12.0`).
26
+
3
27
  ## 0.5.4 - 2026-06-08
4
28
 
5
29
  - Add a BrowserRouterHistory API to sync app back/forward navigation with browser navigation.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Solas is a minimal React meta-framework powered by Vite, created for experimenting with routing, streaming, and prerendering with React Server Components.
4
4
 
5
- Solas is experimental and currently has no automated test suite, so expect rough edges.
5
+ Solas is experimental and under active development, so expect rough edges.
6
6
 
7
7
  ## Install
8
8
 
@@ -201,13 +201,9 @@ That keeps your route params aligned across links, imperative navigation, metada
201
201
 
202
202
  All Solas options are passed to `solas()` inside `defineConfig`.
203
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.
204
+ ### `runtime`
209
205
 
210
- Use the `runtime` config key to select the runtime used by Solas server code.
206
+ Use `runtime` to select the runtime used by Solas server code.
211
207
 
212
208
  Supported values are `'auto'`, `'node'`, and `'bun'`.
213
209
 
@@ -215,104 +211,45 @@ Supported values are `'auto'`, `'node'`, and `'bun'`.
215
211
 
216
212
  If you already run Vite through Bun, `runtime: 'auto'` is usually enough and Solas will detect Bun automatically.
217
213
 
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
-
272
214
  ### `url`
273
215
 
274
- `url` is optional. If you set it, Solas treats it as the public origin for your app.
216
+ Use `url` to set the public origin for your app. It is optional.
275
217
 
276
- Solas resolves it in this order:
218
+ Solas resolves the origin in this order:
277
219
 
278
220
  - the `url` option passed to `solas()`
279
221
  - `VITE_APP_URL`
222
+ - the Vite dev server `host` and `port` (defaults to `http://localhost:8787`)
280
223
 
281
- Solas exposes the resolved value as `import.meta.env.VITE_APP_URL`. If `url` is set, prerender also uses it as the request origin for build-time renders.
224
+ The resolved origin is exposed to your app as `import.meta.env.VITE_APP_URL` and is used for build-time prerender requests and `sitemap` URLs. When `url` is set, it is also used as a trusted origin for CSRF.
282
225
 
283
- In practice, you only need `url` if your app reads `import.meta.env.VITE_APP_URL` or your prerendered output needs a real public origin.
284
-
285
- If you do want to set it explicitly, this is the shape:
226
+ You usually only need `url` in production, where the Vite dev server origin is meaningless. For example, to generate a `sitemap.xml` with real URLs:
286
227
 
287
228
  ```ts
288
229
  export default defineConfig(({ mode }) => ({
289
230
  plugins: [
290
231
  solas({
291
- url: mode === 'production' ? 'https://example.com' : 'http://localhost:8787',
232
+ url: mode === 'production' ? 'https://example.com' : undefined,
233
+ sitemap: true,
292
234
  }),
293
235
  ],
294
236
  }))
295
237
  ```
296
238
 
297
- If you prefer an environment variable, set this instead:
239
+ If you prefer an environment variable, set `VITE_APP_URL` instead:
298
240
 
299
241
  ```sh
300
242
  VITE_APP_URL=https://example.com
301
243
  ```
302
244
 
303
- ### `port`
304
-
305
- Use `port` to change the development server port.
306
-
307
- Default: `8787`
245
+ Set the development port with Vite's `server.port`:
308
246
 
309
247
  ```ts
310
248
  export default defineConfig({
311
- plugins: [
312
- solas({
313
- port: 4000,
314
- }),
315
- ],
249
+ server: {
250
+ port: 4000,
251
+ },
252
+ plugins: [solas(), react()],
316
253
  })
317
254
  ```
318
255
 
@@ -510,13 +447,13 @@ Use `sitemap` to generate a `sitemap.xml` at build time.
510
447
 
511
448
  Default: `false`
512
449
 
513
- When enabled, Solas writes a sitemap containing all routes with deterministic URLs: static routes, prerendered routes, and dynamic routes resolved via `params`. The origin for each URL comes from `config.url`.
450
+ When enabled, Solas writes a sitemap containing all routes with deterministic URLs: static routes, prerendered routes, and dynamic routes resolved via `params`. The origin for each URL comes from the resolved origin (see Config > `url`).
514
451
 
515
452
  ```ts
516
453
  export default defineConfig(({ mode }) => ({
517
454
  plugins: [
518
455
  solas({
519
- url: mode === 'production' ? 'https://example.com' : 'http://localhost:8787',
456
+ url: mode === 'production' ? 'https://example.com' : undefined,
520
457
  sitemap: true,
521
458
  }),
522
459
  ],
@@ -528,10 +465,9 @@ Routes with dynamic segments (`[id]`) or catch-all segments (`[...param]`) are o
528
465
  To add routes that Solas cannot discover automatically (for example, catch-all routes backed by a CMS), pass an object with a `routes` function. The function receives the auto-discovered routes and returns the final list:
529
466
 
530
467
  ```ts
531
- export default defineConfig(({ mode }) => ({
468
+ export default defineConfig({
532
469
  plugins: [
533
470
  solas({
534
- url: mode === 'production' ? 'https://example.com' : 'http://localhost:8787',
535
471
  sitemap: {
536
472
  async routes(existing) {
537
473
  const posts = await getPosts()
@@ -540,7 +476,7 @@ export default defineConfig(({ mode }) => ({
540
476
  },
541
477
  }),
542
478
  ],
543
- }))
479
+ })
544
480
  ```
545
481
 
546
482
  The `routes` function can be async. The callback also lets you filter routes:
@@ -581,6 +517,66 @@ export default defineConfig({
581
517
  })
582
518
  ```
583
519
 
520
+ ## Runtime
521
+
522
+ Solas uses a runtime for filesystem access, mime lookup, and hashing.
523
+
524
+ This is not a deployment or packaging adapter. Platform adapters are not available yet.
525
+
526
+ ### Node runtime
527
+
528
+ If you want to pin Node explicitly, set `runtime: 'node'`:
529
+
530
+ ```ts
531
+ import { defineConfig } from 'vite'
532
+
533
+ import solas from '@jk2908/solas'
534
+ import react from '@vitejs/plugin-react'
535
+
536
+ export default defineConfig({
537
+ plugins: [solas({ runtime: 'node' }), react()],
538
+ })
539
+ ```
540
+
541
+ Use the normal Vite commands with the Node runtime:
542
+
543
+ ```json
544
+ {
545
+ "scripts": {
546
+ "dev": "vite dev",
547
+ "build": "vite build",
548
+ "preview": "vite preview"
549
+ }
550
+ }
551
+ ```
552
+
553
+ ### Bun runtime
554
+
555
+ If you want Solas runtime code to execute in Bun, set `runtime: 'bun'`:
556
+
557
+ ```ts
558
+ import { defineConfig } from 'vite'
559
+
560
+ import solas from '@jk2908/solas'
561
+ import react from '@vitejs/plugin-react'
562
+
563
+ export default defineConfig({
564
+ plugins: [solas({ runtime: 'bun' }), react()],
565
+ })
566
+ ```
567
+
568
+ When you use the Bun runtime, run Vite through Bun so the server/runtime code executes in a Bun process:
569
+
570
+ ```json
571
+ {
572
+ "scripts": {
573
+ "dev": "bunx --bun vite dev",
574
+ "build": "bunx --bun vite build",
575
+ "preview": "bunx --bun vite preview"
576
+ }
577
+ }
578
+ ```
579
+
584
580
  ## Scripts
585
581
 
586
582
  Add scripts to your app:
@@ -619,7 +615,7 @@ When available, Solas checks browser provenance using:
619
615
  - `Origin`
620
616
  - `Referer`
621
617
 
622
- Solas also considers the effective request origin when your app is behind a proxy by using `X-Forwarded-Host`, `X-Forwarded-Proto`, and `config.url` when present.
618
+ Solas also considers the effective request origin when your app is behind a proxy by using `X-Forwarded-Host` and `X-Forwarded-Proto`.
623
619
 
624
620
  If you need to allow a trusted third-party browser POST source, configure it explicitly:
625
621
 
@@ -0,0 +1,25 @@
1
+ import type { PluginConfig } from './types.js';
2
+ export declare const NAME = "Solas";
3
+ export declare const SLUG: string;
4
+ export declare const PKG_NAME: string;
5
+ export declare const OUT_DIR = "dist";
6
+ export declare const APP_DIR = "app";
7
+ export declare const GENERATED_DIR: string;
8
+ export declare const ENTRY_RSC = "entry.rsc.tsx";
9
+ export declare const ENTRY_SSR = "entry.ssr.tsx";
10
+ export declare const ENTRY_BROWSER = "entry.browser.tsx";
11
+ export declare const ASSETS_DIR: string;
12
+ export declare const PUBLIC_DIR = "public";
13
+ export declare const $: unique symbol;
14
+ export declare const REQUEST_META_KEY: string;
15
+ export declare const LOG_LEVELS: readonly ["debug", "info", "warn", "error", "fatal"];
16
+ export declare const RUNTIMES: readonly ["auto", "node", "bun"];
17
+ export declare const PRERENDER_MODES: readonly ["full", "ppr", false];
18
+ export declare const TRAILING_SLASH_MODES: readonly ["always", "never", "ignore"];
19
+ export declare const RUNTIME_MANIFEST = "runtime-manifest.json";
20
+ /**
21
+ * Validate the plugin configuration object, throwing an error if invalid
22
+ * @param input - the unvalidated configuration object
23
+ * @return the typed and validated configuration object
24
+ */
25
+ export declare function validate(input: unknown): PluginConfig;
package/dist/config.js ADDED
@@ -0,0 +1,152 @@
1
+ import * as Config from './config.js';
2
+ export const NAME = 'Solas';
3
+ export const SLUG = NAME.toLowerCase();
4
+ export const PKG_NAME = `@jk2908/${SLUG}`;
5
+ export const OUT_DIR = 'dist';
6
+ export const APP_DIR = 'app';
7
+ export const GENERATED_DIR = `.${SLUG}`;
8
+ export const ENTRY_RSC = 'entry.rsc.tsx';
9
+ export const ENTRY_SSR = 'entry.ssr.tsx';
10
+ export const ENTRY_BROWSER = 'entry.browser.tsx';
11
+ export const ASSETS_DIR = `_${SLUG}`;
12
+ export const PUBLIC_DIR = 'public';
13
+ export const $ = Symbol(SLUG);
14
+ export const REQUEST_META_KEY = `__${SLUG.toUpperCase()}__`;
15
+ export const LOG_LEVELS = ['debug', 'info', 'warn', 'error', 'fatal'];
16
+ export const RUNTIMES = ['auto', 'node', 'bun'];
17
+ export const PRERENDER_MODES = ['full', 'ppr', false];
18
+ export const TRAILING_SLASH_MODES = ['always', 'never', 'ignore'];
19
+ export const RUNTIME_MANIFEST = 'runtime-manifest.json';
20
+ const CONFIG_KEYS = new Set([
21
+ 'runtime',
22
+ 'logger',
23
+ 'metadata',
24
+ 'precompress',
25
+ 'prerender',
26
+ 'sitemap',
27
+ 'trustedOrigins',
28
+ 'trailingSlash',
29
+ 'url',
30
+ ]);
31
+ const LOGGER_KEYS = new Set(['level']);
32
+ /**
33
+ * Validate the plugin configuration object, throwing an error if invalid
34
+ * @param input - the unvalidated configuration object
35
+ * @return the typed and validated configuration object
36
+ */
37
+ export function validate(input) {
38
+ if (input === undefined)
39
+ return {};
40
+ const errors = [];
41
+ if (!isRecord(input)) {
42
+ throw new Error(`[${NAME}] Invalid config:\n- Expected plugin config to be an object`);
43
+ }
44
+ for (const key of Object.keys(input)) {
45
+ if (!CONFIG_KEYS.has(key)) {
46
+ errors.push(`Unknown config key: ${key}`);
47
+ }
48
+ }
49
+ if ('runtime' in input && input.runtime !== undefined && !isRuntime(input.runtime)) {
50
+ errors.push("runtime must be 'auto', 'node', or 'bun'");
51
+ }
52
+ if ('url' in input && input.url !== undefined) {
53
+ if (typeof input.url !== 'string') {
54
+ errors.push('url must be a string');
55
+ }
56
+ else {
57
+ try {
58
+ const url = new URL(input.url);
59
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
60
+ errors.push('url must use http:// or https://');
61
+ }
62
+ }
63
+ catch {
64
+ errors.push('url must be a valid URL');
65
+ }
66
+ }
67
+ }
68
+ if ('precompress' in input && input.precompress !== undefined) {
69
+ if (typeof input.precompress !== 'boolean') {
70
+ errors.push('precompress must be a boolean');
71
+ }
72
+ }
73
+ if ('trustedOrigins' in input && input.trustedOrigins !== undefined) {
74
+ if (!Array.isArray(input.trustedOrigins)) {
75
+ errors.push('trustedOrigins must be an array of origins');
76
+ }
77
+ else {
78
+ for (const [index, value] of input.trustedOrigins.entries()) {
79
+ if (typeof value !== 'string') {
80
+ errors.push(`trustedOrigins[${index}] must be a string`);
81
+ continue;
82
+ }
83
+ try {
84
+ const url = new URL(value);
85
+ const canonical = value.replace(/\/$/, '');
86
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
87
+ errors.push(`trustedOrigins[${index}] must use http:// or https://`);
88
+ continue;
89
+ }
90
+ if (canonical !== url.origin) {
91
+ errors.push(`trustedOrigins[${index}] must be an origin without a path, query, or hash`);
92
+ }
93
+ }
94
+ catch {
95
+ errors.push(`trustedOrigins[${index}] must be a valid URL origin`);
96
+ }
97
+ }
98
+ }
99
+ }
100
+ if ('sitemap' in input && input.sitemap !== undefined && input.sitemap !== false) {
101
+ if (typeof input.sitemap !== 'boolean' && typeof input.sitemap !== 'object') {
102
+ errors.push('sitemap must be a boolean or an object with a routes function');
103
+ }
104
+ if (typeof input.sitemap === 'object' &&
105
+ input.sitemap !== null &&
106
+ typeof input.sitemap.routes !== 'function') {
107
+ errors.push('sitemap.routes must be a function');
108
+ }
109
+ }
110
+ if ('prerender' in input && input.prerender !== undefined) {
111
+ if (!new Set(PRERENDER_MODES).has(input.prerender)) {
112
+ errors.push("prerender must be 'full', 'ppr', or false");
113
+ }
114
+ }
115
+ if ('trailingSlash' in input && input.trailingSlash !== undefined) {
116
+ if (typeof input.trailingSlash !== 'string' ||
117
+ !new Set(TRAILING_SLASH_MODES).has(input.trailingSlash)) {
118
+ errors.push("trailingSlash must be 'always', 'never', or 'ignore'");
119
+ }
120
+ }
121
+ if ('metadata' in input && input.metadata !== undefined && !isRecord(input.metadata)) {
122
+ errors.push('metadata must be an object when provided');
123
+ }
124
+ if ('logger' in input && input.logger !== undefined) {
125
+ if (!isRecord(input.logger)) {
126
+ errors.push('logger must be an object when provided');
127
+ }
128
+ else {
129
+ for (const key of Object.keys(input.logger)) {
130
+ if (!LOGGER_KEYS.has(key)) {
131
+ errors.push(`Unknown logger key: ${key}`);
132
+ }
133
+ }
134
+ if ('level' in input.logger && input.logger.level !== undefined) {
135
+ if (typeof input.logger.level !== 'string' ||
136
+ !new Set(LOG_LEVELS).has(input.logger.level)) {
137
+ errors.push('logger.level must be one of: debug, info, warn, error, fatal');
138
+ }
139
+ }
140
+ }
141
+ }
142
+ if (errors.length > 0) {
143
+ throw new Error(`[${NAME}] Invalid config:\n- ${errors.join('\n- ')}`);
144
+ }
145
+ return input;
146
+ }
147
+ function isRecord(value) {
148
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
149
+ }
150
+ function isRuntime(value) {
151
+ return typeof value === 'string' && new Set(Config.RUNTIMES).has(value);
152
+ }
@@ -0,0 +1,4 @@
1
+ export declare const names: {
2
+ readonly NAVIGATION: `${string}navigation`;
3
+ readonly NAVIGATION_ERROR: `${string}navigationerror`;
4
+ };
package/dist/events.js ADDED
@@ -0,0 +1,5 @@
1
+ import * as Config from './config.js';
2
+ export const names = {
3
+ NAVIGATION: `${Config.SLUG}navigation`,
4
+ NAVIGATION_ERROR: `${Config.SLUG}navigationerror`,
5
+ };
package/dist/index.d.ts CHANGED
@@ -2,6 +2,5 @@ import { type PluginOption } from 'vite';
2
2
  import type { PluginConfig } from './types.js';
3
3
  declare function solas(c?: PluginConfig): PluginOption[];
4
4
  export default solas;
5
- export type * from './solas.d.ts';
6
- export { Solas } from './solas.js';
5
+ export * as Solas from './solas.js';
7
6
  export type * from './types.js';