@ossido-labs/ossido 0.1.2 → 0.1.3
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/dist/esm/actions/index.d.ts +72 -0
- package/dist/esm/actions/index.js +176 -0
- package/dist/esm/actions/index.js.map +1 -0
- package/dist/esm/ssr/polyfills/MessageChannel.js +4 -1
- package/dist/esm/ssr/polyfills/MessageChannel.js.map +1 -1
- package/dist/esm/ssr/server.js +3 -3
- package/dist/esm/ssr/server.js.map +1 -1
- package/package.json +8 -4
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client runtime for Ossido server actions (`#[ossido::action]`).
|
|
3
|
+
*
|
|
4
|
+
* Ossido is not RSC, so there is no `'use server'` server-reference mechanism.
|
|
5
|
+
* Instead the build generates a concrete, typed function per action into
|
|
6
|
+
* `.ossido/actions.ts`, each created by {@link createAction} /
|
|
7
|
+
* {@link createStatefulAction} here. The generated functions satisfy the React
|
|
8
|
+
* 19 call contracts exactly:
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { createUser, submitSignup } from '.ossido/actions'
|
|
12
|
+
* import { Form, useActionState } from '@ossido-labs/ossido/actions'
|
|
13
|
+
*
|
|
14
|
+
* // 1. Imperative, fully typed:
|
|
15
|
+
* const created = await createUser({ name, email })
|
|
16
|
+
*
|
|
17
|
+
* // 2. Progressive-enhancement form (works without JS via a native POST):
|
|
18
|
+
* <Form action={createUser}><input name="name" /></Form>
|
|
19
|
+
*
|
|
20
|
+
* // 3. useActionState:
|
|
21
|
+
* const [state, formAction, isPending] = useActionState(submitSignup, initial)
|
|
22
|
+
* <form action={formAction}>…</form>
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
import { type FormHTMLAttributes, type ReactNode } from 'react';
|
|
26
|
+
/** A domain error returned by an action as `Err(ActionError)` on the server. */
|
|
27
|
+
export interface ActionError {
|
|
28
|
+
message: string;
|
|
29
|
+
fields?: Record<string, string>;
|
|
30
|
+
}
|
|
31
|
+
/** Thrown by an action call when the server returns an error (or panics). */
|
|
32
|
+
export declare class OssidoActionError extends Error {
|
|
33
|
+
fields?: Record<string, string>;
|
|
34
|
+
constructor(detail: ActionError);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A generated action. Callable imperatively with the typed input, or passed to
|
|
38
|
+
* `<form action={fn}>` (React calls it with `FormData`). `url` is the endpoint,
|
|
39
|
+
* used by {@link Form} for the no-JS native POST.
|
|
40
|
+
*/
|
|
41
|
+
export interface ActionFn<Input, Output> {
|
|
42
|
+
(input: Input): Promise<Output>;
|
|
43
|
+
(formData: FormData): Promise<Output>;
|
|
44
|
+
url: string;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A generated stateful action (`useActionState`). Callable imperatively, or
|
|
48
|
+
* invoked by React as `(prevState, formData)`.
|
|
49
|
+
*/
|
|
50
|
+
export interface StatefulActionFn<Input, Output, Prev> {
|
|
51
|
+
(input: Input): Promise<Output>;
|
|
52
|
+
(prev: Prev | null, formData: FormData): Promise<Output>;
|
|
53
|
+
url: string;
|
|
54
|
+
}
|
|
55
|
+
/** Build a stateless action bound to `url`. Used by the generated client. */
|
|
56
|
+
export declare function createAction<Input, Output>(url: string): ActionFn<Input, Output>;
|
|
57
|
+
/** Build a stateful action (`useActionState`) bound to `url`. */
|
|
58
|
+
export declare function createStatefulAction<Input, Output, Prev>(url: string): StatefulActionFn<Input, Output, Prev>;
|
|
59
|
+
export interface FormProps<Input, Output> extends Omit<FormHTMLAttributes<HTMLFormElement>, 'action' | 'method'> {
|
|
60
|
+
/** A generated action (e.g. `createUser`). */
|
|
61
|
+
action: ActionFn<Input, Output>;
|
|
62
|
+
children?: ReactNode;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* A `<form>` wired to a server action with progressive enhancement. When
|
|
66
|
+
* hydrated, submission is intercepted and the action is called with `FormData`.
|
|
67
|
+
* With JS disabled, the rendered `action={fn.url}` makes it a native POST, and
|
|
68
|
+
* the server responds with a 303 Post/Redirect/Get.
|
|
69
|
+
*/
|
|
70
|
+
export declare function Form<Input, Output>({ action, children, onSubmit, ...rest }: FormProps<Input, Output>): ReactNode;
|
|
71
|
+
export { useActionState } from 'react';
|
|
72
|
+
export { useFormStatus } from 'react-dom';
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { createElement, useActionState } from "react";
|
|
2
|
+
import { useFormStatus } from "react-dom";
|
|
3
|
+
|
|
4
|
+
//#region src/actions/index.tsx
|
|
5
|
+
/**
|
|
6
|
+
* Client runtime for Ossido server actions (`#[ossido::action]`).
|
|
7
|
+
*
|
|
8
|
+
* Ossido is not RSC, so there is no `'use server'` server-reference mechanism.
|
|
9
|
+
* Instead the build generates a concrete, typed function per action into
|
|
10
|
+
* `.ossido/actions.ts`, each created by {@link createAction} /
|
|
11
|
+
* {@link createStatefulAction} here. The generated functions satisfy the React
|
|
12
|
+
* 19 call contracts exactly:
|
|
13
|
+
*
|
|
14
|
+
* ```tsx
|
|
15
|
+
* import { createUser, submitSignup } from '.ossido/actions'
|
|
16
|
+
* import { Form, useActionState } from '@ossido-labs/ossido/actions'
|
|
17
|
+
*
|
|
18
|
+
* // 1. Imperative, fully typed:
|
|
19
|
+
* const created = await createUser({ name, email })
|
|
20
|
+
*
|
|
21
|
+
* // 2. Progressive-enhancement form (works without JS via a native POST):
|
|
22
|
+
* <Form action={createUser}><input name="name" /></Form>
|
|
23
|
+
*
|
|
24
|
+
* // 3. useActionState:
|
|
25
|
+
* const [state, formAction, isPending] = useActionState(submitSignup, initial)
|
|
26
|
+
* <form action={formAction}>…</form>
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
/** The reserved field carrying the previous `useActionState` value. */
|
|
30
|
+
const PREV_STATE_FIELD = "__ossido_prev_state";
|
|
31
|
+
/** Thrown by an action call when the server returns an error (or panics). */
|
|
32
|
+
var OssidoActionError = class extends Error {
|
|
33
|
+
constructor(detail) {
|
|
34
|
+
super(detail.message);
|
|
35
|
+
this.name = "OssidoActionError";
|
|
36
|
+
this.fields = detail.fields;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
function navigate(destination) {
|
|
40
|
+
if (typeof window !== "undefined") window.location.assign(destination);
|
|
41
|
+
}
|
|
42
|
+
async function parse(response) {
|
|
43
|
+
let body;
|
|
44
|
+
try {
|
|
45
|
+
body = await response.json();
|
|
46
|
+
} catch {
|
|
47
|
+
body = void 0;
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
ok: response.ok,
|
|
51
|
+
status: response.status,
|
|
52
|
+
body
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function unwrap({ ok, status, body }) {
|
|
56
|
+
if (ok) {
|
|
57
|
+
if (body) {
|
|
58
|
+
if (typeof body.redirect === "string") {
|
|
59
|
+
navigate(body.redirect);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if ("data" in body) return body.data;
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (body && body.error) throw new OssidoActionError(body.error);
|
|
67
|
+
const info = body?.info;
|
|
68
|
+
throw new OssidoActionError({ message: info?.serverError?.message ?? `Server action failed (${status})` });
|
|
69
|
+
}
|
|
70
|
+
function formDataHasFiles(formData) {
|
|
71
|
+
let hasFile = false;
|
|
72
|
+
formData.forEach((value) => {
|
|
73
|
+
if (typeof value !== "string") hasFile = true;
|
|
74
|
+
});
|
|
75
|
+
return hasFile;
|
|
76
|
+
}
|
|
77
|
+
function cloneFormData(formData) {
|
|
78
|
+
const clone = new FormData();
|
|
79
|
+
formData.forEach((value, key) => {
|
|
80
|
+
if (typeof value === "string") clone.append(key, value);
|
|
81
|
+
else clone.append(key, value, value.name);
|
|
82
|
+
});
|
|
83
|
+
return clone;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* POST an action's input, choosing the encoding:
|
|
87
|
+
* - a plain object → JSON;
|
|
88
|
+
* - a `FormData` with only text fields → `application/x-www-form-urlencoded`;
|
|
89
|
+
* - a `FormData` containing files → `multipart/form-data` (sent as-is so the
|
|
90
|
+
* browser sets the boundary and the files are preserved).
|
|
91
|
+
*/
|
|
92
|
+
async function callAction(url, input, hasPrev, prevState) {
|
|
93
|
+
let response;
|
|
94
|
+
if (typeof FormData !== "undefined" && input instanceof FormData) {
|
|
95
|
+
if (formDataHasFiles(input)) {
|
|
96
|
+
let body = input;
|
|
97
|
+
if (hasPrev) {
|
|
98
|
+
body = cloneFormData(input);
|
|
99
|
+
body.set(PREV_STATE_FIELD, JSON.stringify(prevState ?? null));
|
|
100
|
+
}
|
|
101
|
+
response = await fetch(url, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: {
|
|
104
|
+
Accept: "application/json",
|
|
105
|
+
"X-Ossido-Action": "1"
|
|
106
|
+
},
|
|
107
|
+
body
|
|
108
|
+
});
|
|
109
|
+
} else {
|
|
110
|
+
const params = new URLSearchParams();
|
|
111
|
+
input.forEach((value, key) => {
|
|
112
|
+
if (typeof value === "string") params.append(key, value);
|
|
113
|
+
});
|
|
114
|
+
if (hasPrev) params.set(PREV_STATE_FIELD, JSON.stringify(prevState ?? null));
|
|
115
|
+
response = await fetch(url, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
119
|
+
Accept: "application/json",
|
|
120
|
+
"X-Ossido-Action": "1"
|
|
121
|
+
},
|
|
122
|
+
body: params.toString()
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
} else {
|
|
126
|
+
const payload = { input };
|
|
127
|
+
if (hasPrev) payload[PREV_STATE_FIELD] = prevState ?? null;
|
|
128
|
+
response = await fetch(url, {
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: {
|
|
131
|
+
"Content-Type": "application/json",
|
|
132
|
+
Accept: "application/json",
|
|
133
|
+
"X-Ossido-Action": "1"
|
|
134
|
+
},
|
|
135
|
+
body: JSON.stringify(payload)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
return unwrap(await parse(response));
|
|
139
|
+
}
|
|
140
|
+
/** Build a stateless action bound to `url`. Used by the generated client. */
|
|
141
|
+
function createAction(url) {
|
|
142
|
+
const fn = (arg) => callAction(url, arg, false, void 0);
|
|
143
|
+
return Object.assign(fn, { url });
|
|
144
|
+
}
|
|
145
|
+
/** Build a stateful action (`useActionState`) bound to `url`. */
|
|
146
|
+
function createStatefulAction(url) {
|
|
147
|
+
const fn = (a, b) => {
|
|
148
|
+
if (typeof FormData !== "undefined" && b instanceof FormData) return callAction(url, b, true, a);
|
|
149
|
+
return callAction(url, a, false, void 0);
|
|
150
|
+
};
|
|
151
|
+
return Object.assign(fn, { url });
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* A `<form>` wired to a server action with progressive enhancement. When
|
|
155
|
+
* hydrated, submission is intercepted and the action is called with `FormData`.
|
|
156
|
+
* With JS disabled, the rendered `action={fn.url}` makes it a native POST, and
|
|
157
|
+
* the server responds with a 303 Post/Redirect/Get.
|
|
158
|
+
*/
|
|
159
|
+
function Form({ action, children, onSubmit, ...rest }) {
|
|
160
|
+
const handleSubmit = (event) => {
|
|
161
|
+
onSubmit?.(event);
|
|
162
|
+
if (event.defaultPrevented) return;
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
action(new FormData(event.currentTarget));
|
|
165
|
+
};
|
|
166
|
+
return createElement("form", {
|
|
167
|
+
method: "post",
|
|
168
|
+
action: action.url,
|
|
169
|
+
onSubmit: handleSubmit,
|
|
170
|
+
...rest
|
|
171
|
+
}, children);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
//#endregion
|
|
175
|
+
export { Form, OssidoActionError, createAction, createStatefulAction, useActionState, useFormStatus };
|
|
176
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/actions/index.tsx"],"sourcesContent":["/* eslint-disable react-refresh/only-export-components -- Library entry point:\n intentionally exports the action runtime helpers alongside the <Form>\n component. This module is never a Fast Refresh boundary. */\n/**\n * Client runtime for Ossido server actions (`#[ossido::action]`).\n *\n * Ossido is not RSC, so there is no `'use server'` server-reference mechanism.\n * Instead the build generates a concrete, typed function per action into\n * `.ossido/actions.ts`, each created by {@link createAction} /\n * {@link createStatefulAction} here. The generated functions satisfy the React\n * 19 call contracts exactly:\n *\n * ```tsx\n * import { createUser, submitSignup } from '.ossido/actions'\n * import { Form, useActionState } from '@ossido-labs/ossido/actions'\n *\n * // 1. Imperative, fully typed:\n * const created = await createUser({ name, email })\n *\n * // 2. Progressive-enhancement form (works without JS via a native POST):\n * <Form action={createUser}><input name=\"name\" /></Form>\n *\n * // 3. useActionState:\n * const [state, formAction, isPending] = useActionState(submitSignup, initial)\n * <form action={formAction}>…</form>\n * ```\n */\nimport {\n createElement,\n type FormEvent,\n type FormHTMLAttributes,\n type ReactNode,\n} from 'react';\n\n/** The reserved field carrying the previous `useActionState` value. */\nconst PREV_STATE_FIELD = '__ossido_prev_state';\n\n/** A domain error returned by an action as `Err(ActionError)` on the server. */\nexport interface ActionError {\n message: string;\n fields?: Record<string, string>;\n}\n\n/** Thrown by an action call when the server returns an error (or panics). */\nexport class OssidoActionError extends Error {\n fields?: Record<string, string>;\n constructor(detail: ActionError) {\n super(detail.message);\n this.name = 'OssidoActionError';\n this.fields = detail.fields;\n }\n}\n\n/**\n * A generated action. Callable imperatively with the typed input, or passed to\n * `<form action={fn}>` (React calls it with `FormData`). `url` is the endpoint,\n * used by {@link Form} for the no-JS native POST.\n */\nexport interface ActionFn<Input, Output> {\n (input: Input): Promise<Output>;\n (formData: FormData): Promise<Output>;\n url: string;\n}\n\n/**\n * A generated stateful action (`useActionState`). Callable imperatively, or\n * invoked by React as `(prevState, formData)`.\n */\nexport interface StatefulActionFn<Input, Output, Prev> {\n (input: Input): Promise<Output>;\n (prev: Prev | null, formData: FormData): Promise<Output>;\n url: string;\n}\n\ninterface Parsed {\n ok: boolean;\n status: number;\n // The parsed JSON envelope, or undefined if the body was not JSON.\n body: Record<string, unknown> | undefined;\n}\n\nfunction navigate(destination: string): void {\n if (typeof window !== 'undefined') {\n window.location.assign(destination);\n }\n}\n\nasync function parse(response: Response): Promise<Parsed> {\n let body: Record<string, unknown> | undefined;\n try {\n body = (await response.json()) as Record<string, unknown>;\n } catch {\n body = undefined;\n }\n return { ok: response.ok, status: response.status, body };\n}\n\nfunction unwrap<Output>({ ok, status, body }: Parsed): Output {\n if (ok) {\n if (body) {\n if (typeof body.redirect === 'string') {\n navigate(body.redirect);\n return undefined as Output;\n }\n if ('data' in body) return body.data as Output;\n }\n return undefined as Output;\n }\n // Non-2xx: a typed action error, else a panic (`{ info: { serverError } }`).\n if (body && body.error) {\n throw new OssidoActionError(body.error as ActionError);\n }\n const info = body?.info as { serverError?: { message?: string } } | undefined;\n throw new OssidoActionError({\n message: info?.serverError?.message ?? `Server action failed (${status})`,\n });\n}\n\nfunction formDataHasFiles(formData: FormData): boolean {\n let hasFile = false;\n formData.forEach((value) => {\n if (typeof value !== 'string') hasFile = true;\n });\n return hasFile;\n}\n\nfunction cloneFormData(formData: FormData): FormData {\n const clone = new FormData();\n formData.forEach((value, key) => {\n if (typeof value === 'string') clone.append(key, value);\n else clone.append(key, value, value.name);\n });\n return clone;\n}\n\n/**\n * POST an action's input, choosing the encoding:\n * - a plain object → JSON;\n * - a `FormData` with only text fields → `application/x-www-form-urlencoded`;\n * - a `FormData` containing files → `multipart/form-data` (sent as-is so the\n * browser sets the boundary and the files are preserved).\n */\nasync function callAction<Output>(\n url: string,\n input: unknown,\n hasPrev: boolean,\n prevState: unknown,\n): Promise<Output> {\n let response: Response;\n\n if (typeof FormData !== 'undefined' && input instanceof FormData) {\n if (formDataHasFiles(input)) {\n // Send the FormData directly; do NOT set Content-Type (the browser adds\n // the multipart boundary). Clone before appending prev-state so the\n // caller's FormData is not mutated.\n let body = input;\n if (hasPrev) {\n body = cloneFormData(input);\n body.set(PREV_STATE_FIELD, JSON.stringify(prevState ?? null));\n }\n response = await fetch(url, {\n method: 'POST',\n headers: { Accept: 'application/json', 'X-Ossido-Action': '1' },\n body,\n });\n } else {\n const params = new URLSearchParams();\n input.forEach((value, key) => {\n if (typeof value === 'string') params.append(key, value);\n });\n if (hasPrev)\n params.set(PREV_STATE_FIELD, JSON.stringify(prevState ?? null));\n response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/x-www-form-urlencoded',\n Accept: 'application/json',\n 'X-Ossido-Action': '1',\n },\n body: params.toString(),\n });\n }\n } else {\n const payload: Record<string, unknown> = { input };\n if (hasPrev) payload[PREV_STATE_FIELD] = prevState ?? null;\n response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Accept: 'application/json',\n 'X-Ossido-Action': '1',\n },\n body: JSON.stringify(payload),\n });\n }\n\n return unwrap<Output>(await parse(response));\n}\n\n/** Build a stateless action bound to `url`. Used by the generated client. */\nexport function createAction<Input, Output>(\n url: string,\n): ActionFn<Input, Output> {\n const fn = (arg: Input | FormData): Promise<Output> =>\n callAction<Output>(url, arg, false, undefined);\n return Object.assign(fn, { url }) as ActionFn<Input, Output>;\n}\n\n/** Build a stateful action (`useActionState`) bound to `url`. */\nexport function createStatefulAction<Input, Output, Prev>(\n url: string,\n): StatefulActionFn<Input, Output, Prev> {\n const fn = (a: Input | Prev | null, b?: FormData): Promise<Output> => {\n // React's useActionState contract: (prevState, formData).\n if (typeof FormData !== 'undefined' && b instanceof FormData) {\n return callAction<Output>(url, b, true, a);\n }\n // Imperative typed call: (input).\n return callAction<Output>(url, a as Input, false, undefined);\n };\n return Object.assign(fn, { url }) as StatefulActionFn<Input, Output, Prev>;\n}\n\nexport interface FormProps<Input, Output> extends Omit<\n FormHTMLAttributes<HTMLFormElement>,\n 'action' | 'method'\n> {\n /** A generated action (e.g. `createUser`). */\n action: ActionFn<Input, Output>;\n children?: ReactNode;\n}\n\n/**\n * A `<form>` wired to a server action with progressive enhancement. When\n * hydrated, submission is intercepted and the action is called with `FormData`.\n * With JS disabled, the rendered `action={fn.url}` makes it a native POST, and\n * the server responds with a 303 Post/Redirect/Get.\n */\nexport function Form<Input, Output>({\n action,\n children,\n onSubmit,\n ...rest\n}: FormProps<Input, Output>): ReactNode {\n const handleSubmit = (event: FormEvent<HTMLFormElement>): void => {\n onSubmit?.(event);\n if (event.defaultPrevented) return;\n event.preventDefault();\n void action(new FormData(event.currentTarget));\n };\n\n return createElement(\n 'form',\n { method: 'post', action: action.url, onSubmit: handleSubmit, ...rest },\n children,\n );\n}\n\n// Re-exported so consumers get everything from one entry. `isPending` and\n// `useFormStatus().pending` come for free from React once an action is wired.\nexport { useActionState } from 'react';\nexport { useFormStatus } from 'react-dom';\n"],"mappings":"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"}
|
|
@@ -17,8 +17,11 @@ var MessagePortPolyfill = class {
|
|
|
17
17
|
}
|
|
18
18
|
postMessage(message) {
|
|
19
19
|
if (this.isClosed || !this.otherPort) return;
|
|
20
|
+
const other = this.otherPort;
|
|
20
21
|
const event = new MessageEvent("message", { data: message });
|
|
21
|
-
|
|
22
|
+
const schedule = globalThis.setTimeout;
|
|
23
|
+
if (typeof schedule === "function") schedule(() => other.dispatchEvent(event), 0);
|
|
24
|
+
else other.dispatchEvent(event);
|
|
22
25
|
}
|
|
23
26
|
addEventListener(type, listener) {
|
|
24
27
|
if (this.isClosed || type !== "message") return;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MessageChannel.js","names":[],"sources":["../../../../src/ssr/polyfills/MessageChannel.ts"],"sourcesContent":["/* Modified from https://github.com/rocwind/message-port-polyfill/blob/master/src/index.ts\n * MIT License\n *\n * Copyright (c) 2019 Roc\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nexport class MessagePortPolyfill implements MessagePort {\n onmessage: ((this: MessagePort, ev: MessageEvent) => unknown) | null = null;\n /** @warning this is declared to satisfy {@link MessagePort} interface requirements but is never called */\n onmessageerror: ((this: MessagePort, ev: MessageEvent) => unknown) | null =\n null;\n\n otherPort: MessagePortPolyfill | null = null;\n\n private onmessageListeners: Array<(ev: MessageEvent) => void> = [];\n private isClosed = false;\n\n dispatchEvent(event: MessageEvent): boolean {\n if (this.isClosed) return false;\n if (this.onmessage) {\n this.onmessage(event);\n }\n this.onmessageListeners.forEach((listener) => {\n listener(event);\n });\n return true;\n }\n\n postMessage(message: unknown): void {\n if (this.isClosed || !this.otherPort) return;\n\n const event = new MessageEvent('message', { data: message });\n
|
|
1
|
+
{"version":3,"file":"MessageChannel.js","names":[],"sources":["../../../../src/ssr/polyfills/MessageChannel.ts"],"sourcesContent":["/* Modified from https://github.com/rocwind/message-port-polyfill/blob/master/src/index.ts\n * MIT License\n *\n * Copyright (c) 2019 Roc\n *\n * Permission is hereby granted, free of charge, to any person obtaining a copy\n * of this software and associated documentation files (the \"Software\"), to deal\n * in the Software without restriction, including without limitation the rights\n * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n * copies of the Software, and to permit persons to whom the Software is\n * furnished to do so, subject to the following conditions:\n *\n * The above copyright notice and this permission notice shall be included in all\n * copies or substantial portions of the Software.\n *\n * THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n * SOFTWARE.\n */\n\nexport class MessagePortPolyfill implements MessagePort {\n onmessage: ((this: MessagePort, ev: MessageEvent) => unknown) | null = null;\n /** @warning this is declared to satisfy {@link MessagePort} interface requirements but is never called */\n onmessageerror: ((this: MessagePort, ev: MessageEvent) => unknown) | null =\n null;\n\n otherPort: MessagePortPolyfill | null = null;\n\n private onmessageListeners: Array<(ev: MessageEvent) => void> = [];\n private isClosed = false;\n\n dispatchEvent(event: MessageEvent): boolean {\n if (this.isClosed) return false;\n if (this.onmessage) {\n this.onmessage(event);\n }\n this.onmessageListeners.forEach((listener) => {\n listener(event);\n });\n return true;\n }\n\n postMessage(message: unknown): void {\n if (this.isClosed || !this.otherPort) return;\n\n const other = this.otherPort;\n const event = new MessageEvent('message', { data: message });\n\n // Deliver on the next macrotask, matching real `MessageChannel` semantics.\n // React 19's Fizz server renderer schedules resumed/flush work via\n // `port.postMessage` and relies on it being asynchronous — a synchronous\n // dispatch reorders shell emission vs. Suspense-boundary completion and\n // prevents the render from settling. In the ossido SSR isolate `setTimeout`\n // is a native timer whose queue the Rust SSR pump loop drains.\n const schedule = (\n globalThis as {\n setTimeout?: (callback: () => void, delay?: number) => number;\n }\n ).setTimeout;\n\n if (typeof schedule === 'function') {\n schedule(() => other.dispatchEvent(event), 0);\n } else {\n // No timer host (should not happen in ossido SSR): fall back to the prior\n // synchronous delivery so the polyfill still functions.\n other.dispatchEvent(event);\n }\n }\n\n addEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n ): void {\n if (this.isClosed || type !== 'message') return;\n\n if (\n typeof listener === 'function' &&\n !this.onmessageListeners.includes(listener)\n ) {\n this.onmessageListeners.push(listener);\n }\n }\n\n removeEventListener(\n type: string,\n listener: EventListenerOrEventListenerObject,\n ): void {\n if (this.isClosed || type !== 'message') return;\n\n if (typeof listener === 'function') {\n const index = this.onmessageListeners.indexOf(listener);\n if (index !== -1) {\n this.onmessageListeners.splice(index, 1);\n }\n }\n }\n\n start(): void {\n // do nothing at this moment\n }\n\n close(): void {\n this.isClosed = true;\n }\n}\n\nexport class MessageChannelPolyfill implements MessageChannel {\n readonly port1: MessagePortPolyfill;\n readonly port2: MessagePortPolyfill;\n\n constructor() {\n this.port1 = new MessagePortPolyfill();\n this.port2 = new MessagePortPolyfill();\n\n this.port1.otherPort = this.port2;\n this.port2.otherPort = this.port1;\n }\n}\n"],"mappings":";AAwBA,IAAa,sBAAb,MAAwD;;mBACiB;wBAGrE;mBAEsC;4BAEwB,CAAC;kBAC9C;;CAEnB,cAAc,OAA8B;EAC1C,IAAI,KAAK,UAAU,OAAO;EAC1B,IAAI,KAAK,WACP,KAAK,UAAU,KAAK;EAEtB,KAAK,mBAAmB,SAAS,aAAa;GAC5C,SAAS,KAAK;EAChB,CAAC;EACD,OAAO;CACT;CAEA,YAAY,SAAwB;EAClC,IAAI,KAAK,YAAY,CAAC,KAAK,WAAW;EAEtC,MAAM,QAAQ,KAAK;EACnB,MAAM,QAAQ,IAAI,aAAa,WAAW,EAAE,MAAM,QAAQ,CAAC;EAQ3D,MAAM,WACJ,WAGA;EAEF,IAAI,OAAO,aAAa,YACtB,eAAe,MAAM,cAAc,KAAK,GAAG,CAAC;OAI5C,MAAM,cAAc,KAAK;CAE7B;CAEA,iBACE,MACA,UACM;EACN,IAAI,KAAK,YAAY,SAAS,WAAW;EAEzC,IACE,OAAO,aAAa,cACpB,CAAC,KAAK,mBAAmB,SAAS,QAAQ,GAE1C,KAAK,mBAAmB,KAAK,QAAQ;CAEzC;CAEA,oBACE,MACA,UACM;EACN,IAAI,KAAK,YAAY,SAAS,WAAW;EAEzC,IAAI,OAAO,aAAa,YAAY;GAClC,MAAM,QAAQ,KAAK,mBAAmB,QAAQ,QAAQ;GACtD,IAAI,UAAU,IACZ,KAAK,mBAAmB,OAAO,OAAO,CAAC;EAE3C;CACF;CAEA,QAAc,CAEd;CAEA,QAAc;EACZ,KAAK,WAAW;CAClB;AACF;AAEA,IAAa,yBAAb,MAA8D;CAI5D,cAAc;EACZ,KAAK,QAAQ,IAAI,oBAAoB;EACrC,KAAK,QAAQ,IAAI,oBAAoB;EAErC,KAAK,MAAM,YAAY,KAAK;EAC5B,KAAK,MAAM,YAAY,KAAK;CAC9B;AACF"}
|
package/dist/esm/ssr/server.js
CHANGED
|
@@ -7,6 +7,7 @@ import { jsx } from "react/jsx-runtime";
|
|
|
7
7
|
import "fast-text-encoding";
|
|
8
8
|
import "url-search-params-polyfill";
|
|
9
9
|
import { renderToReadableStream } from "react-dom/server";
|
|
10
|
+
import { prerender } from "react-dom/static";
|
|
10
11
|
|
|
11
12
|
//#region src/ssr/server.tsx
|
|
12
13
|
(function(scope = {}) {
|
|
@@ -30,9 +31,8 @@ function serverSideRendering(routeTree) {
|
|
|
30
31
|
* anywhere the caller needs the complete HTML up front.
|
|
31
32
|
*/
|
|
32
33
|
async renderFn(payload) {
|
|
33
|
-
const
|
|
34
|
-
await
|
|
35
|
-
return await streamToString(stream);
|
|
34
|
+
const { prelude } = await prerender(await element(payload));
|
|
35
|
+
return await streamToString(prelude);
|
|
36
36
|
},
|
|
37
37
|
/**
|
|
38
38
|
* Streaming render: flush each HTML chunk to Rust via
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.js","names":[],"sources":["../../../src/ssr/server.tsx"],"sourcesContent":["// #region POLYFILLS\n/**\n * Ossido internally uses a V8 JS engine that implements very few\n * browser/node/deno APIs in order to make it super fast and\n * share it within a multi thread runtime.\n *\n * While this is the reason of its speed, server side rendering\n * requires some JS APIs that need to be polyfilled.\n *\n * We basically have three ways to polyfill APIs:\n * 1. Create them with rust and expose them directly through the V8 engine to\n * the JS source.\n * 2. Polyfill them at the beginning of the JS source\n * (what we are doing here)\n * 3. Inject them via rollup-inject plugin, when needed\n *\n * Q: Why not all the libraries can be just injected with rollup-inject?\n * A: Leaving to rollup the duty of linking them can cause to declare them after their usage.\n * The following APIs are JS classes, and are not hoisted, hence this might\n * cause ReferenceError(s).\n *\n * The best solution is to create these polyfills within the rust environment\n * and share the classes in the JS scope by passing them through the V8 engine\n * (best for speed and code quality).\n *\n * This function might be a good entry point for adding such polyfills\n * (see `Ssr::add_global_fn` in crates/ossido_ssr/src/ssr.rs)\n */\n// Must run before the polyfills below: aliases `global` to `globalThis` so\n// their UMD init IIFEs find a valid scope in the `window`/`global`-less\n// ossido_ssr V8 runtime instead of dereferencing `undefined`.\nimport './polyfills/globalScope';\nimport 'fast-text-encoding';\nimport 'url-search-params-polyfill';\n\n/* eslint-disable import/order, import/newline-after-import */\nimport { MessageChannelPolyfill } from './polyfills/MessageChannel';\n(function (\n scope: Partial<Pick<typeof globalThis, 'MessageChannel'>> = {},\n): void {\n scope['MessageChannel'] = scope['MessageChannel'] ?? MessageChannelPolyfill;\n})(this);\n/* eslint-enable import/order, import/newline-after-import */\n// #endregion POLYFILLS\n\nimport type { ReadableStream } from 'node:stream/web';\n\nimport type { JSX } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport { createRouter, preloadRouteChain } from '@ossido-labs/ossido-router';\nimport type { createRoute } from '@ossido-labs/ossido-router';\n\nimport { OssidoEntryPoint } from '../shared/OssidoEntryPoint';\nimport type { ServerPayload } from '../types';\n\nimport { streamToString, createUtf8Streamer } from './utils';\n\ntype RouteTree = ReturnType<typeof createRoute>;\n\n/**\n * Chunk sink injected by the Rust `ossido_ssr` runtime for streaming renders (see\n * `Ssr::render_to_stream`). Each call hands one HTML fragment to Rust, which\n * forwards it to the client immediately instead of buffering the whole page.\n */\ndeclare const __ssr_write: ((chunk: string) => void) | undefined;\n\ninterface ServerSideRenderer {\n /** Buffered render — resolves the whole page to a single HTML string. */\n renderFn: (payload: string | undefined) => Promise<string>;\n /** Streaming render — flushes each HTML chunk via `__ssr_write`. */\n renderStream: (payload: string | undefined) => Promise<void>;\n}\n\nexport function serverSideRendering(routeTree: RouteTree): ServerSideRenderer {\n // Build the React element for a request. Shared by the buffered and streaming\n // entry points so they render exactly the same tree.\n //\n // Async because the matched route's code (page + wrapping layouts) is\n // preloaded first: routes are `React.lazy`-wrapped, and rendering one that\n // hasn't loaded suspends its boundary, which pushes the page content into an\n // out-of-order late chunk (empty shell paints first — a visible flash on\n // every cold load). In the bundled SSR output the dynamic imports are\n // inlined, so this resolves in a microtask.\n const element = async (payload: string | undefined): Promise<JSX.Element> => {\n const serverPayload = (payload ? JSON.parse(payload) : {}) as ServerPayload;\n const router = createRouter({ routeTree });\n await preloadRouteChain(router, serverPayload.location?.pathname);\n return (\n // `rawServerPayload` is the exact JSON Rust already produced; passing it\n // lets `OssidoScripts` embed it verbatim instead of re-stringifying the\n // parsed payload inside V8.\n <OssidoEntryPoint\n router={router}\n serverPayload={serverPayload}\n rawServerPayload={payload}\n />\n );\n };\n\n return {\n /**\n * Buffered render: resolve the whole page to a single string. Used for\n * error pages, static export (SSG), `catch_all`, and the dev fallback —\n * anywhere the caller needs the complete HTML up front.\n */\n async renderFn(payload: string | undefined): Promise<string> {\n const
|
|
1
|
+
{"version":3,"file":"server.js","names":[],"sources":["../../../src/ssr/server.tsx"],"sourcesContent":["// #region POLYFILLS\n/**\n * Ossido internally uses a V8 JS engine that implements very few\n * browser/node/deno APIs in order to make it super fast and\n * share it within a multi thread runtime.\n *\n * While this is the reason of its speed, server side rendering\n * requires some JS APIs that need to be polyfilled.\n *\n * We basically have three ways to polyfill APIs:\n * 1. Create them with rust and expose them directly through the V8 engine to\n * the JS source.\n * 2. Polyfill them at the beginning of the JS source\n * (what we are doing here)\n * 3. Inject them via rollup-inject plugin, when needed\n *\n * Q: Why not all the libraries can be just injected with rollup-inject?\n * A: Leaving to rollup the duty of linking them can cause to declare them after their usage.\n * The following APIs are JS classes, and are not hoisted, hence this might\n * cause ReferenceError(s).\n *\n * The best solution is to create these polyfills within the rust environment\n * and share the classes in the JS scope by passing them through the V8 engine\n * (best for speed and code quality).\n *\n * This function might be a good entry point for adding such polyfills\n * (see `Ssr::add_global_fn` in crates/ossido_ssr/src/ssr.rs)\n */\n// Must run before the polyfills below: aliases `global` to `globalThis` so\n// their UMD init IIFEs find a valid scope in the `window`/`global`-less\n// ossido_ssr V8 runtime instead of dereferencing `undefined`.\nimport './polyfills/globalScope';\nimport 'fast-text-encoding';\nimport 'url-search-params-polyfill';\n\n/* eslint-disable import/order, import/newline-after-import */\nimport { MessageChannelPolyfill } from './polyfills/MessageChannel';\n(function (\n scope: Partial<Pick<typeof globalThis, 'MessageChannel'>> = {},\n): void {\n scope['MessageChannel'] = scope['MessageChannel'] ?? MessageChannelPolyfill;\n})(this);\n/* eslint-enable import/order, import/newline-after-import */\n// #endregion POLYFILLS\n\nimport type { ReadableStream } from 'node:stream/web';\n\nimport type { JSX } from 'react';\nimport { renderToReadableStream } from 'react-dom/server';\nimport { prerender } from 'react-dom/static';\nimport { createRouter, preloadRouteChain } from '@ossido-labs/ossido-router';\nimport type { createRoute } from '@ossido-labs/ossido-router';\n\nimport { OssidoEntryPoint } from '../shared/OssidoEntryPoint';\nimport type { ServerPayload } from '../types';\n\nimport { streamToString, createUtf8Streamer } from './utils';\n\ntype RouteTree = ReturnType<typeof createRoute>;\n\n/**\n * Chunk sink injected by the Rust `ossido_ssr` runtime for streaming renders (see\n * `Ssr::render_to_stream`). Each call hands one HTML fragment to Rust, which\n * forwards it to the client immediately instead of buffering the whole page.\n */\ndeclare const __ssr_write: ((chunk: string) => void) | undefined;\n\ninterface ServerSideRenderer {\n /** Buffered render — resolves the whole page to a single HTML string. */\n renderFn: (payload: string | undefined) => Promise<string>;\n /** Streaming render — flushes each HTML chunk via `__ssr_write`. */\n renderStream: (payload: string | undefined) => Promise<void>;\n}\n\nexport function serverSideRendering(routeTree: RouteTree): ServerSideRenderer {\n // Build the React element for a request. Shared by the buffered and streaming\n // entry points so they render exactly the same tree.\n //\n // Async because the matched route's code (page + wrapping layouts) is\n // preloaded first: routes are `React.lazy`-wrapped, and rendering one that\n // hasn't loaded suspends its boundary, which pushes the page content into an\n // out-of-order late chunk (empty shell paints first — a visible flash on\n // every cold load). In the bundled SSR output the dynamic imports are\n // inlined, so this resolves in a microtask.\n const element = async (payload: string | undefined): Promise<JSX.Element> => {\n const serverPayload = (payload ? JSON.parse(payload) : {}) as ServerPayload;\n const router = createRouter({ routeTree });\n await preloadRouteChain(router, serverPayload.location?.pathname);\n return (\n // `rawServerPayload` is the exact JSON Rust already produced; passing it\n // lets `OssidoScripts` embed it verbatim instead of re-stringifying the\n // parsed payload inside V8.\n <OssidoEntryPoint\n router={router}\n serverPayload={serverPayload}\n rawServerPayload={payload}\n />\n );\n };\n\n return {\n /**\n * Buffered render: resolve the whole page to a single string. Used for\n * error pages, static export (SSG), `catch_all`, and the dev fallback —\n * anywhere the caller needs the complete HTML up front.\n */\n async renderFn(payload: string | undefined): Promise<string> {\n // `prerender` (react-dom/static) is the SSG-correct API: its promise\n // resolves from React's `onAllReady`, so `prelude` is the fully-settled\n // tree (every Suspense boundary resolved) — no `allReady` dance and no\n // risk of capturing a fallback. Requires the SSR event loop (macrotasks)\n // to settle, same as `renderToReadableStream`.\n const { prelude } = await prerender(await element(payload));\n return await streamToString(\n prelude as unknown as ReadableStream<Uint8Array>,\n );\n },\n\n /**\n * Streaming render: flush each HTML chunk to Rust via\n * `__ssr_write` as React produces it, so the shell reaches the\n * client without waiting for the full page. `renderToReadableStream`\n * rejects on a *shell* error before any chunk is written, so Rust can still\n * send a 500 instead of a partial 200 in that case.\n */\n async renderStream(payload: string | undefined): Promise<void> {\n const write = __ssr_write;\n if (typeof write !== 'function') {\n throw new Error('__ssr_write is not registered by the runtime');\n }\n\n const stream = await renderToReadableStream(await element(payload));\n\n const streamer = createUtf8Streamer();\n for await (const chunk of stream as unknown as ReadableStream<Uint8Array>) {\n const text = streamer.push(chunk);\n if (text) write(text);\n }\n const tail = streamer.flush();\n if (tail) write(tail);\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;CAqCC,SACC,QAA4D,CAAC,GACvD;CACN,MAAM,oBAAoB,MAAM,qBAAqB;AACvD,EAAC,OAAM;AAiCP,SAAgB,oBAAoB,WAA0C;CAU5E,MAAM,UAAU,OAAO,YAAsD;EAC3E,MAAM,gBAAiB,UAAU,KAAK,MAAM,OAAO,IAAI,CAAC;EACxD,MAAM,SAAS,aAAa,EAAE,UAAU,CAAC;EACzC,MAAM,kBAAkB,QAAQ,cAAc,UAAU,QAAQ;EAChE,OAIE,oBAAC,kBAAD;GACU;GACO;GACf,kBAAkB;EACnB;CAEL;CAEA,OAAO;;;;;;EAML,MAAM,SAAS,SAA8C;GAM3D,MAAM,EAAE,YAAY,MAAM,UAAU,MAAM,QAAQ,OAAO,CAAC;GAC1D,OAAO,MAAM,eACX,OACF;EACF;;;;;;;;EASA,MAAM,aAAa,SAA4C;GAC7D,MAAM,QAAQ;GACd,IAAI,OAAO,UAAU,YACnB,MAAM,IAAI,MAAM,8CAA8C;GAGhE,MAAM,SAAS,MAAM,uBAAuB,MAAM,QAAQ,OAAO,CAAC;GAElE,MAAM,WAAW,mBAAmB;GACpC,WAAW,MAAM,SAAS,QAAiD;IACzE,MAAM,OAAO,SAAS,KAAK,KAAK;IAChC,IAAI,MAAM,MAAM,IAAI;GACtB;GACA,MAAM,OAAO,SAAS,MAAM;GAC5B,IAAI,MAAM,MAAM,IAAI;EACtB;CACF;AACF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ossido-labs/ossido",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -27,6 +27,10 @@
|
|
|
27
27
|
"main": "dist/esm/index.js",
|
|
28
28
|
"module": "dist/esm/index.js",
|
|
29
29
|
"exports": {
|
|
30
|
+
"./actions": {
|
|
31
|
+
"types": "./dist/esm/actions/index.d.ts",
|
|
32
|
+
"default": "./dist/esm/actions/index.js"
|
|
33
|
+
},
|
|
30
34
|
"./build": {
|
|
31
35
|
"types": "./dist/esm/build/index.d.ts",
|
|
32
36
|
"default": "./dist/esm/build/index.js"
|
|
@@ -77,9 +81,9 @@
|
|
|
77
81
|
"@rollup/plugin-inject": "^5.0.5",
|
|
78
82
|
"@vitejs/plugin-react-swc": "^4.3.2",
|
|
79
83
|
"fast-text-encoding": "^1.0.6",
|
|
80
|
-
"@ossido-labs/ossido-react-vite-plugin": "0.1.
|
|
81
|
-
"@ossido-labs/ossido-router": "0.1.
|
|
82
|
-
"@ossido-labs/ossido-ui": "0.1.
|
|
84
|
+
"@ossido-labs/ossido-react-vite-plugin": "0.1.3",
|
|
85
|
+
"@ossido-labs/ossido-router": "0.1.3",
|
|
86
|
+
"@ossido-labs/ossido-ui": "0.1.3",
|
|
83
87
|
"url-search-params-polyfill": "^8.2.5",
|
|
84
88
|
"web-streams-polyfill": "^4.0.0"
|
|
85
89
|
},
|