@jk2908/solas 0.3.0 → 0.3.2

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 (75) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/cli/build.d.ts +7 -0
  3. package/dist/cli/build.js +183 -0
  4. package/dist/cli/dev.d.ts +4 -0
  5. package/dist/cli/dev.js +13 -0
  6. package/dist/cli/preview.d.ts +1 -0
  7. package/dist/cli/preview.js +47 -0
  8. package/dist/cli.js +4 -238
  9. package/dist/index.js +2 -0
  10. package/dist/internal/browser-router/link.d.ts +17 -0
  11. package/dist/internal/{navigation → browser-router}/link.js +22 -16
  12. package/dist/internal/browser-router/router.d.ts +184 -0
  13. package/dist/internal/{router/router-provider.js → browser-router/router.js} +81 -12
  14. package/dist/internal/{router → browser-router}/use-router.d.ts +1 -1
  15. package/dist/internal/browser-router/use-router.js +5 -0
  16. package/dist/internal/{navigation → browser-router}/use-search-params.js +1 -1
  17. package/dist/internal/build.js +2 -2
  18. package/dist/internal/codegen/config.js +17 -8
  19. package/dist/internal/codegen/environments.js +7 -7
  20. package/dist/internal/codegen/manifest.js +3 -3
  21. package/dist/internal/codegen/maps.js +11 -15
  22. package/dist/internal/codegen/types.d.ts +5 -0
  23. package/dist/internal/codegen/types.js +48 -0
  24. package/dist/internal/codegen/utils.d.ts +10 -0
  25. package/dist/internal/codegen/utils.js +27 -2
  26. package/dist/internal/env/browser.js +6 -6
  27. package/dist/internal/env/flight.d.ts +29 -0
  28. package/dist/internal/env/flight.js +187 -0
  29. package/dist/internal/env/request-context.d.ts +1 -1
  30. package/dist/internal/env/rsc.d.ts +1 -1
  31. package/dist/internal/env/rsc.js +23 -28
  32. package/dist/internal/env/ssr.d.ts +2 -2
  33. package/dist/internal/env/ssr.js +27 -13
  34. package/dist/internal/env/utils.js +13 -1
  35. package/dist/internal/http-router/create-http-router.d.ts +6 -0
  36. package/dist/internal/{router/create-router.js → http-router/create-http-router.js} +5 -5
  37. package/dist/internal/{router → http-router}/router.d.ts +9 -9
  38. package/dist/internal/{router → http-router}/router.js +20 -19
  39. package/dist/internal/{router → http-router}/utils.d.ts +11 -3
  40. package/dist/internal/{router → http-router}/utils.js +9 -1
  41. package/dist/internal/metadata.js +10 -10
  42. package/dist/internal/prerender.d.ts +4 -9
  43. package/dist/internal/prerender.js +6 -23
  44. package/dist/internal/render/head.js +1 -1
  45. package/dist/internal/render/tree.d.ts +1 -1
  46. package/dist/internal/render/tree.js +17 -13
  47. package/dist/internal/{router/resolver.d.ts → resolver.d.ts} +41 -41
  48. package/dist/internal/{router/resolver.js → resolver.js} +7 -7
  49. package/dist/internal/server/actions.js +1 -1
  50. package/dist/internal/server/cookies.d.ts +3 -2
  51. package/dist/internal/server/cookies.js +4 -3
  52. package/dist/internal/server/dynamic.d.ts +1 -3
  53. package/dist/internal/server/dynamic.js +3 -11
  54. package/dist/internal/server/headers.d.ts +2 -2
  55. package/dist/internal/server/headers.js +3 -3
  56. package/dist/internal/server/url.d.ts +2 -2
  57. package/dist/internal/server/url.js +3 -3
  58. package/dist/navigation.d.ts +2 -4
  59. package/dist/navigation.js +2 -4
  60. package/dist/router.d.ts +3 -4
  61. package/dist/router.js +3 -4
  62. package/dist/solas.d.ts +3 -1
  63. package/dist/solas.js +1 -1
  64. package/dist/types.d.ts +15 -7
  65. package/dist/utils/logger.js +1 -1
  66. package/package.json +2 -7
  67. package/dist/internal/navigation/link.d.ts +0 -13
  68. package/dist/internal/router/create-router.d.ts +0 -6
  69. package/dist/internal/router/router-context.d.ts +0 -15
  70. package/dist/internal/router/router-context.js +0 -8
  71. package/dist/internal/router/router-provider.d.ts +0 -10
  72. package/dist/internal/router/use-router.js +0 -5
  73. /package/dist/internal/{navigation → browser-router}/use-search-params.d.ts +0 -0
  74. /package/dist/internal/{router/prefetcher.d.ts → prefetcher.d.ts} +0 -0
  75. /package/dist/internal/{router/prefetcher.js → prefetcher.js} +0 -0
@@ -0,0 +1,184 @@
1
+ import type { RscPayload } from '../env/rsc.js';
2
+ import { Solas } from '../../solas.js';
3
+ export declare namespace BrowserRouter {
4
+ export type Params = Record<string, string>;
5
+ export type Query = Record<string, string | number | boolean>;
6
+ export type Path = keyof Solas.Routes & string;
7
+ type Replace = {
8
+ replace?: boolean;
9
+ };
10
+ export type GoOptions = {
11
+ replace?: boolean;
12
+ query?: Query;
13
+ params?: Params;
14
+ };
15
+ /**
16
+ * These targets are used as-is. They are not matched against the route table,
17
+ * so this covers normal external URLs and hash-only links
18
+ */
19
+ export type ExternalTarget = `${string}:${string}` | `//${string}` | `#${string}`;
20
+ export function isHashOnlyTarget(target: string): boolean;
21
+ export function isExternalTarget(target: string, origin: string): boolean;
22
+ /**
23
+ * Turn a route pattern into the real path shape a caller can use. In practice,
24
+ * every ':param' or '*' part becomes a plain string slot
25
+ *
26
+ * @example
27
+ * ```ts
28
+ * // '/p/:id' becomes '/p/${string}'
29
+ * // '/test/*' becomes '/test/${string}'
30
+ * // '/posts' stays '/posts'
31
+ * ```
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * type A = ResolvedPath<'/posts/:id'>
36
+ * // '/posts/${string}'
37
+ * ```
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * type B = ResolvedPath<'/docs/*'>
42
+ * // '/docs/${string}'
43
+ * ```
44
+ */
45
+ export type ResolvedPath<TPath extends string> = TPath extends `${infer Start}:${string}/${infer Rest}` ? `${Start}${string}/${ResolvedPath<Rest>}` : TPath extends `${infer Start}:${string}` ? `${Start}${string}` : TPath extends `${infer Start}*${infer Rest}` ? `${Start}${string}${ResolvedPath<Rest>}` : TPath;
46
+ /**
47
+ * Once we have a real path, also allow the usual query-string and hash forms
48
+ *
49
+ * @example
50
+ * ```ts
51
+ * type A = TargetSuffix<'/posts/123'>
52
+ * // '/posts/123' | '/posts/123?${string}' | '/posts/123#${string}' | '/posts/123?${string}#${string}'
53
+ * ```
54
+ */
55
+ export type TargetSuffix<TPath extends string> = TPath | `${TPath}?${string}` | `${TPath}#${string}` | `${TPath}?${string}#${string}`;
56
+ /**
57
+ * This is the final string form a caller can navigate to. It can be an external
58
+ * URL, or a concrete URL that matches one of the known routes
59
+ *
60
+ * @example
61
+ * ```ts
62
+ * type A = Target
63
+ * // 'https://example.com'
64
+ * // '#intro'
65
+ * // '/posts/123'
66
+ * // '/posts/123?draft=true'
67
+ * ```
68
+ */
69
+ export type Target = ExternalTarget | TargetSuffix<ResolvedPath<Path>>;
70
+ /**
71
+ * Extra options for callers who already have a finished target string
72
+ *
73
+ * @example
74
+ * ```ts
75
+ * const a: TargetConfig = { query: { page: 2 } }
76
+ * // params is rejected here because the path is already complete
77
+ * ```
78
+ */
79
+ type TargetConfig = {
80
+ params?: never;
81
+ query?: Query;
82
+ };
83
+ /**
84
+ * Extra options for callers who pass a route pattern and params separately.
85
+ * If the route definition says that route needs params, this type makes
86
+ * those params required. If the route has no params, it rejects them
87
+ *
88
+ * @example
89
+ * ```ts
90
+ * // if Solas.Routes['/posts/:id'] is { params: { id: string } }
91
+ * type A = PatternConfig<'/posts/:id'>
92
+ * // { query?: Query } & { params: { id: string } }
93
+ * ```
94
+ *
95
+ * @example
96
+ * ```ts
97
+ * // if Solas.Routes['/about'] has no params field
98
+ * type B = PatternConfig<'/about'>
99
+ * // { query?: Query } & { params?: never }
100
+ * ```
101
+ */
102
+ type PatternConfig<TPath extends Path> = {
103
+ query?: Query;
104
+ } & (Solas.Routes[TPath] extends {
105
+ params: infer TParams extends Params;
106
+ } ? {
107
+ params: TParams;
108
+ } : {
109
+ params?: never;
110
+ });
111
+ /**
112
+ * Typed <Link /> props, using `href` instead of the internal `to` name
113
+ *
114
+ * `query` is always allowed
115
+ * `params` are only allowed when `href` is a known route pattern
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * const a: LinkProps = { href: '/posts/:id', params: { id: '123' } }
120
+ * const b: LinkProps = { href: '/posts/123?draft=true' }
121
+ * ```
122
+ */
123
+ export type LinkProps = ({
124
+ href: Target;
125
+ } & TargetConfig) | (keyof Solas.Routes extends never ? never : {
126
+ [TPath in Path]: {
127
+ href: TPath;
128
+ } & PatternConfig<TPath>;
129
+ }[Path]);
130
+ /**
131
+ * Typed input for router.go(), using the same route rules as <Link />
132
+ *
133
+ * @example
134
+ * ```ts
135
+ * go('/p/post-2')
136
+ * go('/?foo=bar', { replace: true })
137
+ * ```
138
+ *
139
+ * @example
140
+ * ```ts
141
+ * go('/p/:id', { params: { id: 'post-2' }, replace: true })
142
+ * ```
143
+ *
144
+ * The last overload is the fallback for plain `string` values. The
145
+ * `string extends TTo` check stops that fallback from taking over
146
+ * when TypeScript already knows the caller passed a more specific
147
+ * string literal
148
+ *
149
+ * @example
150
+ * ```ts
151
+ * declare const dynamicPath: string
152
+ * go(dynamicPath, { replace: true })
153
+ * ```
154
+ */
155
+ export type Go = {
156
+ <TTo extends Path>(to: TTo, opts?: PatternConfig<TTo> & Replace): Promise<string>;
157
+ <TTo extends Target>(to: TTo, opts?: TargetConfig & Replace): Promise<string>;
158
+ <TTo extends string>(to: string extends TTo ? TTo : never, opts?: GoOptions): Promise<string>;
159
+ };
160
+ /**
161
+ * Convert a route pattern and params into a real path string. This is used internally
162
+ * to implement <Link /> and router.go
163
+ */
164
+ export function toTarget(path: string, params?: Record<string, string>, query?: BrowserRouter.Query): string;
165
+ export {};
166
+ }
167
+ export declare const BrowserRouterContext: import("react").Context<{
168
+ go: BrowserRouter.Go;
169
+ prefetch: (path: string) => void;
170
+ isNavigating: boolean;
171
+ url: {
172
+ pathname?: string | undefined;
173
+ search?: string | undefined;
174
+ };
175
+ }>;
176
+ export declare function BrowserRouterProvider({ children, setPayload, isNavigating, url }: {
177
+ children: React.ReactNode;
178
+ setPayload?: (payload: RscPayload) => void;
179
+ isNavigating?: boolean;
180
+ url?: {
181
+ pathname?: string;
182
+ search?: string;
183
+ };
184
+ }): import("react/jsx-runtime").JSX.Element;
@@ -1,17 +1,85 @@
1
1
  'use client';
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- import { useCallback, useEffect, useMemo, useRef } from 'react';
3
+ import { createContext, useCallback, useEffect, useMemo, useRef } from 'react';
4
4
  import { createFromFetch } from '@vitejs/plugin-rsc/browser';
5
5
  import { Logger } from '../../utils/logger.js';
6
6
  import { Solas } from '../../solas.js';
7
- import { Prefetcher } from './prefetcher.js';
8
- import { RouterContext } from './router-context.js';
7
+ import { Prefetcher } from './../prefetcher.js';
8
+ export { BrowserRouter };
9
+ var BrowserRouter;
10
+ (function (BrowserRouter) {
11
+ function isHashOnlyTarget(target) {
12
+ return target.startsWith('#');
13
+ }
14
+ BrowserRouter.isHashOnlyTarget = isHashOnlyTarget;
15
+ function isExternalTarget(target, origin) {
16
+ if (isHashOnlyTarget(target))
17
+ return false;
18
+ try {
19
+ return new URL(target, origin).origin !== origin;
20
+ }
21
+ catch {
22
+ return false;
23
+ }
24
+ }
25
+ BrowserRouter.isExternalTarget = isExternalTarget;
26
+ /**
27
+ * Convert a route pattern and params into a real path string. This is used internally
28
+ * to implement <Link /> and router.go
29
+ */
30
+ function toTarget(path, params, query) {
31
+ // keep track of which params were consumed by named `:param` slots
32
+ const used = new Set();
33
+ // replace each named route param with its URL-encoded value
34
+ let to = path.replaceAll(/:([A-Za-z0-9_]+)/g, (_, key) => {
35
+ const value = params?.[key];
36
+ if (value == null) {
37
+ throw new Error(`[Link]: missing route param: ${key}`);
38
+ }
39
+ used.add(key);
40
+ return encodeURIComponent(value);
41
+ });
42
+ if (to.includes('*')) {
43
+ // wildcard routes use the one param that was not already matched by a named slot
44
+ const remaining = Object.entries(params ?? {}).filter(([key]) => !used.has(key));
45
+ if (remaining.length !== 1) {
46
+ throw new Error('[Link]: wildcard routes require exactly one unmatched param');
47
+ }
48
+ // encode each path segment separately so embedded '/' still acts like a path separator
49
+ to = to.replace('*', remaining[0][1].split('/').map(encodeURIComponent).join('/'));
50
+ }
51
+ if (!query)
52
+ return to;
53
+ // split the URL up so new query params can be merged without losing an existing hash
54
+ const hashIndex = to.indexOf('#');
55
+ const hash = hashIndex >= 0 ? to.slice(hashIndex) : '';
56
+ const pathWithSearch = hashIndex >= 0 ? to.slice(0, hashIndex) : to;
57
+ const searchIndex = pathWithSearch.indexOf('?');
58
+ const pathname = searchIndex >= 0 ? pathWithSearch.slice(0, searchIndex) : pathWithSearch;
59
+ const currentSearch = searchIndex >= 0 ? pathWithSearch.slice(searchIndex + 1) : '';
60
+ const search = new URLSearchParams(currentSearch);
61
+ // later values win, so passed query props overwrite any existing query string values
62
+ for (const [key, value] of Object.entries(query)) {
63
+ search.set(key, String(value));
64
+ }
65
+ const value = search.toString();
66
+ // rebuild the URL in the same order: pathname, optional query string, then hash
67
+ return `${pathname}${value.length > 0 ? `?${value}` : ''}${hash}`;
68
+ }
69
+ BrowserRouter.toTarget = toTarget;
70
+ })(BrowserRouter || (BrowserRouter = {}));
71
+ export const BrowserRouterContext = createContext({
72
+ go: async () => '',
73
+ prefetch: () => { },
74
+ isNavigating: false,
75
+ url: {},
76
+ });
9
77
  const DEFAULT_GO_CONFIG = {
10
78
  replace: false,
11
79
  };
12
80
  const logger = new Logger();
13
81
  const prefetcher = new Prefetcher();
14
- export function RouterProvider({ children, setPayload, isNavigating = false, url, }) {
82
+ export function BrowserRouterProvider({ children, setPayload, isNavigating = false, url, }) {
15
83
  // id to track active navigations
16
84
  const id = useRef(0);
17
85
  // abort controller for in-flight navigation
@@ -35,16 +103,15 @@ export function RouterProvider({ children, setPayload, isNavigating = false, url
35
103
  // opportunistically for this navigation
36
104
  let existing = false;
37
105
  try {
38
- const url = new URL(to, window.location.origin);
39
- if (opts?.query) {
40
- for (const [key, value] of Object.entries(opts.query)) {
41
- url.searchParams.set(key, String(value));
42
- }
106
+ const target = BrowserRouter.toTarget(to, opts.params, opts.query);
107
+ if (BrowserRouter.isExternalTarget(target, window.location.origin)) {
108
+ throw new Error('[router.go]: external URLs are not supported. Use <a> instead');
43
109
  }
110
+ const url = new URL(target, window.location.origin);
44
111
  const key = Prefetcher.key(url.toString(), window.location.origin);
45
112
  if (!key)
46
113
  throw new Error('Invalid navigation url');
47
- // switch to the normalized target once the url is valid
114
+ // switch to the normalised target once the url is valid
48
115
  path = key;
49
116
  // if the target was already prefetched, use the cached response promise
50
117
  // and set existing to true so we don't remove it from cache
@@ -128,7 +195,9 @@ export function RouterProvider({ children, setPayload, isNavigating = false, url
128
195
  prefetcher.set(key, fetch(key, { headers: { Accept: 'text/x-component' } }));
129
196
  }, []);
130
197
  useEffect(() => {
131
- const handler = () => go(window.location.href, { replace: true });
198
+ const handler = () => go(BrowserRouter.toTarget(window.location.pathname + window.location.search), {
199
+ replace: true,
200
+ });
132
201
  window.addEventListener('popstate', handler);
133
202
  return () => {
134
203
  controller.current?.abort();
@@ -145,5 +214,5 @@ export function RouterProvider({ children, setPayload, isNavigating = false, url
145
214
  search: url?.search,
146
215
  },
147
216
  }), [go, prefetch, isNavigating, url]);
148
- return _jsx(RouterContext, { value: value, children: children });
217
+ return _jsx(BrowserRouterContext, { value: value, children: children });
149
218
  }
@@ -1,5 +1,5 @@
1
1
  export declare function useRouter(): {
2
- go: (to: string, opts?: import("./router-context.js").Navigation.GoOptions | undefined) => Promise<string>;
2
+ go: import("./router.js").BrowserRouter.Go;
3
3
  prefetch: (path: string) => void;
4
4
  isNavigating: boolean;
5
5
  url: {
@@ -0,0 +1,5 @@
1
+ import { use } from 'react';
2
+ import { BrowserRouterContext } from './router.js';
3
+ export function useRouter() {
4
+ return use(BrowserRouterContext);
5
+ }
@@ -1,6 +1,6 @@
1
1
  import { useMemo, useSyncExternalStore } from 'react';
2
- import { useRouter } from '../../router.js';
3
2
  import { Solas } from '../../solas.js';
3
+ import { useRouter } from '../browser-router/use-router.js';
4
4
  export function useSearchParams() {
5
5
  const { url } = useRouter();
6
6
  const search = useSyncExternalStore(fn => {
@@ -1,8 +1,8 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { Solas } from '../solas.js';
4
3
  import { Logger } from '../utils/logger.js';
5
- import { normalisePathname } from './router/utils.js';
4
+ import { Solas } from '../solas.js';
5
+ import { normalisePathname } from './http-router/utils.js';
6
6
  import { Prerender } from './prerender.js';
7
7
  export { Build };
8
8
  /**
@@ -1,19 +1,28 @@
1
1
  import { Solas } from '../../solas.js';
2
- import { AUTOGEN_MSG, toSourceLiteral } from './utils.js';
2
+ import { AUTOGEN_MSG, source, toSourceLiteral } from './utils.js';
3
3
  /**
4
4
  * Generates the code to create an exported config object
5
5
  */
6
6
  export function writeConfig(config) {
7
- return `
7
+ const loggerLevel = config.logger?.level;
8
+ const importLines = [
9
+ `import type { PluginConfig } from '${Solas.Config.PKG_NAME}'`,
10
+ loggerLevel ? `import { Logger } from '${Solas.Config.PKG_NAME}/utils/logger'` : '',
11
+ ]
12
+ .filter(Boolean)
13
+ .join('\n');
14
+ const configStatement = `const config = ${toSourceLiteral(config)} as const satisfies PluginConfig`;
15
+ const loggerStatement = loggerLevel
16
+ ? `Logger.defaultLevel = ${toSourceLiteral(loggerLevel)}`
17
+ : '';
18
+ return source `
8
19
  ${AUTOGEN_MSG}
9
20
 
10
- import type { PluginConfig } from '${Solas.Config.PKG_NAME}'
11
- import { Logger } from '${Solas.Config.PKG_NAME}/utils/logger'
21
+ ${importLines}
12
22
 
13
- const config = ${toSourceLiteral(config)} as const satisfies PluginConfig
14
-
15
- if (config.logger?.level) Logger.defaultLevel = config.logger.level
23
+ ${configStatement}
24
+ ${loggerStatement}
16
25
 
17
26
  export { config }
18
- `.trim();
27
+ `;
19
28
  }
@@ -1,10 +1,10 @@
1
1
  import { Solas } from '../../solas.js';
2
- import { AUTOGEN_MSG } from './utils.js';
2
+ import { AUTOGEN_MSG, source } from './utils.js';
3
3
  /**
4
4
  * Generates the RSC entry code
5
5
  */
6
6
  export function writeRSCEntry() {
7
- return `
7
+ return source `
8
8
  ${AUTOGEN_MSG}
9
9
 
10
10
  import { createHandler } from '${Solas.Config.PKG_NAME}/env/rsc'
@@ -20,27 +20,27 @@ export function writeRSCEntry() {
20
20
  export default createHandler(config, manifest, importMap, artifactManifest)
21
21
 
22
22
  import.meta.hot?.accept()
23
- `.trim();
23
+ `;
24
24
  }
25
25
  /**
26
26
  * Generates the SSR entry code
27
27
  */
28
28
  export function writeSSREntry() {
29
- return `
29
+ return source `
30
30
  ${AUTOGEN_MSG}
31
31
 
32
32
  export { prerender, resume, ssr } from '${Solas.Config.PKG_NAME}/env/ssr'
33
- `.trim();
33
+ `;
34
34
  }
35
35
  /**
36
36
  * Generates the browser entry code
37
37
  */
38
38
  export function writeBrowserEntry() {
39
- return `
39
+ return source `
40
40
  ${AUTOGEN_MSG}
41
41
 
42
42
  import { browser } from '${Solas.Config.PKG_NAME}/env/browser'
43
43
 
44
44
  browser()
45
- `.trim();
45
+ `;
46
46
  }
@@ -1,14 +1,14 @@
1
1
  import { Solas } from '../../solas.js';
2
- import { AUTOGEN_MSG, toSourceLiteral } from './utils.js';
2
+ import { AUTOGEN_MSG, source, toSourceLiteral } from './utils.js';
3
3
  /**
4
4
  * Generates the code to create an exported manifest object
5
5
  */
6
6
  export function writeManifest(manifest) {
7
- return `
7
+ return source `
8
8
  ${AUTOGEN_MSG}
9
9
 
10
10
  import type { Manifest } from '${Solas.Config.PKG_NAME}'
11
11
 
12
12
  export const manifest = ${toSourceLiteral(manifest)} as const satisfies Manifest
13
- `.trim();
13
+ `;
14
14
  }
@@ -1,5 +1,5 @@
1
1
  import { Solas } from '../../solas.js';
2
- import { AUTOGEN_MSG, toIdentifier, toIdentifierList, toRelativeModuleSpecifier, toStringLiteral, } from './utils.js';
2
+ import { AUTOGEN_MSG, indent, source, toIdentifier, toIdentifierList, toRelativeModuleSpecifier, toStringLiteral, } from './utils.js';
3
3
  /**
4
4
  * Generates the import map for all route components, endpoints, layouts, shells, and middlewares
5
5
  */
@@ -61,26 +61,22 @@ export function writeMaps(imports, modules) {
61
61
  parts.push(`middlewares: [${middleware}]`);
62
62
  }
63
63
  if (parts.length === 0)
64
- return `\t${toStringLiteral(moduleId)}: {}`;
65
- return `\t${toStringLiteral(moduleId)}: {\n\t\t${parts.join(',\n\t\t')}\n\t}`;
64
+ return `${toStringLiteral(moduleId)}: {}`;
65
+ return `${toStringLiteral(moduleId)}: {\n${parts.map(part => indent(part, 1)).join(',\n')}\n}`;
66
66
  });
67
- return `
67
+ const importLines = [...statics, ...dynamics].join('\n');
68
+ const entries = map.map(entry => indent(entry, 1)).join(',\n');
69
+ return source `
68
70
  ${AUTOGEN_MSG}
69
71
 
70
72
  import type { ImportMap } from '${Solas.Config.PKG_NAME}'
71
-
72
- ${statics.length
73
- ? `${statics.join('\n')}
74
-
75
- `
76
- : ''}${dynamics.length
77
- ? `${dynamics.join('\n')}
78
-
79
- `
73
+ ${importLines
74
+ ? `
75
+ ${importLines}`
80
76
  : ''}
81
77
 
82
78
  export const importMap = {
83
- ${map.join(',\n')}
79
+ ${entries}
84
80
  } as const satisfies ImportMap
85
- `.trim();
81
+ `;
86
82
  }
@@ -0,0 +1,5 @@
1
+ import { Manifest } from '../../types.js';
2
+ /**
3
+ * Generates runtime types
4
+ */
5
+ export declare function writeTypes(manifest: Manifest): string;
@@ -0,0 +1,48 @@
1
+ import { Solas } from '../../solas.js';
2
+ import { Build } from '../build.js';
3
+ import { AUTOGEN_MSG, source } from './utils.js';
4
+ function render(path, params) {
5
+ if (!params || params.length === 0) {
6
+ return `\t\t\t'${path}': {}`;
7
+ }
8
+ const fields = params.map(param => `\t\t\t\t\t${param}: string`).join('\n');
9
+ return [`\t\t\t'${path}': {`, `\t\t\t\tparams: {`, fields, `\t\t\t\t}`, `\t\t\t}`].join('\n');
10
+ }
11
+ /**
12
+ * Generates runtime types
13
+ */
14
+ export function writeTypes(manifest) {
15
+ const routes = new Map();
16
+ for (const path in manifest) {
17
+ const route = manifest[path];
18
+ if (Array.isArray(route)) {
19
+ for (const r of route) {
20
+ if (r.__kind !== Build.EntryKind.PAGE)
21
+ continue;
22
+ routes.set(r.__path, r.__params);
23
+ }
24
+ }
25
+ else {
26
+ if (route.__kind !== Build.EntryKind.PAGE)
27
+ continue;
28
+ routes.set(route.__path, route.__params);
29
+ }
30
+ }
31
+ const body = [...routes.entries()]
32
+ // sort routes by path for stable output
33
+ .toSorted(([a], [b]) => a.localeCompare(b))
34
+ .map(([path, params]) => render(path, params))
35
+ .join('\n');
36
+ return source `
37
+ ${AUTOGEN_MSG}
38
+
39
+ import '${Solas.Config.PKG_NAME}'
40
+
41
+ declare module '${Solas.Config.PKG_NAME}' {
42
+ export namespace Solas {
43
+ export interface Routes {
44
+ ${body}
45
+ }
46
+ }
47
+ }`;
48
+ }
@@ -15,7 +15,17 @@ export declare function toIdentifierList(values: readonly (string | null)[], lab
15
15
  * Escape text into a safe string literal for generated source
16
16
  */
17
17
  export declare function toStringLiteral(value: string, quoteStyle?: "'" | '"'): string;
18
+ /**
19
+ * Dedent an interpolated template literal while preserving indentation for
20
+ * multiline substitutions
21
+ */
22
+ export declare function source(strings: TemplateStringsArray, ...values: Array<string | number | boolean | false | null | undefined>): string;
18
23
  /**
19
24
  * Emit readable ts source for generated config and manifest data
20
25
  */
21
26
  export declare function toSourceLiteral(value: unknown, level?: number): string;
27
+ /**
28
+ * Indent each line of a block of source code by the specified level for embedding in
29
+ * generated output
30
+ */
31
+ export declare function indent(value: string, level?: number): string;
@@ -60,6 +60,31 @@ export function toStringLiteral(value, quoteStyle = "'") {
60
60
  .replace(/\r/g, '\\r')
61
61
  .replace(/\t/g, '\\t')}${quoteStyle}`;
62
62
  }
63
+ /**
64
+ * Dedent an interpolated template literal while preserving indentation for
65
+ * multiline substitutions
66
+ */
67
+ export function source(strings, ...values) {
68
+ let text = strings[0] ?? '';
69
+ for (let index = 0; index < values.length; index += 1) {
70
+ const value = values[index];
71
+ const indentation = text.match(/(?:^|\n)([ \t]*)$/)?.[1] ?? '';
72
+ const chunk = value === false || value == null ? '' : String(value);
73
+ text += chunk.replace(/\n/g, `\n${indentation}`);
74
+ text += strings[index + 1] ?? '';
75
+ }
76
+ const lines = text.replace(/^\n+|\n+$/g, '').split('\n');
77
+ const margin = lines
78
+ .filter(line => line.trim().length > 0)
79
+ .reduce((smallest, line) => {
80
+ const indentation = line.match(/^[ \t]*/)?.[0].length ?? 0;
81
+ return Math.min(smallest, indentation);
82
+ }, Number.POSITIVE_INFINITY);
83
+ if (!Number.isFinite(margin) || margin === 0) {
84
+ return lines.join('\n');
85
+ }
86
+ return lines.map(line => line.slice(margin)).join('\n');
87
+ }
63
88
  /**
64
89
  * Convert a string into a valid unquoted property key if possible, otherwise quote it
65
90
  * as a string literal for generated source
@@ -120,7 +145,7 @@ export function toSourceLiteral(value, level = 0) {
120
145
  }
121
146
  return `${prefix}${source.replace(/\n/g, `\n${INDENT.repeat(level + 1)}`)}`;
122
147
  }
123
- return `${prefix}${toSourceLiteral(entryValue, level + 1).replace(/\n/g, `\n${INDENT.repeat(level + 1)}`)}`;
148
+ return `${prefix}${toSourceLiteral(entryValue, level + 1)}`;
124
149
  })
125
150
  .join(',\n'),
126
151
  `${INDENT.repeat(level)}}`,
@@ -132,7 +157,7 @@ export function toSourceLiteral(value, level = 0) {
132
157
  * Indent each line of a block of source code by the specified level for embedding in
133
158
  * generated output
134
159
  */
135
- function indent(value, level = 1) {
160
+ export function indent(value, level = 1) {
136
161
  const prefix = INDENT.repeat(level);
137
162
  return value
138
163
  .split('\n')
@@ -2,18 +2,18 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { StrictMode, Suspense, useCallback, useState, useTransition } from 'react';
3
3
  import { hydrateRoot } from 'react-dom/client';
4
4
  import { createFromFetch, createFromReadableStream, createTemporaryReferenceSet, encodeReply, setServerCallback, } from '@vitejs/plugin-rsc/browser';
5
- import { rscStream } from 'rsc-html-stream/client';
5
+ import { BrowserRouterProvider } from '../browser-router/router.js';
6
6
  import { RedirectBoundary } from '../navigation/redirect-boundary.js';
7
7
  import { Head } from '../render/head.js';
8
- import { RouterProvider } from '../router/router-provider.js';
9
8
  import { ErrorBoundary } from '../ui/error-boundary.js';
9
+ import { rscStream } from './flight.js';
10
10
  /**
11
11
  * Browser RSC hydration entry point
12
12
  */
13
13
  export async function browser() {
14
- const payload = await createFromReadableStream(rscStream, {
15
- unstable_allowPartialStream: true,
16
- });
14
+ // read the initial payload from the inline __FLIGHT_DATA pushes that were
15
+ // injected into the html document during ssr/prerender
16
+ const payload = await createFromReadableStream(rscStream);
17
17
  const payloadSetter = {
18
18
  current: () => { },
19
19
  };
@@ -28,7 +28,7 @@ export async function browser() {
28
28
  // make the latest payload updater available to action/hmr callbacks
29
29
  // immediately during render, without waiting for an effect to run
30
30
  payloadSetter.current = setPayloadInTransition;
31
- return (_jsx(RedirectBoundary, { children: _jsxs(RouterProvider, { setPayload: setPayloadInTransition, isNavigating: isPending, url: p.url, children: [
31
+ return (_jsx(RedirectBoundary, { children: _jsxs(BrowserRouterProvider, { setPayload: setPayloadInTransition, isNavigating: isPending, url: p.url, children: [
32
32
  _jsx(ErrorBoundary, { fallback: null, children: _jsx(Suspense, { fallback: null, children: _jsx(Head, { metadata: p.metadata }) }) }), p.root] }) }));
33
33
  }
34
34
  setServerCallback(async (id, args) => {