netjes 0.0.0-stage → 0.0.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.
- package/LICENSE +21 -0
- package/README.md +54 -2
- package/bin/netjes.mjs +13 -0
- package/cli/agents.mjs +66 -0
- package/cli/commands.mjs +467 -0
- package/cli/help.mjs +89 -0
- package/cli/info.mjs +454 -0
- package/cli/main.mjs +167 -0
- package/cli/packages.mjs +19 -0
- package/cli/process-io.mjs +163 -0
- package/cli/routes.mjs +31 -0
- package/cli/scaffold.mjs +193 -0
- package/cli/suggest.mjs +50 -0
- package/cli/terminal.mjs +40 -0
- package/dist/actions.d.ts +98 -0
- package/dist/actions.d.ts.map +1 -0
- package/dist/actions.js +176 -0
- package/dist/actions.js.map +1 -0
- package/dist/client-entry.d.ts +2 -0
- package/dist/client-entry.d.ts.map +1 -0
- package/dist/client-entry.js +126 -0
- package/dist/client-entry.js.map +1 -0
- package/dist/deferred.d.ts +39 -0
- package/dist/deferred.d.ts.map +1 -0
- package/dist/deferred.js +123 -0
- package/dist/deferred.js.map +1 -0
- package/dist/define-page.d.ts +15 -0
- package/dist/define-page.d.ts.map +1 -0
- package/dist/define-page.js +27 -0
- package/dist/define-page.js.map +1 -0
- package/dist/document.d.ts +19 -0
- package/dist/document.d.ts.map +1 -0
- package/dist/document.js +17 -0
- package/dist/document.js.map +1 -0
- package/dist/draft-mode.d.ts +5 -0
- package/dist/draft-mode.d.ts.map +1 -0
- package/dist/draft-mode.js +6 -0
- package/dist/draft-mode.js.map +1 -0
- package/dist/dynamic.d.ts +20 -0
- package/dist/dynamic.d.ts.map +1 -0
- package/dist/dynamic.js +37 -0
- package/dist/dynamic.js.map +1 -0
- package/dist/form-data.d.ts +10 -0
- package/dist/form-data.d.ts.map +1 -0
- package/dist/form-data.js +10 -0
- package/dist/form-data.js.map +1 -0
- package/dist/form.d.ts +35 -0
- package/dist/form.d.ts.map +1 -0
- package/dist/form.js +80 -0
- package/dist/form.js.map +1 -0
- package/dist/href.d.ts +25 -0
- package/dist/href.d.ts.map +1 -0
- package/dist/href.js +71 -0
- package/dist/href.js.map +1 -0
- package/dist/image.d.ts +36 -0
- package/dist/image.d.ts.map +1 -0
- package/dist/image.js +60 -0
- package/dist/image.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/link.d.ts +17 -0
- package/dist/link.d.ts.map +1 -0
- package/dist/link.js +46 -0
- package/dist/link.js.map +1 -0
- package/dist/locale.d.ts +3 -0
- package/dist/locale.d.ts.map +1 -0
- package/dist/locale.js +6 -0
- package/dist/locale.js.map +1 -0
- package/dist/metadata.d.ts +83 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +100 -0
- package/dist/metadata.js.map +1 -0
- package/dist/modules.d.ts +74 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +85 -0
- package/dist/modules.js.map +1 -0
- package/dist/nonce.d.ts +8 -0
- package/dist/nonce.d.ts.map +1 -0
- package/dist/nonce.js +12 -0
- package/dist/nonce.js.map +1 -0
- package/dist/payload.d.ts +171 -0
- package/dist/payload.d.ts.map +1 -0
- package/dist/payload.js +273 -0
- package/dist/payload.js.map +1 -0
- package/dist/public-config.d.ts +11 -0
- package/dist/public-config.d.ts.map +1 -0
- package/dist/public-config.js +10 -0
- package/dist/public-config.js.map +1 -0
- package/dist/query.d.ts +22 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +68 -0
- package/dist/query.js.map +1 -0
- package/dist/render-cleanup.d.ts +6 -0
- package/dist/render-cleanup.d.ts.map +1 -0
- package/dist/render-cleanup.js +7 -0
- package/dist/render-cleanup.js.map +1 -0
- package/dist/root.d.ts +12 -0
- package/dist/root.d.ts.map +1 -0
- package/dist/root.js +26 -0
- package/dist/root.js.map +1 -0
- package/dist/route-tree.d.ts +56 -0
- package/dist/route-tree.d.ts.map +1 -0
- package/dist/route-tree.js +86 -0
- package/dist/route-tree.js.map +1 -0
- package/dist/router.d.ts +118 -0
- package/dist/router.d.ts.map +1 -0
- package/dist/router.js +938 -0
- package/dist/router.js.map +1 -0
- package/dist/schema.d.ts +35 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +74 -0
- package/dist/schema.js.map +1 -0
- package/dist/script.d.ts +23 -0
- package/dist/script.d.ts.map +1 -0
- package/dist/script.js +89 -0
- package/dist/script.js.map +1 -0
- package/dist/server-entry.d.ts +2 -0
- package/dist/server-entry.d.ts.map +1 -0
- package/dist/server-entry.js +4 -0
- package/dist/server-entry.js.map +1 -0
- package/dist/server-render.d.ts +19 -0
- package/dist/server-render.d.ts.map +1 -0
- package/dist/server-render.js +193 -0
- package/dist/server-render.js.map +1 -0
- package/dist/testing-setup.d.ts +2 -0
- package/dist/testing-setup.d.ts.map +1 -0
- package/dist/testing-setup.js +8 -0
- package/dist/testing-setup.js.map +1 -0
- package/dist/testing.d.ts +76 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +106 -0
- package/dist/testing.js.map +1 -0
- package/dist/vitals-config.d.ts +4 -0
- package/dist/vitals-config.d.ts.map +1 -0
- package/dist/vitals-config.js +5 -0
- package/dist/vitals-config.js.map +1 -0
- package/dist/vitals.d.ts +13 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +60 -0
- package/dist/vitals.js.map +1 -0
- package/dist/zod.d.ts +19 -0
- package/dist/zod.d.ts.map +1 -0
- package/dist/zod.js +65 -0
- package/dist/zod.js.map +1 -0
- package/package.json +147 -4
- package/sidecar/server.mjs +325 -0
- package/skills/netjes/SKILL.md +93 -0
- package/skills/netjes/references/caching-and-streaming.md +68 -0
- package/skills/netjes/references/data-and-actions.md +98 -0
- package/skills/netjes/references/deployment.md +57 -0
- package/skills/netjes/references/routing.md +65 -0
- package/skills/netjes/references/testing.md +57 -0
- package/src/actions.tsx +241 -0
- package/src/client-entry.tsx +126 -0
- package/src/deferred.ts +134 -0
- package/src/define-page.tsx +41 -0
- package/src/document.tsx +49 -0
- package/src/draft-mode.ts +6 -0
- package/src/dynamic.tsx +64 -0
- package/src/form-data.ts +10 -0
- package/src/form.tsx +122 -0
- package/src/href.ts +79 -0
- package/src/image.tsx +91 -0
- package/src/index.ts +40 -0
- package/src/link.tsx +73 -0
- package/src/locale.ts +6 -0
- package/src/metadata.tsx +181 -0
- package/src/modules.ts +133 -0
- package/src/nonce.tsx +14 -0
- package/src/payload.ts +405 -0
- package/src/public-config.tsx +19 -0
- package/src/query.tsx +82 -0
- package/src/render-cleanup.ts +7 -0
- package/src/root.tsx +39 -0
- package/src/route-tree.tsx +139 -0
- package/src/router.tsx +1188 -0
- package/src/schema.ts +98 -0
- package/src/script.tsx +103 -0
- package/src/server-entry.tsx +4 -0
- package/src/server-render.tsx +223 -0
- package/src/testing-setup.ts +8 -0
- package/src/testing.tsx +177 -0
- package/src/virtual.d.ts +12 -0
- package/src/vitals-config.ts +6 -0
- package/src/vitals.tsx +68 -0
- package/src/zod.ts +72 -0
- package/template/.dockerignore +11 -0
- package/template/Dockerfile +58 -0
- package/template/Features/Home/HomePage.cs +32 -0
- package/template/Program.cs +28 -0
- package/template/Properties/launchSettings.json +13 -0
- package/template/README.md +24 -0
- package/template/SiteProps.cs +4 -0
- package/template/_AGENTS.md +64 -0
- package/template/_CLAUDE.md +1 -0
- package/template/__NAME__.csproj +11 -0
- package/template/_gitattributes +2 -0
- package/template/_gitignore +11 -0
- package/template/app/about/page.tsx +17 -0
- package/template/app/error.tsx +15 -0
- package/template/app/layout.tsx +33 -0
- package/template/app/not-found.tsx +14 -0
- package/template/app/page.tsx +14 -0
- package/template/appsettings.Development.json +3 -0
- package/template/appsettings.json +9 -0
- package/template/components/Greeting.test.tsx +25 -0
- package/template/components/Greeting.tsx +26 -0
- package/template/components/HelloForm.test.tsx +27 -0
- package/template/components/HelloForm.tsx +34 -0
- package/template/docker-compose.override.yml +15 -0
- package/template/docker-compose.prod.yml +26 -0
- package/template/docker-compose.yml +16 -0
- package/template/generated/netjes.ts +73 -0
- package/template/package.json +36 -0
- package/template/public/favicon.svg +6 -0
- package/template/styles/global.css +7 -0
- package/template/tsconfig.json +19 -0
- package/template/vite.config.ts +7 -0
- package/vite/plugin.d.ts +29 -0
- package/vite/plugin.mjs +291 -0
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { type ActionResponse, type AntiforgeryToken } from './payload.ts';
|
|
2
|
+
import { type StandardSchemaV1 } from './schema.ts';
|
|
3
|
+
/**
|
|
4
|
+
* What a server action resolves to: its result, or the validation errors (status 422) per field. An action typed with
|
|
5
|
+
* a result can still answer `ServerAction.Redirect(...)` (then `result` is undefined and the router has already moved
|
|
6
|
+
* on), so code that runs after `await action(...)` in a component that may stay mounted should not assume one.
|
|
7
|
+
*/
|
|
8
|
+
export type ActionState<TResult> = {
|
|
9
|
+
ok: true;
|
|
10
|
+
result: TResult;
|
|
11
|
+
} | {
|
|
12
|
+
ok: false;
|
|
13
|
+
status: number;
|
|
14
|
+
errors: Record<string, string[]>;
|
|
15
|
+
values?: Record<string, string | string[]>;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* A reference to a C# server action (`app.MapAction(name, ...)`). Call it with a plain object (sent as JSON) or
|
|
19
|
+
* FormData (sent as a form post); use it in a form through {@link useAction}.
|
|
20
|
+
*/
|
|
21
|
+
export interface ServerAction<TInput, TResult> {
|
|
22
|
+
(input: TInput | FormData): Promise<ActionState<TResult>>;
|
|
23
|
+
readonly actionName: string;
|
|
24
|
+
readonly url: string;
|
|
25
|
+
}
|
|
26
|
+
/** Thrown into the nearest error boundary when the server could not run the action (not for validation errors). */
|
|
27
|
+
export declare class ServerActionError extends Error {
|
|
28
|
+
name: string;
|
|
29
|
+
readonly status: number;
|
|
30
|
+
constructor(message: string, status: number);
|
|
31
|
+
}
|
|
32
|
+
/** Runs action calls for the page: the router in the browser (fetch, then redirect/refresh/query updates), a fake in tests. */
|
|
33
|
+
export type ActionClient = {
|
|
34
|
+
invoke(name: string, url: string, input: unknown): Promise<ActionResponse>;
|
|
35
|
+
/** The antiforgery form field, left out of the values a failed call hands back. */
|
|
36
|
+
tokenField?(): string | undefined;
|
|
37
|
+
};
|
|
38
|
+
/** A router is mounting (called as it first renders, in the browser): calls from now on wait until it registers. */
|
|
39
|
+
export declare function expectActionClient(): void;
|
|
40
|
+
/** The router registers itself; there is one per page in the browser. */
|
|
41
|
+
export declare function setActionClient(next: ActionClient | null): () => void;
|
|
42
|
+
export declare const RETURN_FIELD = "__netjes_return";
|
|
43
|
+
type ActionMeta = {
|
|
44
|
+
url: string;
|
|
45
|
+
name: string;
|
|
46
|
+
};
|
|
47
|
+
/** The action URL and name a form action carries, so `<Form>` can render a real form post on the server. */
|
|
48
|
+
export declare function actionMetaOf(action: unknown): ActionMeta | undefined;
|
|
49
|
+
export declare function actionRequest(input: unknown, token: AntiforgeryToken | undefined, returnUrl: string): RequestInit;
|
|
50
|
+
/**
|
|
51
|
+
* The fetch half of a call, for the router's action client. `tokenRefused` says the server refused the call's
|
|
52
|
+
* antiforgery token, so the action did not run; no other answer says that, whatever its status. `onCookiesChanged`
|
|
53
|
+
* runs as soon as the server says the call set or deleted a cookie (signed someone in or out), which script cannot
|
|
54
|
+
* see in the response itself, whatever the rest of the answer turns out to be.
|
|
55
|
+
*/
|
|
56
|
+
export declare function fetchAction(name: string, url: string, init: RequestInit, onCookiesChanged?: () => void): Promise<{
|
|
57
|
+
response: ActionResponse;
|
|
58
|
+
tokenRefused: boolean;
|
|
59
|
+
}>;
|
|
60
|
+
/**
|
|
61
|
+
* Declares a server action by name. The result is parsed with `options.result` when given (zod, valibot, ...).
|
|
62
|
+
* `npx netjes generate` writes these for every `MapAction`, with types from the C# signature.
|
|
63
|
+
*/
|
|
64
|
+
export declare function serverAction<TInput = FormData, TResult = unknown>(name: string, options?: {
|
|
65
|
+
result?: StandardSchemaV1<unknown, TResult>;
|
|
66
|
+
}): ServerAction<TInput, TResult>;
|
|
67
|
+
/** Options for `useAction`. */
|
|
68
|
+
export type UseActionOptions<TResult> = {
|
|
69
|
+
/** The state before the first submission. Default null. */
|
|
70
|
+
initialState?: ActionState<TResult> | null;
|
|
71
|
+
/**
|
|
72
|
+
* Called with the form's data inside the action's transition, just before the call to the server: the place for
|
|
73
|
+
* `useOptimistic` updates, which React keeps until the action (and the page refresh after it) finishes.
|
|
74
|
+
*/
|
|
75
|
+
onSubmit?: (formData: FormData) => void;
|
|
76
|
+
};
|
|
77
|
+
/**
|
|
78
|
+
* `useActionState` for a server action: `[state, formAction, isPending]`. Pass `formAction` to `<Form action>`;
|
|
79
|
+
* `useFormStatus` works as usual, and `onSubmit` is where optimistic updates go. After a failed post without
|
|
80
|
+
* JavaScript, the state starts as that failure, with the submitted values.
|
|
81
|
+
*/
|
|
82
|
+
export declare function useAction<TInput, TResult>(action: ServerAction<TInput, TResult>, options?: UseActionOptions<TResult>): [state: ActionState<TResult> | null, formAction: (formData: FormData) => void, isPending: boolean];
|
|
83
|
+
/** A page action's failure, in the shape of `useAction`'s failure state. */
|
|
84
|
+
export type PageActionFailure = {
|
|
85
|
+
ok: false;
|
|
86
|
+
status: number;
|
|
87
|
+
errors: Record<string, string[]>;
|
|
88
|
+
values: Record<string, string | string[]>;
|
|
89
|
+
};
|
|
90
|
+
/**
|
|
91
|
+
* After the input of the page's action (`MapPageAction`) failed its checks, its DataAnnotations or
|
|
92
|
+
* `IValidatableObject`: the errors per field and the submitted values of the fields the action declares, as the page
|
|
93
|
+
* is rendered again from its loader, with and without JavaScript. Else null, also after a server action's failure,
|
|
94
|
+
* which `useAction` reads.
|
|
95
|
+
*/
|
|
96
|
+
export declare function usePageActionState(): PageActionFailure | null;
|
|
97
|
+
export {};
|
|
98
|
+
//# sourceMappingURL=actions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.d.ts","sourceRoot":"","sources":["../src/actions.tsx"],"names":[],"mappings":"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"}
|
package/dist/actions.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { useActionState, useMemo } from 'react';
|
|
2
|
+
import { encodeForm } from './form-data.js';
|
|
3
|
+
import { ANTIFORGERY_HEADER, COOKIES_HEADER, DATA_HEADER, parseActionResponse } from './payload.js';
|
|
4
|
+
import { usePagePayload } from './router.js';
|
|
5
|
+
import { parseWith } from './schema.js';
|
|
6
|
+
/** Thrown into the nearest error boundary when the server could not run the action (not for validation errors). */
|
|
7
|
+
export class ServerActionError extends Error {
|
|
8
|
+
name = 'ServerActionError';
|
|
9
|
+
status;
|
|
10
|
+
constructor(message, status) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.status = status;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
let client = null;
|
|
16
|
+
/**
|
|
17
|
+
* Set from the moment a router renders until it registers: React dispatches the clicks that came in while it hydrated
|
|
18
|
+
* the page before it runs the router's effects, and runs a page's own effects before its router's. Calls made then
|
|
19
|
+
* wait here.
|
|
20
|
+
*/
|
|
21
|
+
let arriving = null;
|
|
22
|
+
/** A router is mounting (called as it first renders, in the browser): calls from now on wait until it registers. */
|
|
23
|
+
export function expectActionClient() {
|
|
24
|
+
if (arriving)
|
|
25
|
+
return;
|
|
26
|
+
let resolve = () => { };
|
|
27
|
+
const ready = new Promise((done) => {
|
|
28
|
+
resolve = done;
|
|
29
|
+
});
|
|
30
|
+
arriving = { ready, resolve };
|
|
31
|
+
}
|
|
32
|
+
/** The router registers itself; there is one per page in the browser. */
|
|
33
|
+
export function setActionClient(next) {
|
|
34
|
+
client = next;
|
|
35
|
+
if (next && arriving) {
|
|
36
|
+
arriving.resolve();
|
|
37
|
+
arriving = null;
|
|
38
|
+
}
|
|
39
|
+
return () => {
|
|
40
|
+
if (client === next)
|
|
41
|
+
client = null;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** How long a call waits for the router on its way; one whose render React threw away never registers. */
|
|
45
|
+
const READY_TIMEOUT_MS = 10_000;
|
|
46
|
+
/**
|
|
47
|
+
* The registered client, once the router on its way has registered; the error without a router on its way, or when it
|
|
48
|
+
* has not registered in time.
|
|
49
|
+
*/
|
|
50
|
+
async function readyClient(name) {
|
|
51
|
+
const waitingFor = arriving;
|
|
52
|
+
if (!client && waitingFor) {
|
|
53
|
+
let timer;
|
|
54
|
+
const timedOut = new Promise((resolve) => {
|
|
55
|
+
timer = setTimeout(() => resolve('timeout'), READY_TIMEOUT_MS);
|
|
56
|
+
});
|
|
57
|
+
// Given up on, so that later calls fail at once.
|
|
58
|
+
if ((await Promise.race([waitingFor.ready, timedOut])) === 'timeout' && arriving === waitingFor)
|
|
59
|
+
arriving = null;
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
}
|
|
62
|
+
if (!client)
|
|
63
|
+
throw new Error(`netjes: server action ${name} was called before the page was ready`);
|
|
64
|
+
return client;
|
|
65
|
+
}
|
|
66
|
+
export const RETURN_FIELD = '__netjes_return';
|
|
67
|
+
const ACTION_META = Symbol.for('netjes.action');
|
|
68
|
+
/** The action URL and name a form action carries, so `<Form>` can render a real form post on the server. */
|
|
69
|
+
export function actionMetaOf(action) {
|
|
70
|
+
return typeof action === 'function' ? action[ACTION_META] : undefined;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The fields of a rejected form, as the failure state's `values`: React resets a form after its action runs, so a
|
|
74
|
+
* `defaultValue={state.values.name}` is what keeps what the user typed. Files, the token and Netjes's own fields are
|
|
75
|
+
* left out, and so is anything outside `fields`: the server names the fields a post without JavaScript would echo
|
|
76
|
+
* (those the action declares, none private).
|
|
77
|
+
*/
|
|
78
|
+
function submittedValues(input, tokenField, fields) {
|
|
79
|
+
if (!(input instanceof FormData))
|
|
80
|
+
return undefined;
|
|
81
|
+
const values = {};
|
|
82
|
+
for (const [key, value] of input.entries()) {
|
|
83
|
+
if (typeof value !== 'string' || key === tokenField || key.startsWith('__netjes_'))
|
|
84
|
+
continue;
|
|
85
|
+
if (fields && !fields.includes(key))
|
|
86
|
+
continue;
|
|
87
|
+
const existing = values[key];
|
|
88
|
+
values[key] = existing === undefined ? value : Array.isArray(existing) ? [...existing, value] : [existing, value];
|
|
89
|
+
}
|
|
90
|
+
return values;
|
|
91
|
+
}
|
|
92
|
+
export function actionRequest(input, token, returnUrl) {
|
|
93
|
+
const headers = { [DATA_HEADER]: '1', Accept: 'application/json' };
|
|
94
|
+
if (token?.headerName && token.requestToken)
|
|
95
|
+
headers[token.headerName] = token.requestToken;
|
|
96
|
+
if (input instanceof FormData) {
|
|
97
|
+
if (!input.has(RETURN_FIELD))
|
|
98
|
+
input.append(RETURN_FIELD, returnUrl);
|
|
99
|
+
const encoded = encodeForm(input);
|
|
100
|
+
if (encoded.contentType)
|
|
101
|
+
headers['Content-Type'] = encoded.contentType;
|
|
102
|
+
return { method: 'POST', headers, body: encoded.body, credentials: 'same-origin' };
|
|
103
|
+
}
|
|
104
|
+
headers['Content-Type'] = 'application/json';
|
|
105
|
+
return { method: 'POST', headers, body: JSON.stringify(input ?? {}), credentials: 'same-origin' };
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The fetch half of a call, for the router's action client. `tokenRefused` says the server refused the call's
|
|
109
|
+
* antiforgery token, so the action did not run; no other answer says that, whatever its status. `onCookiesChanged`
|
|
110
|
+
* runs as soon as the server says the call set or deleted a cookie (signed someone in or out), which script cannot
|
|
111
|
+
* see in the response itself, whatever the rest of the answer turns out to be.
|
|
112
|
+
*/
|
|
113
|
+
export async function fetchAction(name, url, init, onCookiesChanged) {
|
|
114
|
+
const response = await fetch(url, init);
|
|
115
|
+
if (response.headers.get(COOKIES_HEADER) === 'changed')
|
|
116
|
+
onCookiesChanged?.();
|
|
117
|
+
const type = response.headers.get('content-type') ?? '';
|
|
118
|
+
if (!type.includes('application/json'))
|
|
119
|
+
throw new ServerActionError(`Server action ${name} failed with status ${response.status}`, response.status);
|
|
120
|
+
return { response: parseActionResponse(await response.json()), tokenRefused: response.headers.get(ANTIFORGERY_HEADER) === 'invalid' };
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Declares a server action by name. The result is parsed with `options.result` when given (zod, valibot, ...).
|
|
124
|
+
* `npx netjes generate` writes these for every `MapAction`, with types from the C# signature.
|
|
125
|
+
*/
|
|
126
|
+
export function serverAction(name, options = {}) {
|
|
127
|
+
const url = `/_netjes/actions/${name}`;
|
|
128
|
+
async function call(input) {
|
|
129
|
+
const active = client ?? (await readyClient(name));
|
|
130
|
+
const answer = await active.invoke(name, url, input);
|
|
131
|
+
if (!answer.ok && answer.status !== 422) {
|
|
132
|
+
throw new ServerActionError(answer.message ?? `Server action ${name} failed with status ${answer.status}`, answer.status);
|
|
133
|
+
}
|
|
134
|
+
if (!answer.ok) {
|
|
135
|
+
const values = submittedValues(input, active.tokenField?.(), answer.fields);
|
|
136
|
+
return { ok: false, status: answer.status, errors: answer.errors ?? {}, ...(values ? { values } : {}) };
|
|
137
|
+
}
|
|
138
|
+
// `ServerAction.Redirect(...)` and `Ok()` send no result, even from an action whose C# type promises one.
|
|
139
|
+
const result = options.result && answer.result !== undefined ? parseWith(options.result, answer.result, `result of ${name}`) : answer.result;
|
|
140
|
+
return { ok: true, result };
|
|
141
|
+
}
|
|
142
|
+
return Object.assign(call, { actionName: name, url });
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* `useActionState` for a server action: `[state, formAction, isPending]`. Pass `formAction` to `<Form action>`;
|
|
146
|
+
* `useFormStatus` works as usual, and `onSubmit` is where optimistic updates go. After a failed post without
|
|
147
|
+
* JavaScript, the state starts as that failure, with the submitted values.
|
|
148
|
+
*/
|
|
149
|
+
export function useAction(action, options = {}) {
|
|
150
|
+
const { actionState } = usePagePayload();
|
|
151
|
+
const initial = actionState?.action === action.actionName
|
|
152
|
+
? { ok: false, status: 422, errors: actionState.errors, values: actionState.values }
|
|
153
|
+
: (options.initialState ?? null);
|
|
154
|
+
const { onSubmit } = options;
|
|
155
|
+
const [state, dispatch, pending] = useActionState((_previous, formData) => {
|
|
156
|
+
onSubmit?.(formData);
|
|
157
|
+
return action(formData);
|
|
158
|
+
}, initial);
|
|
159
|
+
if (!(ACTION_META in dispatch)) {
|
|
160
|
+
Object.defineProperty(dispatch, ACTION_META, { value: { url: action.url, name: action.actionName } });
|
|
161
|
+
}
|
|
162
|
+
return [state, dispatch, pending];
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* After the input of the page's action (`MapPageAction`) failed its checks, its DataAnnotations or
|
|
166
|
+
* `IValidatableObject`: the errors per field and the submitted values of the fields the action declares, as the page
|
|
167
|
+
* is rendered again from its loader, with and without JavaScript. Else null, also after a server action's failure,
|
|
168
|
+
* which `useAction` reads.
|
|
169
|
+
*/
|
|
170
|
+
export function usePageActionState() {
|
|
171
|
+
const { actionState, route } = usePagePayload();
|
|
172
|
+
return useMemo(() => actionState && route !== null && actionState.action === route
|
|
173
|
+
? { ok: false, status: 422, errors: actionState.errors, values: actionState.values }
|
|
174
|
+
: null, [actionState, route]);
|
|
175
|
+
}
|
|
176
|
+
//# sourceMappingURL=actions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"actions.js","sourceRoot":"","sources":["../src/actions.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-entry.d.ts","sourceRoot":"","sources":["../src/client-entry.tsx"],"names":[],"mappings":"AACA,OAAO,+BAA+B,CAAC"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Must come first: installs the React Fast Refresh runtime before any component module runs (empty in builds).
|
|
3
|
+
import '@vitejs/plugin-react/preamble';
|
|
4
|
+
import { createRoot, hydrateRoot } from 'react-dom/client';
|
|
5
|
+
import { pages } from 'virtual:netjes/pages';
|
|
6
|
+
import { deferredRegistry, revivePayload } from './deferred.js';
|
|
7
|
+
import { createModuleLoader } from './modules.js';
|
|
8
|
+
import { DATA_ELEMENT_ID, SHELL_END_ID, parseInitialPayload } from './payload.js';
|
|
9
|
+
import { Root } from './root.js';
|
|
10
|
+
import { withClockOffset } from './router.js';
|
|
11
|
+
/**
|
|
12
|
+
* The payload is the body's first element, so it is normally parsed by the time this async script runs. If the
|
|
13
|
+
* script won the race, wait for the element rather than for the whole (streaming) document.
|
|
14
|
+
*/
|
|
15
|
+
function payloadElement() {
|
|
16
|
+
// Complete once the parser has moved past it (a large payload can arrive in several chunks).
|
|
17
|
+
const complete = () => {
|
|
18
|
+
const element = document.getElementById(DATA_ELEMENT_ID);
|
|
19
|
+
return element && (element.nextSibling || document.readyState !== 'loading') ? element : null;
|
|
20
|
+
};
|
|
21
|
+
const found = complete();
|
|
22
|
+
if (found || document.readyState !== 'loading')
|
|
23
|
+
return Promise.resolve(found ?? document.getElementById(DATA_ELEMENT_ID));
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
const observer = new MutationObserver(() => {
|
|
26
|
+
const element = complete();
|
|
27
|
+
if (element) {
|
|
28
|
+
observer.disconnect();
|
|
29
|
+
resolve(element);
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
observer.observe(document, { childList: true, subtree: true });
|
|
33
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
34
|
+
observer.disconnect();
|
|
35
|
+
resolve(document.getElementById(DATA_ELEMENT_ID));
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolves once the parser has passed the end of the shell, the empty element `Document` writes last in the body.
|
|
41
|
+
* This script is async, so a cached copy can run as soon as the payload is in, before most of the page is: hydrating
|
|
42
|
+
* then would meet half a document, give up and render the page in the browser, and the parser would add the rest of
|
|
43
|
+
* the server's HTML below it, so the page would show twice.
|
|
44
|
+
*/
|
|
45
|
+
function shellParsed() {
|
|
46
|
+
if (document.getElementById(SHELL_END_ID) || document.readyState !== 'loading')
|
|
47
|
+
return Promise.resolve();
|
|
48
|
+
return new Promise((resolve) => {
|
|
49
|
+
const done = () => {
|
|
50
|
+
observer.disconnect();
|
|
51
|
+
document.removeEventListener('DOMContentLoaded', done);
|
|
52
|
+
resolve();
|
|
53
|
+
};
|
|
54
|
+
const observer = new MutationObserver(() => {
|
|
55
|
+
if (document.getElementById(SHELL_END_ID))
|
|
56
|
+
done();
|
|
57
|
+
});
|
|
58
|
+
observer.observe(document, { childList: true, subtree: true });
|
|
59
|
+
document.addEventListener('DOMContentLoaded', done);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
const dataJson = (await payloadElement())?.textContent;
|
|
63
|
+
if (!dataJson)
|
|
64
|
+
throw new Error(`netjes: #${DATA_ELEMENT_ID} is missing; was this page rendered by Netjes?`);
|
|
65
|
+
const globals = self;
|
|
66
|
+
const deferred = deferredRegistry();
|
|
67
|
+
const settle = (id, value, error) => (error === undefined || error === null ? deferred.resolve(id, value) : deferred.reject(id, error));
|
|
68
|
+
/**
|
|
69
|
+
* The values the head bootstrap collected: its own plain object, `{ id: [value, error] }`. Anything else is page
|
|
70
|
+
* markup standing in for the global (an element with `id="__netjes_v"`), and must not stop the page from hydrating.
|
|
71
|
+
*/
|
|
72
|
+
function collectedValues(collected) {
|
|
73
|
+
if (typeof collected !== 'object' || collected === null)
|
|
74
|
+
return [];
|
|
75
|
+
const prototype = Object.getPrototypeOf(collected);
|
|
76
|
+
return prototype === Object.prototype || prototype === null ? Object.entries(collected) : [];
|
|
77
|
+
}
|
|
78
|
+
for (const [id, entry] of collectedValues(globals.__netjes_v)) {
|
|
79
|
+
if (Array.isArray(entry))
|
|
80
|
+
settle(id, entry[0], typeof entry[1] === 'string' ? entry[1] : undefined);
|
|
81
|
+
}
|
|
82
|
+
globals.__netjes_d = settle;
|
|
83
|
+
const parsed = revivePayload(parseInitialPayload(JSON.parse(dataJson)), deferred.source);
|
|
84
|
+
// Judge the server's seeded data as fresh as the server did when it rendered, so hydration matches.
|
|
85
|
+
const payload = withClockOffset(parsed, parsed.renderedAt);
|
|
86
|
+
const loader = createModuleLoader(pages, { nameFailedImports: true });
|
|
87
|
+
/** How long the first page waits before it tries a module that failed to load once more. */
|
|
88
|
+
const RETRY_DELAY_MS = 500;
|
|
89
|
+
/**
|
|
90
|
+
* The first page's modules. One that fails to load (a dropped request, a deploy that removed its chunk) gets one more
|
|
91
|
+
* try, which the browser fetches again. If that fails too, the page stays as the server sent it, without the client
|
|
92
|
+
* router: its links and forms are plain HTML, and the browser loads each in full.
|
|
93
|
+
*/
|
|
94
|
+
async function firstPageModules() {
|
|
95
|
+
try {
|
|
96
|
+
return await loader.loadRoute(payload);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
await new Promise((resolve) => setTimeout(resolve, RETRY_DELAY_MS));
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return await loader.loadRoute(payload);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
const reason = error instanceof Error ? error.message : `netjes: the page's modules could not be loaded (${String(error)})`;
|
|
106
|
+
const outcome = payload.document.clientRender
|
|
107
|
+
? 'the page cannot render without it'
|
|
108
|
+
: 'the page stays as the server rendered it, without the client router, and its links and forms load pages in full';
|
|
109
|
+
console.error(`${reason}: ${outcome}`, error);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const modules = await firstPageModules();
|
|
114
|
+
if (modules) {
|
|
115
|
+
const root = _jsx(Root, { payload: payload, modules: modules, loader: loader, dataJson: dataJson });
|
|
116
|
+
if (payload.document.clientRender) {
|
|
117
|
+
// No sidecar was ready, so the server sent the document without the page: there is nothing to hydrate. React keeps
|
|
118
|
+
// the scripts and stylesheets already in it and replaces the rest.
|
|
119
|
+
createRoot(document).render(root);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
await shellParsed();
|
|
123
|
+
hydrateRoot(document, root);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=client-entry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client-entry.js","sourceRoot":"","sources":["../src/client-entry.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { PagePayload } from './payload.ts';
|
|
2
|
+
/** A promise that records its outcome the way React's `use()` reads it, so a settled one renders without suspending. */
|
|
3
|
+
export type Thenable<T> = Promise<T> & {
|
|
4
|
+
status?: 'pending' | 'fulfilled' | 'rejected';
|
|
5
|
+
value?: T;
|
|
6
|
+
reason?: unknown;
|
|
7
|
+
};
|
|
8
|
+
export type Deferred<T> = {
|
|
9
|
+
promise: Thenable<T>;
|
|
10
|
+
resolve(value: T): void;
|
|
11
|
+
reject(reason: unknown): void;
|
|
12
|
+
};
|
|
13
|
+
export declare function createDeferred<T>(): Deferred<T>;
|
|
14
|
+
/** Where revived placeholders get their promise from, by id. */
|
|
15
|
+
export type DeferredSource = (id: string) => Thenable<unknown>;
|
|
16
|
+
/** Replaces every `{ $netjes: 'deferred', id }` in a JSON value; returns the same object when there are none. */
|
|
17
|
+
export declare function reviveDeferred(value: unknown, source: DeferredSource): unknown;
|
|
18
|
+
/** Revives the placeholders in a page's props and its segments' props. */
|
|
19
|
+
export declare function revivePayload<T extends PagePayload>(payload: T, source: DeferredSource): T;
|
|
20
|
+
/** A source that hands out one deferred per id and settles it when the value arrives. */
|
|
21
|
+
export declare function deferredRegistry(): {
|
|
22
|
+
source: DeferredSource;
|
|
23
|
+
resolve(id: string, value: unknown): void;
|
|
24
|
+
reject(id: string, message: string): void;
|
|
25
|
+
/** Rejects every value still pending, e.g. when the stream that was to deliver them ended. */
|
|
26
|
+
rejectPending(message: (id: string) => string): void;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Inline in every document: collects deferred values streamed as `<script>self.__netjes_d(id, value, error)</script>`
|
|
30
|
+
* until the client entry takes over (it replaces `__netjes_d`). The async client entry can run first, so this never
|
|
31
|
+
* replaces a handler that is already there. It creates `__netjes_v` up front, as a window property of its own: until
|
|
32
|
+
* then, page markup with `id="__netjes_v"` would be what that name reads (DOM clobbering).
|
|
33
|
+
*/
|
|
34
|
+
export declare const DEFERRED_BOOTSTRAP = "self.__netjes_d=self.__netjes_d||function(i,v,e){self.__netjes_v[i]=[v,e]};Object.prototype.hasOwnProperty.call(self,'__netjes_v')||(self.__netjes_v={})";
|
|
35
|
+
/** JSON that can sit inside a <script> element. */
|
|
36
|
+
export declare function htmlSafeJson(value: unknown): string;
|
|
37
|
+
/** The script that hands one deferred value (or its error) to the browser. */
|
|
38
|
+
export declare function deferredScript(id: string, value: unknown, error?: string, nonce?: string): string;
|
|
39
|
+
//# sourceMappingURL=deferred.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deferred.d.ts","sourceRoot":"","sources":["../src/deferred.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,WAAW,EAAc,MAAM,cAAc,CAAC;AAE5D,wHAAwH;AACxH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,GAAG;IAAE,MAAM,CAAC,EAAE,SAAS,GAAG,WAAW,GAAG,UAAU,CAAC;IAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAEtH,MAAM,MAAM,QAAQ,CAAC,CAAC,IAAI;IAAE,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAAC,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI,CAAC;IAAC,MAAM,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAA;CAAE,CAAC;AAE3G,wBAAgB,cAAc,CAAC,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAuB/C;AAED,gEAAgE;AAChE,MAAM,MAAM,cAAc,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,QAAQ,CAAC,OAAO,CAAC,CAAC;AAM/D,iHAAiH;AACjH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,cAAc,GAAG,OAAO,CAoB9E;AAED,0EAA0E;AAC1E,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,EAAE,OAAO,EAAE,CAAC,EAAE,MAAM,EAAE,cAAc,GAAG,CAAC,CAW1F;AAED,yFAAyF;AACzF,wBAAgB,gBAAgB;IAa5B,MAAM,EAAiC,cAAc;IACrD,OAAO,KAAK,MAAM,SAAS,OAAO;IAGlC,MAAM,KAAK,MAAM,WAAW,MAAM;IAGlC,8FAA8F;IAC9F,aAAa,UAAU,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM;EAMhD;AAED;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,6JAC6H,CAAC;AAI7J,mDAAmD;AACnD,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAEnD;AAED,8EAA8E;AAC9E,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAQjG"}
|
package/dist/deferred.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export function createDeferred() {
|
|
2
|
+
let resolve;
|
|
3
|
+
let reject;
|
|
4
|
+
const promise = new Promise((res, rej) => {
|
|
5
|
+
resolve = res;
|
|
6
|
+
reject = rej;
|
|
7
|
+
});
|
|
8
|
+
promise.status = 'pending';
|
|
9
|
+
return {
|
|
10
|
+
promise,
|
|
11
|
+
resolve(value) {
|
|
12
|
+
if (promise.status !== 'pending')
|
|
13
|
+
return;
|
|
14
|
+
promise.status = 'fulfilled';
|
|
15
|
+
promise.value = value;
|
|
16
|
+
resolve(value);
|
|
17
|
+
},
|
|
18
|
+
reject(reason) {
|
|
19
|
+
if (promise.status !== 'pending')
|
|
20
|
+
return;
|
|
21
|
+
promise.status = 'rejected';
|
|
22
|
+
promise.reason = reason;
|
|
23
|
+
reject(reason);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function isPlaceholder(value) {
|
|
28
|
+
return value.$netjes === 'deferred' && typeof value.id === 'string';
|
|
29
|
+
}
|
|
30
|
+
/** Replaces every `{ $netjes: 'deferred', id }` in a JSON value; returns the same object when there are none. */
|
|
31
|
+
export function reviveDeferred(value, source) {
|
|
32
|
+
if (typeof value !== 'object' || value === null)
|
|
33
|
+
return value;
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
let changed = false;
|
|
36
|
+
const items = value.map((item) => {
|
|
37
|
+
const revived = reviveDeferred(item, source);
|
|
38
|
+
if (revived !== item)
|
|
39
|
+
changed = true;
|
|
40
|
+
return revived;
|
|
41
|
+
});
|
|
42
|
+
return changed ? items : value;
|
|
43
|
+
}
|
|
44
|
+
if (isPlaceholder(value))
|
|
45
|
+
return source(value.id);
|
|
46
|
+
let changed = false;
|
|
47
|
+
const entries = Object.entries(value).map(([key, item]) => {
|
|
48
|
+
const revived = reviveDeferred(item, source);
|
|
49
|
+
if (revived !== item)
|
|
50
|
+
changed = true;
|
|
51
|
+
return [key, revived];
|
|
52
|
+
});
|
|
53
|
+
// Keys are defined, not assigned: JSON.parse keeps "__proto__" as a key, and assigning it would set the prototype.
|
|
54
|
+
return changed ? Object.fromEntries(entries) : value;
|
|
55
|
+
}
|
|
56
|
+
/** Revives the placeholders in a page's props and its segments' props. */
|
|
57
|
+
export function revivePayload(payload, source) {
|
|
58
|
+
const props = reviveDeferred(payload.props, source);
|
|
59
|
+
let segmentsChanged = false;
|
|
60
|
+
const segments = payload.segments.map((segment) => {
|
|
61
|
+
if (!segment.props)
|
|
62
|
+
return segment;
|
|
63
|
+
const revived = reviveDeferred(segment.props, source);
|
|
64
|
+
if (revived === segment.props)
|
|
65
|
+
return segment;
|
|
66
|
+
segmentsChanged = true;
|
|
67
|
+
return { ...segment, props: revived };
|
|
68
|
+
});
|
|
69
|
+
return props === payload.props && !segmentsChanged ? payload : { ...payload, props, segments };
|
|
70
|
+
}
|
|
71
|
+
/** A source that hands out one deferred per id and settles it when the value arrives. */
|
|
72
|
+
export function deferredRegistry() {
|
|
73
|
+
const entries = new Map();
|
|
74
|
+
const entry = (id) => {
|
|
75
|
+
let found = entries.get(id);
|
|
76
|
+
if (!found) {
|
|
77
|
+
found = createDeferred();
|
|
78
|
+
// Unread values must not surface as unhandled rejections.
|
|
79
|
+
found.promise.catch(() => { });
|
|
80
|
+
entries.set(id, found);
|
|
81
|
+
}
|
|
82
|
+
return found;
|
|
83
|
+
};
|
|
84
|
+
return {
|
|
85
|
+
source: ((id) => entry(id).promise),
|
|
86
|
+
resolve(id, value) {
|
|
87
|
+
entry(id).resolve(value);
|
|
88
|
+
},
|
|
89
|
+
reject(id, message) {
|
|
90
|
+
entry(id).reject(new Error(message));
|
|
91
|
+
},
|
|
92
|
+
/** Rejects every value still pending, e.g. when the stream that was to deliver them ended. */
|
|
93
|
+
rejectPending(message) {
|
|
94
|
+
for (const [id, deferred] of entries) {
|
|
95
|
+
if (deferred.promise.status === 'pending')
|
|
96
|
+
deferred.reject(new Error(message(id)));
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Inline in every document: collects deferred values streamed as `<script>self.__netjes_d(id, value, error)</script>`
|
|
103
|
+
* until the client entry takes over (it replaces `__netjes_d`). The async client entry can run first, so this never
|
|
104
|
+
* replaces a handler that is already there. It creates `__netjes_v` up front, as a window property of its own: until
|
|
105
|
+
* then, page markup with `id="__netjes_v"` would be what that name reads (DOM clobbering).
|
|
106
|
+
*/
|
|
107
|
+
export const DEFERRED_BOOTSTRAP = "self.__netjes_d=self.__netjes_d||function(i,v,e){self.__netjes_v[i]=[v,e]};Object.prototype.hasOwnProperty.call(self,'__netjes_v')||(self.__netjes_v={})";
|
|
108
|
+
const UNSAFE = /[<>&\u2028\u2029]/g;
|
|
109
|
+
/** JSON that can sit inside a <script> element. */
|
|
110
|
+
export function htmlSafeJson(value) {
|
|
111
|
+
return (JSON.stringify(value) ?? 'null').replace(UNSAFE, (c) => `\\u${c.charCodeAt(0).toString(16).padStart(4, '0')}`);
|
|
112
|
+
}
|
|
113
|
+
/** The script that hands one deferred value (or its error) to the browser. */
|
|
114
|
+
export function deferredScript(id, value, error, nonce) {
|
|
115
|
+
// The value goes as JSON text for JSON.parse, which reads it as the other paths do: as an object literal, a
|
|
116
|
+
// "__proto__" key would set the object's prototype instead.
|
|
117
|
+
const json = htmlSafeJson(JSON.stringify(value) ?? 'null');
|
|
118
|
+
const args = error === undefined ? `${htmlSafeJson(id)},JSON.parse(${json})` : `${htmlSafeJson(id)},null,${htmlSafeJson(error)}`;
|
|
119
|
+
// Under a CSP with nonces, inline scripts run only with the page's nonce (base64, but escaped all the same).
|
|
120
|
+
const attribute = nonce ? ` nonce="${nonce.replace(/[^A-Za-z0-9+/=_-]/g, '')}"` : '';
|
|
121
|
+
return `<script${attribute}>self.__netjes_d(${args})</script>`;
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=deferred.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deferred.js","sourceRoot":"","sources":["../src/deferred.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ComponentType, FunctionComponent } from 'react';
|
|
2
|
+
import type { LayoutProps } from './modules.ts';
|
|
3
|
+
import { type StandardSchemaV1 } from './schema.ts';
|
|
4
|
+
export type { StandardSchemaV1 } from './schema.ts';
|
|
5
|
+
/**
|
|
6
|
+
* Declares a page whose props (sent by the C# loader) are validated with a Standard Schema before rendering,
|
|
7
|
+
* so the component receives parsed, typed props instead of trusting the server's JSON.
|
|
8
|
+
*/
|
|
9
|
+
export declare function definePage<Schema extends StandardSchemaV1>(schema: Schema, Page: ComponentType<StandardSchemaV1.InferOutput<Schema>>): FunctionComponent<Record<string, unknown>>;
|
|
10
|
+
/**
|
|
11
|
+
* Declares a layout whose loader props (from `MapLayoutLoader` in C#) are validated with a Standard Schema. The
|
|
12
|
+
* component gets the parsed props next to `children` and `params`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function defineLayout<Schema extends StandardSchemaV1>(schema: Schema, Layout: ComponentType<StandardSchemaV1.InferOutput<Schema> & LayoutProps>): FunctionComponent<LayoutProps & Record<string, unknown>>;
|
|
15
|
+
//# sourceMappingURL=define-page.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"define-page.d.ts","sourceRoot":"","sources":["../src/define-page.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAa,MAAM,OAAO,CAAC;AACzE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,KAAK,gBAAgB,EAAa,MAAM,aAAa,CAAC;AAE/D,YAAY,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEpD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,MAAM,SAAS,gBAAgB,EACxD,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,aAAa,CAAC,gBAAgB,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,GACxD,iBAAiB,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAO5C;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,SAAS,gBAAgB,EAC1D,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,aAAa,CAAC,gBAAgB,CAAC,WAAW,CAAC,MAAM,CAAC,GAAG,WAAW,CAAC,GACxE,iBAAiB,CAAC,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAW1D"}
|