@jk2908/solas 0.3.7 → 0.4.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 (55) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +66 -6
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +75 -6
  5. package/dist/internal/browser-router/link.d.ts +1 -1
  6. package/dist/internal/browser-router/link.js +1 -1
  7. package/dist/internal/browser-router/router.d.ts +2 -165
  8. package/dist/internal/browser-router/router.js +3 -99
  9. package/dist/internal/browser-router/shared.d.ts +169 -0
  10. package/dist/internal/browser-router/shared.js +71 -0
  11. package/dist/internal/browser-router/use-router.d.ts +1 -1
  12. package/dist/internal/build.js +14 -14
  13. package/dist/internal/codegen/environments.js +5 -4
  14. package/dist/internal/env/browser.js +11 -9
  15. package/dist/internal/env/rsc.d.ts +2 -2
  16. package/dist/internal/env/rsc.js +170 -86
  17. package/dist/internal/http-router/create-http-router.d.ts +1 -1
  18. package/dist/internal/http-router/create-http-router.js +4 -2
  19. package/dist/internal/http-router/router.d.ts +4 -14
  20. package/dist/internal/http-router/router.js +32 -59
  21. package/dist/internal/navigation/http-exception.d.ts +8 -4
  22. package/dist/internal/navigation/http-exception.js +46 -6
  23. package/dist/internal/postbuild.d.ts +1 -0
  24. package/dist/{cli/build.js → internal/postbuild.js} +13 -48
  25. package/dist/internal/prerender.d.ts +4 -19
  26. package/dist/internal/prerender.js +8 -98
  27. package/dist/internal/public-files.d.ts +18 -0
  28. package/dist/internal/public-files.js +63 -0
  29. package/dist/internal/render/tree.d.ts +0 -3
  30. package/dist/internal/render/tree.js +1 -6
  31. package/dist/internal/resolver.d.ts +31 -23
  32. package/dist/internal/server/actions.d.ts +2 -5
  33. package/dist/internal/server/actions.js +4 -35
  34. package/dist/internal/server/csrf.d.ts +14 -0
  35. package/dist/internal/server/csrf.js +98 -0
  36. package/dist/internal/ui/defaults/error.d.ts +2 -0
  37. package/dist/internal/ui/defaults/error.js +1 -1
  38. package/dist/navigation.d.ts +1 -1
  39. package/dist/router.d.ts +1 -0
  40. package/dist/router.js +1 -0
  41. package/dist/solas.d.ts +12 -1
  42. package/dist/solas.js +116 -1
  43. package/dist/types.d.ts +27 -5
  44. package/dist/utils/base-path.d.ts +14 -0
  45. package/dist/utils/base-path.js +85 -0
  46. package/dist/utils/export-reader.d.ts +6 -1
  47. package/dist/utils/export-reader.js +24 -15
  48. package/package.json +3 -6
  49. package/dist/cli/build.d.ts +0 -7
  50. package/dist/cli/dev.d.ts +0 -4
  51. package/dist/cli/dev.js +0 -13
  52. package/dist/cli/preview.d.ts +0 -1
  53. package/dist/cli/preview.js +0 -47
  54. package/dist/cli.d.ts +0 -2
  55. package/dist/cli.js +0 -28
@@ -0,0 +1,169 @@
1
+ import { Solas } from '../../solas.js';
2
+ export declare namespace BrowserRouter {
3
+ export type Params = Record<string, string>;
4
+ export type Query = Record<string, string | number | boolean>;
5
+ export type Path = keyof Solas.Routes & string;
6
+ type Replace = {
7
+ replace?: boolean;
8
+ };
9
+ export type GoOptions = {
10
+ replace?: boolean;
11
+ query?: Query;
12
+ params?: Params;
13
+ };
14
+ /**
15
+ * These targets are used as-is. They are not matched against the route table,
16
+ * so this covers normal external URLs and hash-only links
17
+ */
18
+ export type ExternalTarget = `${string}:${string}` | `//${string}` | `#${string}`;
19
+ export function isHashOnlyTarget(target: string): boolean;
20
+ export function isExternalTarget(target: string, origin: string): boolean;
21
+ /**
22
+ * Turn a route pattern into the real path shape a caller can use. In practice,
23
+ * every ':param' or '*' part becomes a plain string slot
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * // '/p/:id' becomes '/p/${string}'
28
+ * // '/test/*' becomes '/test/${string}'
29
+ * // '/posts' stays '/posts'
30
+ * ```
31
+ *
32
+ * @example
33
+ * ```ts
34
+ * type A = ResolvedPath<'/posts/:id'>
35
+ * // '/posts/${string}'
36
+ * ```
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * type B = ResolvedPath<'/docs/*'>
41
+ * // '/docs/${string}'
42
+ * ```
43
+ */
44
+ 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;
45
+ /**
46
+ * Once we have a real path, also allow the usual query-string and hash forms
47
+ *
48
+ * @example
49
+ * ```ts
50
+ * type A = TargetSuffix<'/posts/123'>
51
+ * // '/posts/123' | '/posts/123?${string}' | '/posts/123#${string}' | '/posts/123?${string}#${string}'
52
+ * ```
53
+ */
54
+ export type TargetSuffix<TPath extends string> = TPath | `${TPath}?${string}` | `${TPath}#${string}` | `${TPath}?${string}#${string}`;
55
+ /**
56
+ * This is the final string form a caller can navigate to. It can be an external
57
+ * URL, or a concrete URL that matches one of the known routes
58
+ *
59
+ * @example
60
+ * ```ts
61
+ * type A = Target
62
+ * // 'https://example.com'
63
+ * // '#intro'
64
+ * // '/posts/123'
65
+ * // '/posts/123?draft=true'
66
+ * ```
67
+ */
68
+ export type Target = ExternalTarget | TargetSuffix<ResolvedPath<Path>>;
69
+ /**
70
+ * Extra options for callers who already have a finished target string
71
+ *
72
+ * @example
73
+ * ```ts
74
+ * const a: TargetConfig = { query: { page: 2 } }
75
+ * // params is rejected here because the path is already complete
76
+ * ```
77
+ */
78
+ type TargetConfig = {
79
+ params?: never;
80
+ query?: Query;
81
+ };
82
+ /**
83
+ * Extra options for callers who pass a route pattern and params separately.
84
+ * If the route definition says that route needs params, this type makes
85
+ * those params required. If the route has no params, it rejects them
86
+ *
87
+ * @example
88
+ * ```ts
89
+ * // if Solas.Routes['/posts/:id'] is { params: { id: string } }
90
+ * type A = PatternConfig<'/posts/:id'>
91
+ * // { query?: Query } & { params: { id: string } }
92
+ * ```
93
+ *
94
+ * @example
95
+ * ```ts
96
+ * // if Solas.Routes['/about'] has no params field
97
+ * type B = PatternConfig<'/about'>
98
+ * // { query?: Query } & { params?: never }
99
+ * ```
100
+ */
101
+ type PatternConfig<TPath extends Path> = {
102
+ query?: Query;
103
+ } & (Solas.Routes[TPath] extends {
104
+ params: infer TParams extends Params;
105
+ } ? {
106
+ params: TParams;
107
+ } : {
108
+ params?: never;
109
+ });
110
+ /**
111
+ * Typed <Link /> props, using `href` instead of the internal `to` name
112
+ *
113
+ * `query` is always allowed
114
+ * `params` are only allowed when `href` is a known route pattern
115
+ *
116
+ * @example
117
+ * ```ts
118
+ * const a: LinkProps = { href: '/posts/:id', params: { id: '123' } }
119
+ * const b: LinkProps = { href: '/posts/123?draft=true' }
120
+ * ```
121
+ */
122
+ export type LinkProps = ({
123
+ href: Target;
124
+ } & TargetConfig) | (keyof Solas.Routes extends never ? never : {
125
+ [TPath in Path]: {
126
+ href: TPath;
127
+ } & PatternConfig<TPath>;
128
+ }[Path]);
129
+ /**
130
+ * Typed input for router.go(), using the same route rules as <Link />
131
+ *
132
+ * @example
133
+ * ```ts
134
+ * go('/p/post-2')
135
+ * go('/?foo=bar', { replace: true })
136
+ * ```
137
+ *
138
+ * @example
139
+ * ```ts
140
+ * go('/p/:id', { params: { id: 'post-2' }, replace: true })
141
+ * ```
142
+ *
143
+ * The last overload is the fallback for plain `string` values. The
144
+ * `string extends TTo` check stops that fallback from taking over
145
+ * when TypeScript already knows the caller passed a more specific
146
+ * string literal
147
+ *
148
+ * @example
149
+ * ```ts
150
+ * declare const dynamicPath: string
151
+ * go(dynamicPath, { replace: true })
152
+ * ```
153
+ */
154
+ export type Go = {
155
+ <TTo extends Path>(to: TTo, opts?: PatternConfig<TTo> & Replace): Promise<string>;
156
+ <TTo extends Target>(to: TTo, opts?: TargetConfig & Replace): Promise<string>;
157
+ <TTo extends string>(to: string extends TTo ? TTo : never, opts?: GoOptions): Promise<string>;
158
+ };
159
+ /**
160
+ * Convert a route pattern and params into a real path string. This is used internally
161
+ * to implement <Link /> and router.go
162
+ */
163
+ export function toTarget(path: string, params?: Record<string, string>, query?: Query): string;
164
+ export {};
165
+ }
166
+ /**
167
+ * Apply the base path to a target string when needed
168
+ */
169
+ export declare function withBase(target: string): string;
@@ -0,0 +1,71 @@
1
+ import { BasePath } from '../../utils/base-path.js';
2
+ const BASE_PATH = BasePath.normalise(import.meta.env.BASE_URL);
3
+ export { BrowserRouter };
4
+ var BrowserRouter;
5
+ (function (BrowserRouter) {
6
+ function isHashOnlyTarget(target) {
7
+ return target.startsWith('#');
8
+ }
9
+ BrowserRouter.isHashOnlyTarget = isHashOnlyTarget;
10
+ function isExternalTarget(target, origin) {
11
+ if (isHashOnlyTarget(target))
12
+ return false;
13
+ try {
14
+ return new URL(target, origin).origin !== origin;
15
+ }
16
+ catch {
17
+ return false;
18
+ }
19
+ }
20
+ BrowserRouter.isExternalTarget = isExternalTarget;
21
+ /**
22
+ * Convert a route pattern and params into a real path string. This is used internally
23
+ * to implement <Link /> and router.go
24
+ */
25
+ function toTarget(path, params, query) {
26
+ const used = new Set();
27
+ let to = path.replaceAll(/:([A-Za-z0-9_]+)/g, (_, key) => {
28
+ const value = params?.[key];
29
+ if (value == null) {
30
+ throw new Error(`[Link]: missing route param: ${key}`);
31
+ }
32
+ used.add(key);
33
+ return encodeURIComponent(value);
34
+ });
35
+ if (to.includes('*')) {
36
+ const remaining = Object.entries(params ?? {}).filter(([key]) => !used.has(key));
37
+ if (remaining.length !== 1) {
38
+ throw new Error('[Link]: wildcard routes require exactly one unmatched param');
39
+ }
40
+ to = to.replace('*', remaining[0][1].split('/').map(encodeURIComponent).join('/'));
41
+ }
42
+ if (!query)
43
+ return withBase(to);
44
+ const hashIndex = to.indexOf('#');
45
+ const hash = hashIndex >= 0 ? to.slice(hashIndex) : '';
46
+ const pathWithSearch = hashIndex >= 0 ? to.slice(0, hashIndex) : to;
47
+ const searchIndex = pathWithSearch.indexOf('?');
48
+ const pathname = searchIndex >= 0 ? pathWithSearch.slice(0, searchIndex) : pathWithSearch;
49
+ const currentSearch = searchIndex >= 0 ? pathWithSearch.slice(searchIndex + 1) : '';
50
+ const search = new URLSearchParams(currentSearch);
51
+ for (const [key, value] of Object.entries(query)) {
52
+ search.set(key, String(value));
53
+ }
54
+ const value = search.toString();
55
+ return withBase(`${pathname}${value.length > 0 ? `?${value}` : ''}${hash}`);
56
+ }
57
+ BrowserRouter.toTarget = toTarget;
58
+ })(BrowserRouter || (BrowserRouter = {}));
59
+ /**
60
+ * Apply the base path to a target string when needed
61
+ */
62
+ export function withBase(target) {
63
+ if (BrowserRouter.isHashOnlyTarget(target))
64
+ return target;
65
+ if (target.startsWith('//') || /^[A-Za-z][A-Za-z\d+.-]*:/.test(target))
66
+ return target;
67
+ const suffixIndex = target.search(/[?#]/);
68
+ const pathname = suffixIndex === -1 ? target : target.slice(0, suffixIndex);
69
+ const suffix = suffixIndex === -1 ? '' : target.slice(suffixIndex);
70
+ return `${BasePath.apply(pathname || '/', BASE_PATH)}${suffix}`;
71
+ }
@@ -1,5 +1,5 @@
1
1
  export declare function useRouter(): {
2
- go: import("./router.js").BrowserRouter.Go;
2
+ go: import("./shared.js").BrowserRouter.Go;
3
3
  prefetch: (path: string) => void;
4
4
  isNavigating: boolean;
5
5
  url: {
@@ -324,7 +324,7 @@ var Build;
324
324
  try {
325
325
  if (!this.buildContext || !this.config)
326
326
  continue;
327
- const { shell: shellPath, layouts: layoutPaths, '401s': unauthorizedPaths, '403s': forbiddenPaths, page: pagePath, '404s': notFoundPaths, '500s': serverErrorPaths, loaders: loaderPaths, middlewares: middlewarePaths, dir, } = segment;
327
+ const { shell: shellPath, layouts: layoutPaths, '401s': unauthorisedPaths, '403s': forbiddenPaths, page: pagePath, '404s': notFoundPaths, '500s': serverErrorPaths, loaders: loaderPaths, middlewares: middlewarePaths, dir, } = segment;
328
328
  // derive the route pattern from the directory path, falling
329
329
  // back to a synthetic +page.tsx when this is
330
330
  // a layout-only segment
@@ -347,7 +347,7 @@ var Build;
347
347
  const shellImport = Finder.getImportPath(shellPath);
348
348
  const shellId = `${Build.EntryKind.SHELL}${Bun.hash(shellImport)}`;
349
349
  const layoutIds = [];
350
- const unauthorizedIds = [];
350
+ const unauthorisedIds = [];
351
351
  const forbiddenIds = [];
352
352
  const notFoundIds = [];
353
353
  const serverErrorIds = [];
@@ -375,17 +375,17 @@ var Build;
375
375
  applyPrerenderMode(prerenderCache.get(layoutPath));
376
376
  layoutIds.push(layoutId);
377
377
  }
378
- for (const unauthorizedPath of unauthorizedPaths) {
379
- if (!unauthorizedPath) {
380
- unauthorizedIds.push(null);
378
+ for (const unauthorisedPath of unauthorisedPaths) {
379
+ if (!unauthorisedPath) {
380
+ unauthorisedIds.push(null);
381
381
  continue;
382
382
  }
383
- const unauthorizedImport = Finder.getImportPath(unauthorizedPath);
384
- const unauthorizedId = `${Build.EntryKind['401']}${Bun.hash(unauthorizedImport)}`;
385
- unauthorizedIds.push(unauthorizedId);
386
- if (!processed.has(unauthorizedPath)) {
387
- imports.components.dynamic.set(unauthorizedId, unauthorizedImport);
388
- processed.add(unauthorizedPath);
383
+ const unauthorisedImport = Finder.getImportPath(unauthorisedPath);
384
+ const unauthorisedId = `${Build.EntryKind['401']}${Bun.hash(unauthorisedImport)}`;
385
+ unauthorisedIds.push(unauthorisedId);
386
+ if (!processed.has(unauthorisedPath)) {
387
+ imports.components.dynamic.set(unauthorisedId, unauthorisedImport);
388
+ processed.add(unauthorisedPath);
389
389
  }
390
390
  }
391
391
  for (const forbiddenPath of forbiddenPaths) {
@@ -475,7 +475,7 @@ var Build;
475
475
  processed.add(pagePath);
476
476
  }
477
477
  // resolve final prerender mode after shell → layout → page overrides
478
- const shouldPrerender = currentPrerenderMode !== false;
478
+ const shouldPrerender = currentPrerenderMode !== false && this.buildContext.command === 'build';
479
479
  const prerenderMode = shouldPrerender
480
480
  ? currentPrerenderMode
481
481
  : false;
@@ -506,7 +506,7 @@ var Build;
506
506
  method: 'get',
507
507
  paths: {
508
508
  layouts: [shellPath, ...layoutPaths].map(layout => layout ? Finder.getImportPath(layout) : null),
509
- '401s': unauthorizedPaths.map(unauthorized => unauthorized ? Finder.getImportPath(unauthorized) : null),
509
+ '401s': unauthorisedPaths.map(unauthorised => unauthorised ? Finder.getImportPath(unauthorised) : null),
510
510
  '403s': forbiddenPaths.map(forbidden => forbidden ? Finder.getImportPath(forbidden) : null),
511
511
  '404s': notFoundPaths.map(notFound => notFound ? Finder.getImportPath(notFound) : null),
512
512
  '500s': serverErrorPaths.map(serverError => serverError ? Finder.getImportPath(serverError) : null),
@@ -533,7 +533,7 @@ var Build;
533
533
  shellId,
534
534
  layoutIds,
535
535
  pageId: pagePath ? entryId : undefined,
536
- '401Ids': unauthorizedIds,
536
+ '401Ids': unauthorisedIds,
537
537
  '403Ids': forbiddenIds,
538
538
  '404Ids': notFoundIds,
539
539
  '500Ids': serverErrorIds,
@@ -8,18 +8,19 @@ export function writeRSCEntry() {
8
8
  ${AUTOGEN_MSG}
9
9
 
10
10
  import { createHandler } from '${Solas.Config.PKG_NAME}/env/rsc'
11
- import { Prerender } from '${Solas.Config.PKG_NAME}/prerender'
12
11
  import { Solas } from '${Solas.Config.PKG_NAME}'
13
12
 
14
13
  import { manifest } from './manifest.js'
15
14
  import { importMap } from './maps.js'
16
15
  import { config } from './config.js'
17
16
 
18
- const artifactManifest = await Prerender.Artifact.loadManifest(Solas.Config.OUT_DIR)
17
+ const runtimeManifest = await Solas.Runtime.loadManifest(Solas.Config.OUT_DIR)
19
18
 
20
- export default createHandler(config, manifest, importMap, artifactManifest)
19
+ export default createHandler(config, manifest, importMap, runtimeManifest)
21
20
 
22
- import.meta.hot?.accept()
21
+ if (import.meta.hot) {
22
+ import.meta.hot.accept()
23
+ }
23
24
  `;
24
25
  }
25
26
  /**
@@ -50,13 +50,15 @@ export async function browser() {
50
50
  hydrateRoot(document, _jsx(StrictMode, { children: _jsx(A, {}) }), {
51
51
  formState: payload.formState,
52
52
  });
53
- import.meta.hot?.on?.('rsc:update', async () => {
54
- try {
55
- const p = await createFromFetch(fetch(window.location.href, { headers: { Accept: 'text/x-component' } }));
56
- payloadSetter.current(p);
57
- }
58
- catch (err) {
59
- console.error('[hmr] failed to refresh rsc payload', err);
60
- }
61
- });
53
+ if (import.meta.hot) {
54
+ import.meta.hot.on?.('rsc:update', async () => {
55
+ try {
56
+ const p = await createFromFetch(fetch(window.location.href, { headers: { Accept: 'text/x-component' } }));
57
+ payloadSetter.current(p);
58
+ }
59
+ catch (err) {
60
+ console.error('[hmr] failed to refresh rsc payload', err);
61
+ }
62
+ });
63
+ }
62
64
  }
@@ -1,7 +1,7 @@
1
1
  import type { ReactFormState } from 'react-dom/client';
2
2
  import type { ImportMap, Manifest, RuntimeConfig } from '../../types.js';
3
+ import { Solas } from '../../solas.js';
3
4
  import { Metadata } from '../metadata.js';
4
- import { Prerender } from '../prerender.js';
5
5
  export type RscPayload = {
6
6
  returnValue?: {
7
7
  ok: boolean;
@@ -20,6 +20,6 @@ export type RscPayload = {
20
20
  * route manifest, and import map to build the router once, then returns an object
21
21
  * with a fetch method that handles requests
22
22
  */
23
- export declare function createHandler(config: RuntimeConfig, manifest: Manifest, importMap: ImportMap, artifactManifest?: Prerender.Artifact.Manifest | null): {
23
+ export declare function createHandler(config: RuntimeConfig, manifest: Manifest, importMap: ImportMap, runtimeManifest?: Solas.Runtime.Manifest | null): {
24
24
  fetch(req: Request): Promise<Response>;
25
25
  };