@c9up/aurora 0.1.24 → 0.1.26
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/README.md +92 -0
- package/dist/AuroraManager.d.ts +22 -1
- package/dist/AuroraManager.js +26 -0
- package/dist/AuroraProvider.d.ts +1 -1
- package/dist/AuroraProvider.js +9 -9
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +49 -8
- package/dist/form.d.ts +19 -9
- package/dist/form.js +5 -2
- package/dist/http.d.ts +12 -0
- package/dist/http.js +51 -2
- package/dist/index.d.ts +1 -1
- package/dist/live.d.ts +1 -1
- package/dist/liveServer.d.ts +12 -0
- package/dist/liveServer.js +15 -1
- package/dist/relay.js +11 -1
- package/dist/route.js +9 -1
- package/dist/server/renderPage.d.ts +25 -0
- package/dist/server/renderPage.js +52 -13
- package/dist/server.d.ts +1 -1
- package/dist/services/main.js +9 -0
- package/dist/url.d.ts +7 -0
- package/dist/url.js +15 -3
- package/package.json +1 -1
- package/src/AuroraManager.ts +49 -0
- package/src/AuroraProvider.ts +13 -11
- package/src/browser.ts +57 -8
- package/src/form.ts +20 -7
- package/src/http.ts +67 -2
- package/src/index.ts +1 -0
- package/src/live.ts +1 -0
- package/src/liveServer.ts +25 -2
- package/src/relay.ts +9 -1
- package/src/route.ts +10 -1
- package/src/server/renderPage.ts +100 -14
- package/src/server.ts +2 -0
- package/src/services/main.ts +9 -0
- package/src/url.ts +21 -3
package/src/http.ts
CHANGED
|
@@ -29,6 +29,13 @@ export interface HttpClientOptions {
|
|
|
29
29
|
token?: string | null | (() => string | null | undefined);
|
|
30
30
|
/** Default `credentials` mode (e.g. `"include"` to send cookies). */
|
|
31
31
|
credentials?: RequestCredentials;
|
|
32
|
+
/**
|
|
33
|
+
* Allow default bearer/default Authorization headers to be sent to absolute
|
|
34
|
+
* cross-origin URLs. Default `false`: same-origin API clients should not leak
|
|
35
|
+
* credentials if an untrusted value becomes the request URL. Per-request
|
|
36
|
+
* `headers.Authorization` is still treated as explicit caller intent.
|
|
37
|
+
*/
|
|
38
|
+
allowCrossOriginAuth?: boolean;
|
|
32
39
|
/**
|
|
33
40
|
* Default timeout in ms — the request is aborted (rejecting with a
|
|
34
41
|
* `TimeoutError`) if it doesn't settle in time. Combined with a per-request
|
|
@@ -50,6 +57,11 @@ export interface HttpRequestOptions<T = unknown> {
|
|
|
50
57
|
timeout?: number;
|
|
51
58
|
/** `credentials` mode for this request. */
|
|
52
59
|
credentials?: RequestCredentials;
|
|
60
|
+
/**
|
|
61
|
+
* Per-request override for sending managed auth headers to cross-origin
|
|
62
|
+
* absolute URLs. Default inherits the client option (`false` by default).
|
|
63
|
+
*/
|
|
64
|
+
allowCrossOriginAuth?: boolean;
|
|
53
65
|
/**
|
|
54
66
|
* Runtime validator/mapper for the parsed body. When provided, the return
|
|
55
67
|
* type is whatever it returns — no unchecked cast. When omitted, the parsed
|
|
@@ -131,6 +143,36 @@ function hasHeader(headers: Record<string, string>, name: string): boolean {
|
|
|
131
143
|
return false;
|
|
132
144
|
}
|
|
133
145
|
|
|
146
|
+
/** Delete every case variant of a header from a plain header record. */
|
|
147
|
+
function deleteHeader(headers: Record<string, string>, name: string): void {
|
|
148
|
+
const lower = name.toLowerCase();
|
|
149
|
+
for (const key of Object.keys(headers)) {
|
|
150
|
+
if (key.toLowerCase() === lower) delete headers[key];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function originOf(value: string): string | null {
|
|
155
|
+
try {
|
|
156
|
+
if (/^[a-z][a-z\d+\-.]*:\/\//i.test(value)) return new URL(value).origin;
|
|
157
|
+
if (typeof window !== "undefined")
|
|
158
|
+
return new URL(value, window.location.href).origin;
|
|
159
|
+
return null;
|
|
160
|
+
} catch {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function isCrossOriginAbsoluteUrl(url: string, baseURL: string): boolean {
|
|
166
|
+
if (!/^[a-z][a-z\d+\-.]*:\/\//i.test(url)) return false;
|
|
167
|
+
const targetOrigin = originOf(url);
|
|
168
|
+
if (targetOrigin === null) return true;
|
|
169
|
+
const baseOrigin = baseURL ? originOf(baseURL) : null;
|
|
170
|
+
if (baseOrigin !== null) return targetOrigin !== baseOrigin;
|
|
171
|
+
if (typeof window !== "undefined")
|
|
172
|
+
return targetOrigin !== window.location.origin;
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
|
|
134
176
|
/** Merge abort signals into one (whichever fires first wins). `undefined` if none. */
|
|
135
177
|
function combineSignals(
|
|
136
178
|
signals: ReadonlyArray<AbortSignal | undefined>,
|
|
@@ -168,6 +210,7 @@ export class HttpClient {
|
|
|
168
210
|
readonly #token?: string | null | (() => string | null | undefined);
|
|
169
211
|
readonly #credentials?: RequestCredentials;
|
|
170
212
|
readonly #timeout?: number;
|
|
213
|
+
readonly #allowCrossOriginAuth: boolean;
|
|
171
214
|
|
|
172
215
|
constructor(options: HttpClientOptions = {}) {
|
|
173
216
|
this.#baseURL = options.baseURL ?? "";
|
|
@@ -175,6 +218,7 @@ export class HttpClient {
|
|
|
175
218
|
this.#token = options.token;
|
|
176
219
|
this.#credentials = options.credentials;
|
|
177
220
|
this.#timeout = options.timeout;
|
|
221
|
+
this.#allowCrossOriginAuth = options.allowCrossOriginAuth ?? false;
|
|
178
222
|
}
|
|
179
223
|
|
|
180
224
|
/** Set a default header for every subsequent request (case-insensitive replace). Chainable. */
|
|
@@ -284,6 +328,8 @@ export class HttpClient {
|
|
|
284
328
|
token: options.token ?? this.#token,
|
|
285
329
|
credentials: options.credentials ?? this.#credentials,
|
|
286
330
|
timeout: options.timeout ?? this.#timeout,
|
|
331
|
+
allowCrossOriginAuth:
|
|
332
|
+
options.allowCrossOriginAuth ?? this.#allowCrossOriginAuth,
|
|
287
333
|
});
|
|
288
334
|
}
|
|
289
335
|
|
|
@@ -313,12 +359,31 @@ export class HttpClient {
|
|
|
313
359
|
body: unknown,
|
|
314
360
|
options: HttpRequestOptions,
|
|
315
361
|
): Promise<Response> {
|
|
362
|
+
const finalUrl = this.#buildUrl(url, options.query);
|
|
363
|
+
const crossOrigin = isCrossOriginAbsoluteUrl(finalUrl, this.#baseURL);
|
|
364
|
+
const allowCrossOriginAuth =
|
|
365
|
+
options.allowCrossOriginAuth ?? this.#allowCrossOriginAuth;
|
|
366
|
+
const explicitRequestAuth =
|
|
367
|
+
options.headers !== undefined &&
|
|
368
|
+
hasHeader(options.headers, "authorization");
|
|
316
369
|
const headers: Record<string, string> = {
|
|
317
370
|
...this.#headers,
|
|
318
371
|
...options.headers,
|
|
319
372
|
};
|
|
373
|
+
if (
|
|
374
|
+
crossOrigin &&
|
|
375
|
+
!allowCrossOriginAuth &&
|
|
376
|
+
!explicitRequestAuth &&
|
|
377
|
+
hasHeader(this.#headers, "authorization")
|
|
378
|
+
) {
|
|
379
|
+
deleteHeader(headers, "authorization");
|
|
380
|
+
}
|
|
320
381
|
const token = this.#resolveToken(options.token);
|
|
321
|
-
if (
|
|
382
|
+
if (
|
|
383
|
+
token != null &&
|
|
384
|
+
!hasHeader(headers, "authorization") &&
|
|
385
|
+
(!crossOrigin || allowCrossOriginAuth)
|
|
386
|
+
) {
|
|
322
387
|
headers.Authorization = `Bearer ${token}`;
|
|
323
388
|
}
|
|
324
389
|
|
|
@@ -341,7 +406,7 @@ export class HttpClient {
|
|
|
341
406
|
timeout !== undefined ? AbortSignal.timeout(timeout) : undefined,
|
|
342
407
|
]);
|
|
343
408
|
|
|
344
|
-
return fetch(
|
|
409
|
+
return fetch(finalUrl, {
|
|
345
410
|
method,
|
|
346
411
|
headers,
|
|
347
412
|
body: payload,
|
package/src/index.ts
CHANGED
package/src/live.ts
CHANGED
package/src/liveServer.ts
CHANGED
|
@@ -29,9 +29,19 @@ export interface LiveHttpContext {
|
|
|
29
29
|
export interface WireLiveEventsOptions {
|
|
30
30
|
/** Route path for inbound events (must match the client transport). */
|
|
31
31
|
path?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Optional per-request guard. Return `false` to reject the event with 403.
|
|
34
|
+
* Use this to enforce the same auth/CSRF/owner policy as the page that mounted
|
|
35
|
+
* the live session. When omitted, aurora preserves the framework-agnostic
|
|
36
|
+
* legacy behavior and expects the host route/middleware to guard the endpoint.
|
|
37
|
+
*/
|
|
38
|
+
authorize?: (
|
|
39
|
+
ctx: LiveHttpContext,
|
|
40
|
+
body: LiveEventBody,
|
|
41
|
+
) => boolean | Promise<boolean>;
|
|
32
42
|
}
|
|
33
43
|
|
|
34
|
-
interface LiveEventBody {
|
|
44
|
+
export interface LiveEventBody {
|
|
35
45
|
id: string;
|
|
36
46
|
event: string;
|
|
37
47
|
payload?: unknown;
|
|
@@ -57,13 +67,26 @@ export function wireLiveEvents(
|
|
|
57
67
|
options: WireLiveEventsOptions = {},
|
|
58
68
|
): void {
|
|
59
69
|
const path = options.path ?? DEFAULT_LIVE_EVENT_PATH;
|
|
60
|
-
router.post(path, (ctx) => {
|
|
70
|
+
router.post(path, async (ctx) => {
|
|
61
71
|
const body = ctx.request.body();
|
|
62
72
|
if (!isLiveEventBody(body)) {
|
|
63
73
|
ctx.response.status(400);
|
|
64
74
|
ctx.response.json({ error: "live event requires { id, event }" });
|
|
65
75
|
return;
|
|
66
76
|
}
|
|
77
|
+
let authorized = true;
|
|
78
|
+
if (options.authorize) {
|
|
79
|
+
try {
|
|
80
|
+
authorized = await options.authorize(ctx, body);
|
|
81
|
+
} catch {
|
|
82
|
+
authorized = false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (!authorized) {
|
|
86
|
+
ctx.response.status(403);
|
|
87
|
+
ctx.response.json({ error: "forbidden live event" });
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
67
90
|
const handled = live.event(body.id, body.event, body.payload);
|
|
68
91
|
if (!handled) {
|
|
69
92
|
ctx.response.status(404);
|
package/src/relay.ts
CHANGED
|
@@ -345,7 +345,15 @@ async function postHandshake(url: string, channel: string): Promise<void> {
|
|
|
345
345
|
function retrieveXsrfToken(): string | null {
|
|
346
346
|
if (typeof document === "undefined") return null;
|
|
347
347
|
const match = document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]*)/);
|
|
348
|
-
|
|
348
|
+
if (!match) return null;
|
|
349
|
+
try {
|
|
350
|
+
return decodeURIComponent(match[1]);
|
|
351
|
+
} catch {
|
|
352
|
+
// A malformed cookie must not break subscribe/unsubscribe handshakes. The
|
|
353
|
+
// server will reject an invalid token normally; the client should not throw
|
|
354
|
+
// before it even sends the request.
|
|
355
|
+
return match[1];
|
|
356
|
+
}
|
|
349
357
|
}
|
|
350
358
|
|
|
351
359
|
function safeJson<T>(raw: unknown): T | null {
|
package/src/route.ts
CHANGED
|
@@ -72,10 +72,19 @@ const DEFAULT_SHELL = (body: string, entry: string): string =>
|
|
|
72
72
|
</head>
|
|
73
73
|
<body>
|
|
74
74
|
<div id="aurora-root">${body}</div>
|
|
75
|
-
<script type="module" src="${entry}"></script>
|
|
75
|
+
<script type="module" src="${escapeAttr(entry)}"></script>
|
|
76
76
|
</body>
|
|
77
77
|
</html>`;
|
|
78
78
|
|
|
79
|
+
function escapeAttr(value: string): string {
|
|
80
|
+
return value
|
|
81
|
+
.replace(/&/g, "&")
|
|
82
|
+
.replace(/"/g, """)
|
|
83
|
+
.replace(/'/g, "'")
|
|
84
|
+
.replace(/</g, "<")
|
|
85
|
+
.replace(/>/g, ">");
|
|
86
|
+
}
|
|
87
|
+
|
|
79
88
|
/**
|
|
80
89
|
* Build a Ream-compatible route handler that SSR-renders the given
|
|
81
90
|
* factory and serves the full HTML document.
|
package/src/server/renderPage.ts
CHANGED
|
@@ -23,10 +23,11 @@
|
|
|
23
23
|
* </body>
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
27
|
+
import { setCookieStoreReader } from "../browser.js";
|
|
27
28
|
import type { Pages } from "../Pages.js";
|
|
28
29
|
import { renderToString } from "../ssr.js";
|
|
29
|
-
import {
|
|
30
|
+
import { setRouteManifestReader } from "../url.js";
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* Structural slice of the host framework's response. Same shape
|
|
@@ -70,6 +71,29 @@ export interface RenderPageOptions {
|
|
|
70
71
|
* targets.
|
|
71
72
|
*/
|
|
72
73
|
rootId?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Root element tag for the SSR + hydrated tree. Defaults to `div`.
|
|
76
|
+
* Mirrors Inertia's root tag customization (`@inertia({ as: ... })`) while
|
|
77
|
+
* keeping aurora independent from a template engine.
|
|
78
|
+
*/
|
|
79
|
+
rootTag?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Optional class attribute on the root element. Mirrors
|
|
82
|
+
* `@inertia({ class: ... })`.
|
|
83
|
+
*/
|
|
84
|
+
rootClass?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Shared props merged into every page render before invoking the page factory.
|
|
87
|
+
* Use this for global data such as user, flash and validation errors. A
|
|
88
|
+
* function receives the current HTTP context and may be async, matching
|
|
89
|
+
* Adonis/Inertia's request middleware `share()` model.
|
|
90
|
+
*/
|
|
91
|
+
shared?: SharedProps | SharedPropsResolver;
|
|
92
|
+
/**
|
|
93
|
+
* Asset/version marker serialized with the page payload. Apps can use this to
|
|
94
|
+
* detect stale client state when their frontend build changes.
|
|
95
|
+
*/
|
|
96
|
+
assetsVersion?: string;
|
|
73
97
|
/**
|
|
74
98
|
* Named-route manifest (`name → path-pattern`) for the isomorphic
|
|
75
99
|
* `urlFor()` helper — build it with Ream's `router.namedManifest()`. It is
|
|
@@ -92,11 +116,26 @@ export interface RenderPageOptions {
|
|
|
92
116
|
cookies?: string[];
|
|
93
117
|
}
|
|
94
118
|
|
|
119
|
+
export type SharedProps = Record<string, unknown>;
|
|
120
|
+
export type SharedPropsResolver = (
|
|
121
|
+
ctx: RenderHttpContext,
|
|
122
|
+
) => SharedProps | Promise<SharedProps>;
|
|
123
|
+
|
|
95
124
|
/** A request that can read a cookie by name — the structural slice we need. */
|
|
96
125
|
interface CookieReadableRequest {
|
|
97
126
|
cookie(name: string): string | null;
|
|
98
127
|
}
|
|
99
128
|
|
|
129
|
+
interface RenderScope {
|
|
130
|
+
cookies: Record<string, string>;
|
|
131
|
+
routes: Record<string, string>;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const renderScope = new AsyncLocalStorage<RenderScope>();
|
|
135
|
+
|
|
136
|
+
setCookieStoreReader(() => renderScope.getStore()?.cookies);
|
|
137
|
+
setRouteManifestReader(() => renderScope.getStore()?.routes);
|
|
138
|
+
|
|
100
139
|
function isCookieReadable(request: unknown): request is CookieReadableRequest {
|
|
101
140
|
return (
|
|
102
141
|
typeof request === "object" &&
|
|
@@ -127,21 +166,31 @@ export async function renderPage<P>(
|
|
|
127
166
|
props: P,
|
|
128
167
|
options: RenderPageOptions = {},
|
|
129
168
|
): Promise<void> {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
169
|
+
const scope: RenderScope = {
|
|
170
|
+
cookies: options.cookies
|
|
171
|
+
? readRequestCookies(ctx.request, options.cookies)
|
|
172
|
+
: {},
|
|
173
|
+
routes: options.routes ?? {},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
return renderScope.run(scope, () =>
|
|
177
|
+
renderPageInScope(ctx, pages, name, props, options),
|
|
139
178
|
);
|
|
179
|
+
}
|
|
140
180
|
|
|
181
|
+
async function renderPageInScope<P>(
|
|
182
|
+
ctx: RenderHttpContext,
|
|
183
|
+
pages: Pages,
|
|
184
|
+
name: string,
|
|
185
|
+
props: P,
|
|
186
|
+
options: RenderPageOptions,
|
|
187
|
+
): Promise<void> {
|
|
141
188
|
const factory = await pages.resolve(name);
|
|
189
|
+
const shared = await resolveSharedProps(ctx, options.shared);
|
|
190
|
+
const pageProps = mergeProps(shared, props);
|
|
142
191
|
// The factory must be invoked the SAME way client-side for hydrate
|
|
143
192
|
// to find matching slots — `Page(props)` is the contract.
|
|
144
|
-
const tree = await factory(
|
|
193
|
+
const tree = await factory(pageProps as never);
|
|
145
194
|
const body = renderToString(tree);
|
|
146
195
|
|
|
147
196
|
const importmap = {
|
|
@@ -149,6 +198,8 @@ export async function renderPage<P>(
|
|
|
149
198
|
...options.importmap,
|
|
150
199
|
};
|
|
151
200
|
const rootId = options.rootId ?? "aurora-root";
|
|
201
|
+
const rootTag = normalizeRootTag(options.rootTag ?? "div");
|
|
202
|
+
const rootClass = options.rootClass;
|
|
152
203
|
const lang = options.lang ?? "en";
|
|
153
204
|
const pageUrl = pages.urlFor(name);
|
|
154
205
|
|
|
@@ -161,13 +212,14 @@ export async function renderPage<P>(
|
|
|
161
212
|
${options.headExtra ?? ""}
|
|
162
213
|
</head>
|
|
163
214
|
<body>
|
|
164
|
-
|
|
215
|
+
<${rootTag}${rootAttrs(rootId, rootClass)}>${body}</${rootTag}>
|
|
165
216
|
<script id="aurora-page-data" type="application/json">${escapeJsonForScript({
|
|
166
217
|
name,
|
|
167
|
-
props,
|
|
218
|
+
props: pageProps,
|
|
168
219
|
url: pageUrl,
|
|
169
220
|
rootId,
|
|
170
221
|
routes: options.routes ?? {},
|
|
222
|
+
version: options.assetsVersion ?? null,
|
|
171
223
|
})}</script>
|
|
172
224
|
<script type="module">
|
|
173
225
|
import { hydrate, setRouteManifest } from '@c9up/aurora'
|
|
@@ -183,6 +235,40 @@ hydrate(document.getElementById(data.rootId), () => Page(data.props))
|
|
|
183
235
|
ctx.response.send(doc);
|
|
184
236
|
}
|
|
185
237
|
|
|
238
|
+
async function resolveSharedProps(
|
|
239
|
+
ctx: RenderHttpContext,
|
|
240
|
+
shared: RenderPageOptions["shared"],
|
|
241
|
+
): Promise<SharedProps> {
|
|
242
|
+
if (!shared) return {};
|
|
243
|
+
return typeof shared === "function" ? shared(ctx) : shared;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function mergeProps<P>(shared: SharedProps, props: P): P | SharedProps {
|
|
247
|
+
if (Object.keys(shared).length === 0) return props;
|
|
248
|
+
if (isPlainRecord(props)) return { ...shared, ...props };
|
|
249
|
+
return { ...shared, page: props };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
253
|
+
return (
|
|
254
|
+
typeof value === "object" &&
|
|
255
|
+
value !== null &&
|
|
256
|
+
!Array.isArray(value) &&
|
|
257
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function normalizeRootTag(tag: string): string {
|
|
262
|
+
if (/^[a-z][a-z0-9-]*$/i.test(tag)) return tag.toLowerCase();
|
|
263
|
+
throw new Error(`[aurora] illegal root tag: ${JSON.stringify(tag)}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function rootAttrs(id: string, className: string | undefined): string {
|
|
267
|
+
const attrs = [`id="${escapeAttr(id)}"`];
|
|
268
|
+
if (className) attrs.push(`class="${escapeAttr(className)}"`);
|
|
269
|
+
return ` ${attrs.join(" ")}`;
|
|
270
|
+
}
|
|
271
|
+
|
|
186
272
|
function escapeAttr(value: string): string {
|
|
187
273
|
return value
|
|
188
274
|
.replace(/&/g, "&")
|
package/src/server.ts
CHANGED
package/src/services/main.ts
CHANGED
|
@@ -26,6 +26,15 @@ export function getAurora(): AuroraManager | undefined {
|
|
|
26
26
|
|
|
27
27
|
const aurora: AuroraManager = new Proxy({} as AuroraManager, {
|
|
28
28
|
get(_target, prop) {
|
|
29
|
+
// A module loader inspects what it imports before anyone uses it: it reads
|
|
30
|
+
// `then` to decide whether the namespace is thenable, and various symbols
|
|
31
|
+
// for interop and formatting. Throwing on those turns a plain
|
|
32
|
+
// `import { setX } from ".../services/main"` into a crash at import time,
|
|
33
|
+
// far from any real use. They are not members of what this stands in for,
|
|
34
|
+
// so answer undefined and let a genuine access be the one that reports.
|
|
35
|
+
if (typeof prop === "symbol" || prop === "then") {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
29
38
|
if (!instance) {
|
|
30
39
|
throw new Error(
|
|
31
40
|
"[aurora] AuroraManager singleton accessed before AuroraProvider.boot() ran " +
|
package/src/url.ts
CHANGED
|
@@ -19,6 +19,23 @@
|
|
|
19
19
|
|
|
20
20
|
let manifest: Record<string, string> = {};
|
|
21
21
|
|
|
22
|
+
type RouteManifestReader = () => Record<string, string> | undefined;
|
|
23
|
+
let routeManifestReader: RouteManifestReader | undefined;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @internal Server-side hook used by `renderPage()` to provide a request-scoped
|
|
27
|
+
* route manifest without importing Node built-ins from this browser-safe module.
|
|
28
|
+
*/
|
|
29
|
+
export function setRouteManifestReader(
|
|
30
|
+
reader: RouteManifestReader | undefined,
|
|
31
|
+
): void {
|
|
32
|
+
routeManifestReader = reader;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function activeManifest(): Record<string, string> {
|
|
36
|
+
return routeManifestReader?.() ?? manifest;
|
|
37
|
+
}
|
|
38
|
+
|
|
22
39
|
/**
|
|
23
40
|
* Install the `name → path-pattern` map `urlFor` resolves against (e.g.
|
|
24
41
|
* `{ 'users.show': '/users/:id' }`, from Ream's `router.namedManifest()`).
|
|
@@ -31,7 +48,7 @@ export function setRouteManifest(routes: Record<string, string>): void {
|
|
|
31
48
|
|
|
32
49
|
/** The currently-installed route manifest (mainly for tests/introspection). */
|
|
33
50
|
export function getRouteManifest(): Record<string, string> {
|
|
34
|
-
return { ...
|
|
51
|
+
return { ...activeManifest() };
|
|
35
52
|
}
|
|
36
53
|
|
|
37
54
|
/**
|
|
@@ -44,9 +61,10 @@ export function urlFor(
|
|
|
44
61
|
params?: Record<string, string | number>,
|
|
45
62
|
query?: Record<string, string | number>,
|
|
46
63
|
): string {
|
|
47
|
-
const
|
|
64
|
+
const routes = activeManifest();
|
|
65
|
+
const pattern = routes[name];
|
|
48
66
|
if (pattern === undefined) {
|
|
49
|
-
const known = Object.keys(
|
|
67
|
+
const known = Object.keys(routes);
|
|
50
68
|
throw new Error(
|
|
51
69
|
`[aurora] urlFor: unknown route '${name}'. ${
|
|
52
70
|
known.length > 0
|