@ossido-labs/ossido 0.1.1 → 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/README.md +71 -16
- 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/build/index.js.map +1 -1
- 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
package/README.md
CHANGED
|
@@ -1,28 +1,83 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://ossido.dev">
|
|
3
|
+
<img src="https://raw.githubusercontent.com/ossido-labs/ossido/main/assets/header.png" alt="Ossido" width="100%">
|
|
4
|
+
</a>
|
|
2
5
|
|
|
3
|
-
The
|
|
4
|
-
full-stack framework.
|
|
6
|
+
<p><strong>The full-stack React framework powered by a Rust backend — built for usability and performance.</strong></p>
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
<p>
|
|
9
|
+
<a href="https://crates.io/crates/ossido_cli"><img src="https://img.shields.io/crates/v/ossido_cli?logo=rust&label=crates.io&color=E43717" alt="crates.io version"></a>
|
|
10
|
+
<a href="https://www.npmjs.com/package/@ossido-labs/ossido"><img src="https://img.shields.io/npm/v/@ossido-labs/ossido?logo=npm&label=npm&color=CB3837" alt="npm version"></a>
|
|
11
|
+
<a href="https://github.com/ossido-labs/ossido/actions/workflows/rust-ci.yml"><img src="https://github.com/ossido-labs/ossido/actions/workflows/rust-ci.yml/badge.svg" alt="Rust CI"></a>
|
|
12
|
+
<a href="https://github.com/ossido-labs/ossido/actions/workflows/typescript-ci.yml"><img src="https://github.com/ossido-labs/ossido/actions/workflows/typescript-ci.yml/badge.svg" alt="TypeScript CI"></a>
|
|
13
|
+
<a href="./LICENSE.md"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT"></a>
|
|
14
|
+
<a href="https://discord.com/invite/3ddKV4e83M"><img src="https://img.shields.io/discord/1536194582889627658?style=flat" alt="Discord"></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p>
|
|
18
|
+
<a href="https://ossido.dev">Documentation</a> ·
|
|
19
|
+
<a href="https://discord.com/invite/3ddKV4e83M">Discord</a> ·
|
|
20
|
+
<a href="https://ossido.dev/documentation/contributing">Contributing</a>
|
|
21
|
+
</p>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
Ossido is a full-stack web framework for building React applications with a Rust backend. If you have
|
|
27
|
+
experience with [Next.js](https://nextjs.org/), you will feel right at home.
|
|
28
|
+
|
|
29
|
+
> ### **🚨 This project is still in early development. Please report any issues you find. 🚨**
|
|
30
|
+
|
|
31
|
+
> **Ossido is the successor to [Tuono](https://github.com/tuono-labs/tuono).** It began as a fork of
|
|
32
|
+
> the (now unmaintained) Tuono framework and carries its development forward.
|
|
33
|
+
>
|
|
34
|
+
> The name is a nod to its origin: _Tuono_ is Italian for "thunder", and _Ossido_ is Italian for
|
|
35
|
+
> "oxide" — a rust-themed wink to the Rust core that powers the backend.
|
|
36
|
+
|
|
37
|
+
## Features
|
|
38
|
+
|
|
39
|
+
- 🟦 **Native TypeScript** — first-class TypeScript support out of the box
|
|
40
|
+
- 🌐 **File-based routing** — Next.js-like routing conventions
|
|
41
|
+
- 🍭 **CSS/SCSS modules** — scoped styling with zero configuration
|
|
42
|
+
- 🧬 **Server-side rendering** — fast, SEO-friendly SSR
|
|
43
|
+
- 🔥 **Hot module reload** — instant feedback during development
|
|
44
|
+
- ⚡ **Rust backend** — a high-performance core built on Rust
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
Available on macOS, Linux and Windows.
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
cargo install ossido_cli
|
|
52
|
+
```
|
|
11
53
|
|
|
12
54
|
## Getting started
|
|
13
55
|
|
|
14
|
-
Create a new project
|
|
56
|
+
Create a new project:
|
|
15
57
|
|
|
16
58
|
```sh
|
|
17
59
|
ossido new [PROJECT_NAME]
|
|
18
60
|
```
|
|
19
61
|
|
|
20
|
-
|
|
62
|
+
> Pass the `--template` (or `-t`) flag to start from an existing
|
|
63
|
+
> [template](https://github.com/ossido-labs/ossido/tree/main/examples).
|
|
64
|
+
|
|
65
|
+
Install the dependencies with your favourite JavaScript package manager (e.g. `npm install`), then
|
|
66
|
+
start the development server:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
ossido dev
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Documentation
|
|
73
|
+
|
|
74
|
+
Full documentation is available at [ossido.dev](https://ossido.dev/).
|
|
75
|
+
|
|
76
|
+
## Contributing
|
|
77
|
+
|
|
78
|
+
Contributions, ideas and suggestions are welcome and encouraged. See the
|
|
79
|
+
[Contributing guide](https://ossido.dev/documentation/contributing) to get started.
|
|
21
80
|
|
|
22
|
-
|
|
23
|
-
- `ossido/config` — `OssidoConfig` type and config helpers
|
|
24
|
-
- `ossido/client` / `ossido/hydration` — client bootstrap + hydration
|
|
25
|
-
- `ossido/ssr` — server-side rendering entry
|
|
26
|
-
- `ossido/build` / `ossido/build-client` — build pipeline
|
|
81
|
+
## License
|
|
27
82
|
|
|
28
|
-
|
|
83
|
+
Licensed under the [MIT License](./LICENSE.md).
|
|
@@ -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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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/build/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport { resolve, join } from 'node:path';\nimport { readdirSync, readFileSync } from 'node:fs';\nimport { gzipSync } from 'node:zlib';\n\nimport type { InlineConfig, Plugin } from 'vite';\nimport { build, createServer, mergeConfig } from 'vite';\nimport react from '@vitejs/plugin-react-swc';\nimport inject from '@rollup/plugin-inject';\nimport { OssidoReactPlugin, routeGenerator } from '@ossido-labs/ossido-react-vite-plugin';\nimport { ErrorOverlayVitePlugin } from '@ossido-labs/ossido-ui/vite-plugin';\n\nimport type { OssidoConfig } from '../config';\n\nimport type { InternalOssidoConfig } from './types';\nimport { blockingAsync, listFilesRecursive } from './utils';\nimport { createJsonConfig, loadConfig } from './config';\nimport { ENV_PREFIX } from './constants';\nimport { createOssidoViteLogger } from './logger';\n\nconst require = createRequire(import.meta.url);\n\n/**\n * Absolute entry paths for the generated `.ossido` scaffold.\n *\n * The vite config sets `root: '.ossido'`. Vite's `build()` resolves a relative\n * `rollupOptions.input` against the CWD, but the dev server's dependency\n * scanner (rolldown, vite 8+) resolves it against `root` — so a path like\n * `./.ossido/client-main.tsx` double-nests to `.ossido/.ossido/...` and fails to\n * resolve. Absolute paths are unambiguous across both code paths.\n */\nconst CLIENT_MAIN_ENTRY = resolve('.ossido/client-main.tsx');\nconst SERVER_MAIN_ENTRY = resolve('.ossido/server-main.tsx');\n\n/**\n * `@rollup/plugin-inject` injects these imports into every module that\n * references the globals below — including third-party files deep in\n * `node_modules` (e.g. `react-dom/server`). Vite 8's bundler (rolldown)\n * resolves an injected bare specifier relative to the *importing* file, so\n * `'@ossido-labs/ossido/ssr'` fails to resolve from within `react-dom`. Resolving to\n * absolute paths up-front makes the injected imports resolvable from anywhere\n * (and is a no-op for rollup on Vite <= 7).\n */\nconst SSR_POLYFILLS_MODULE = require.resolve('@ossido-labs/ossido/ssr');\nconst WEB_STREAMS_POLYFILL_MODULE = require.resolve('web-streams-polyfill');\n\nconst VITE_SSR_PLUGINS: Array<Plugin> = [\n {\n enforce: 'post',\n // `@rollup/plugin-inject` is typed against an older rollup than the one\n // bundled with vite, so its `Plugin` type needs widening to vite's.\n ...(inject({\n ReadableStream: [WEB_STREAMS_POLYFILL_MODULE, 'ReadableStream'],\n\n /**\n * Added to support `react@19`\n * @see https://github.com/tuono-labs/tuono/issues/218\n */\n MessageChannel: [SSR_POLYFILLS_MODULE, 'MessageChannelPolyfill'],\n MessageEvent: [SSR_POLYFILLS_MODULE, 'MessageEventPolyfill'],\n Event: [SSR_POLYFILLS_MODULE, 'EventPolyfill'],\n }) as unknown as Plugin),\n },\n];\n\n/**\n * From a resolved {@link InternalOssidoConfig} return a `vite` \"mergeable\"\n * {@link InlineConfig} including all default ossido related options\n */\nfunction createBaseViteConfigFromOssidoConfig(\n ossidoConfig: InternalOssidoConfig,\n): InlineConfig {\n /**\n * @warning Keep in sync with {@link LazyLoadingPlugin} tests:\n * packages/lazy-fn-vite-plugin/tests/transpileSource.test.ts\n */\n const pluginFilesInclude = /\\.(jsx|js|mdx|md|tsx|ts)$/;\n\n const viteBaseConfig: InlineConfig = {\n root: '.ossido',\n // `'error'` (not `'silent'`) so build failures surface instead of\n // degrading to a silent client-side fallback with hydration mismatches.\n logLevel: 'error',\n // Tag vite's output `[FE]` so it matches the rest of the Ossido logs. The\n // level is passed explicitly so the logger honours it (see the logger docs)\n // — this keeps vite's info/benign-warning noise out at `'error'`.\n customLogger: createOssidoViteLogger('error'),\n publicDir: '../public',\n cacheDir: 'cache',\n envDir: '../',\n envPrefix: ENV_PREFIX,\n\n define: {\n // Baked into the client bundle for `ossido build --static` (the CLI sets\n // `OSSIDO_STATIC` for the react build). The router reads it to fetch the\n // pre-rendered `.json` data files instead of the extensionless\n // `/__ossido/data{path}` route, which has no server in a static export.\n __OSSIDO_STATIC__: JSON.stringify(process.env.OSSIDO_STATIC === 'true'),\n // Baked into both bundles so `<CriticalCss>` skips rendering its dev\n // stylesheet links when `dev.criticalCss: false` — the vite endpoint is\n // disabled in that case, so the links would otherwise be dead requests\n // that block rendering.\n __OSSIDO_CRITICAL_CSS__: JSON.stringify(\n ossidoConfig.dev?.criticalCss ?? true,\n ),\n // Baked into both bundles so the router wraps client navigations in\n // `document.startViewTransition` only when the app opted in via\n // `viewTransitions: true`. Off by default.\n __OSSIDO_VIEW_TRANSITIONS__: JSON.stringify(\n ossidoConfig.viewTransitions ?? false,\n ),\n },\n\n resolve: {\n alias: ossidoConfig.vite?.alias ?? {},\n },\n\n css: ossidoConfig.vite?.css,\n\n optimizeDeps: ossidoConfig.vite?.optimizeDeps,\n\n plugins: [\n ...(ossidoConfig.vite?.plugins ?? []),\n\n /**\n * even if `include` is not a valid option for this\n * plugin, we have to use it.\n * If not specified, when running `ossido dev`, the mdx\n * won't be compiled include any style in the page and it might\n * seem broken.\n */\n // @ts-expect-error see above comment\n react({ include: pluginFilesInclude }),\n\n OssidoReactPlugin({ criticalCss: ossidoConfig.dev?.criticalCss ?? true }),\n ],\n };\n\n // seems redundant but it's useful to log the value when debugging, until we have a logging infrastructure.\n return viteBaseConfig;\n}\n\nconst developmentSSRBundle = (): void => {\n blockingAsync(async () => {\n const config = await loadConfig();\n await build(\n mergeConfig<InlineConfig, InlineConfig>(\n createBaseViteConfigFromOssidoConfig(config),\n {\n // A dedicated cache dir, separate from the client dev server's\n // (`.ossido/cache`). Both run concurrently in dev, and this SSR bundle\n // is rebuilt on every `.tsx`/`.mdx`/CSS-module edit — sharing the cache\n // makes each rebuild invalidate the dev server's optimized-deps cache,\n // which triggers a spurious full-page reload (and data refetch) right\n // after the in-place Fast Refresh has already applied.\n cacheDir: 'cache-ssr',\n plugins: VITE_SSR_PLUGINS,\n build: {\n ssr: true,\n minify: false,\n outDir: 'server',\n emptyOutDir: true,\n rollupOptions: {\n input: SERVER_MAIN_ENTRY,\n output: {\n entryFileNames: 'dev-server.js',\n // ESM (not IIFE) so the SSR entry can use top-level `await`; the\n // Rust runtime compiles it as a V8 module (`Ssr::from_module`).\n // The build stays a single `dev-server.js` — the only file the\n // runtime reads — because the SSR build disables code splitting.\n format: 'es',\n },\n },\n },\n ssr: {\n target: 'webworker',\n noExternal: true,\n },\n },\n ),\n );\n });\n};\n\nconst developmentCSRWatch = (): void => {\n blockingAsync(async () => {\n const config = await loadConfig();\n\n const server = await createServer(\n mergeConfig<InlineConfig, InlineConfig>(\n createBaseViteConfigFromOssidoConfig(config),\n {\n // Entry point for the development vite proxy\n base: '/vite-server/',\n plugins: [ErrorOverlayVitePlugin],\n\n server: {\n host: config.server.host,\n port: config.server.port + 1,\n strictPort: true,\n },\n build: {\n manifest: true,\n emptyOutDir: true,\n rollupOptions: {\n input: CLIENT_MAIN_ENTRY,\n },\n },\n },\n ),\n );\n await server.listen();\n });\n};\n\nconst KB = 1024;\n\nconst formatKb = (bytes: number): string => `${(bytes / KB).toFixed(1)} kB`;\n\n/**\n * Print a compact per-asset size summary (raw + gzip) for the prod build, so\n * size regressions are visible on every `ossido build` without extra tooling.\n * Reads the emitted files directly — works regardless of vite's log level.\n */\nconst printBundleSummary = (): void => {\n interface AssetRow {\n label: string;\n size: number;\n gzip: number;\n }\n\n const collectDir = (dir: string, prefix: string): Array<AssetRow> => {\n try {\n return readdirSync(dir, { recursive: true, encoding: 'utf-8' })\n .filter((file) => /\\.(js|css)$/.test(file))\n .map((file) => {\n const content = readFileSync(join(dir, file));\n return {\n label: `${prefix}/${file}`,\n size: content.length,\n gzip: gzipSync(content).length,\n };\n });\n } catch {\n return [];\n }\n };\n\n const assets = [\n ...collectDir(resolve('out/client'), 'client'),\n ...collectDir(resolve('out/server'), 'server'),\n ].sort((a, b) => b.size - a.size);\n\n if (!assets.length) return;\n\n const labelWidth = Math.max(...assets.map((asset) => asset.label.length));\n console.log('');\n for (const asset of assets) {\n console.log(\n ` ${asset.label.padEnd(labelWidth)} ${formatKb(asset.size).padStart(10)} │ gzip: ${formatKb(asset.gzip).padStart(9)}`,\n );\n }\n const total = assets.reduce((sum, asset) => sum + asset.size, 0);\n const totalGzip = assets.reduce((sum, asset) => sum + asset.gzip, 0);\n console.log(\n ` ${'total'.padEnd(labelWidth)} ${formatKb(total).padStart(10)} │ gzip: ${formatKb(totalGzip).padStart(9)}`,\n );\n console.log('');\n};\n\nconst buildProd = (): void => {\n blockingAsync(async () => {\n const config = await loadConfig();\n\n // Generate the route tree once up front. Both builds' plugin instances\n // re-run the generator on `configResolved`, but it skips the write when\n // the content is unchanged — pre-generating removes the concurrent-write\n // hazard so the two builds (independent outputs) can run in parallel.\n await routeGenerator();\n\n await Promise.all([\n build(\n mergeConfig<InlineConfig, InlineConfig>(\n createBaseViteConfigFromOssidoConfig(config),\n {\n build: {\n manifest: true,\n emptyOutDir: true,\n outDir: '../out/client',\n rollupOptions: {\n input: CLIENT_MAIN_ENTRY,\n },\n },\n },\n ),\n ),\n build(\n mergeConfig<InlineConfig, InlineConfig>(\n createBaseViteConfigFromOssidoConfig(config),\n {\n plugins: VITE_SSR_PLUGINS,\n build: {\n ssr: true,\n minify: true,\n outDir: '../out/server',\n emptyOutDir: true,\n rollupOptions: {\n input: SERVER_MAIN_ENTRY,\n output: {\n entryFileNames: 'prod-server.js',\n // ESM (not IIFE) so the SSR entry can use top-level `await`;\n // the Rust runtime compiles it as a V8 module\n // (`Ssr::from_module`). The build stays a single\n // `prod-server.js` — the only file the runtime reads —\n // because the SSR build disables code splitting.\n format: 'es',\n },\n },\n },\n ssr: {\n target: 'webworker',\n noExternal: true,\n },\n },\n ),\n ),\n ]);\n\n printBundleSummary();\n });\n};\n\nconst buildConfig = (): void => {\n blockingAsync(async (): Promise<void> => {\n await build({\n root: '.ossido',\n // `'error'` (not `'silent'`) so config build failures are visible.\n logLevel: 'error',\n cacheDir: 'cache',\n envDir: '../',\n build: {\n ssr: true,\n outDir: 'config',\n emptyOutDir: true,\n rollupOptions: {\n input: './ossido.config.ts',\n output: {\n entryFileNames: 'config.mjs',\n },\n },\n },\n });\n\n const config = await loadConfig();\n await createJsonConfig(config);\n });\n};\n\n/**\n * Run a user-defined build lifecycle hook (`prebuild` / `postbuild`) from\n * `ossido.config.ts`. Invoked by the Rust CLI (`ossido-run-build-hook <name>`)\n * at the matching point of `ossido build`. No-ops when the hook is undefined.\n *\n * The output/public dirs and mode mirror what the build produces: static builds\n * deploy `out/static`, server builds `out`. `postbuild` gets the emitted-file\n * manifest; `prebuild` runs before anything is emitted, so its manifest is empty.\n */\nconst runBuildHook = (name: 'prebuild' | 'postbuild'): void =>\n blockingAsync(async () => {\n const config = await loadConfig();\n const hook = config.build?.[name];\n if (typeof hook !== 'function') return;\n\n const mode = process.env.OSSIDO_STATIC === 'true' ? 'static' : 'server';\n const outputDirectory = resolve(mode === 'static' ? 'out/static' : 'out');\n const publicDirectory = resolve('public');\n const manifest =\n name === 'postbuild' ? listFilesRecursive(outputDirectory) : [];\n\n // `loadConfig` returns the resolved `InternalOssidoConfig` (a structural\n // superset of the public `OssidoConfig` the hook context exposes).\n await hook({\n mode,\n outputDirectory,\n publicDirectory,\n manifest,\n config: config as OssidoConfig,\n });\n });\n\nexport {\n buildProd,\n buildConfig,\n developmentCSRWatch,\n developmentSSRBundle,\n runBuildHook,\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/build/index.ts"],"sourcesContent":["import { createRequire } from 'node:module';\nimport { resolve, join } from 'node:path';\nimport { readdirSync, readFileSync } from 'node:fs';\nimport { gzipSync } from 'node:zlib';\n\nimport type { InlineConfig, Plugin } from 'vite';\nimport { build, createServer, mergeConfig } from 'vite';\nimport react from '@vitejs/plugin-react-swc';\nimport inject from '@rollup/plugin-inject';\nimport {\n OssidoReactPlugin,\n routeGenerator,\n} from '@ossido-labs/ossido-react-vite-plugin';\nimport { ErrorOverlayVitePlugin } from '@ossido-labs/ossido-ui/vite-plugin';\n\nimport type { OssidoConfig } from '../config';\n\nimport type { InternalOssidoConfig } from './types';\nimport { blockingAsync, listFilesRecursive } from './utils';\nimport { createJsonConfig, loadConfig } from './config';\nimport { ENV_PREFIX } from './constants';\nimport { createOssidoViteLogger } from './logger';\n\nconst require = createRequire(import.meta.url);\n\n/**\n * Absolute entry paths for the generated `.ossido` scaffold.\n *\n * The vite config sets `root: '.ossido'`. Vite's `build()` resolves a relative\n * `rollupOptions.input` against the CWD, but the dev server's dependency\n * scanner (rolldown, vite 8+) resolves it against `root` — so a path like\n * `./.ossido/client-main.tsx` double-nests to `.ossido/.ossido/...` and fails to\n * resolve. Absolute paths are unambiguous across both code paths.\n */\nconst CLIENT_MAIN_ENTRY = resolve('.ossido/client-main.tsx');\nconst SERVER_MAIN_ENTRY = resolve('.ossido/server-main.tsx');\n\n/**\n * `@rollup/plugin-inject` injects these imports into every module that\n * references the globals below — including third-party files deep in\n * `node_modules` (e.g. `react-dom/server`). Vite 8's bundler (rolldown)\n * resolves an injected bare specifier relative to the *importing* file, so\n * `'@ossido-labs/ossido/ssr'` fails to resolve from within `react-dom`. Resolving to\n * absolute paths up-front makes the injected imports resolvable from anywhere\n * (and is a no-op for rollup on Vite <= 7).\n */\nconst SSR_POLYFILLS_MODULE = require.resolve('@ossido-labs/ossido/ssr');\nconst WEB_STREAMS_POLYFILL_MODULE = require.resolve('web-streams-polyfill');\n\nconst VITE_SSR_PLUGINS: Array<Plugin> = [\n {\n enforce: 'post',\n // `@rollup/plugin-inject` is typed against an older rollup than the one\n // bundled with vite, so its `Plugin` type needs widening to vite's.\n ...(inject({\n ReadableStream: [WEB_STREAMS_POLYFILL_MODULE, 'ReadableStream'],\n\n /**\n * Added to support `react@19`\n * @see https://github.com/tuono-labs/tuono/issues/218\n */\n MessageChannel: [SSR_POLYFILLS_MODULE, 'MessageChannelPolyfill'],\n MessageEvent: [SSR_POLYFILLS_MODULE, 'MessageEventPolyfill'],\n Event: [SSR_POLYFILLS_MODULE, 'EventPolyfill'],\n }) as unknown as Plugin),\n },\n];\n\n/**\n * From a resolved {@link InternalOssidoConfig} return a `vite` \"mergeable\"\n * {@link InlineConfig} including all default ossido related options\n */\nfunction createBaseViteConfigFromOssidoConfig(\n ossidoConfig: InternalOssidoConfig,\n): InlineConfig {\n /**\n * @warning Keep in sync with {@link LazyLoadingPlugin} tests:\n * packages/lazy-fn-vite-plugin/tests/transpileSource.test.ts\n */\n const pluginFilesInclude = /\\.(jsx|js|mdx|md|tsx|ts)$/;\n\n const viteBaseConfig: InlineConfig = {\n root: '.ossido',\n // `'error'` (not `'silent'`) so build failures surface instead of\n // degrading to a silent client-side fallback with hydration mismatches.\n logLevel: 'error',\n // Tag vite's output `[FE]` so it matches the rest of the Ossido logs. The\n // level is passed explicitly so the logger honours it (see the logger docs)\n // — this keeps vite's info/benign-warning noise out at `'error'`.\n customLogger: createOssidoViteLogger('error'),\n publicDir: '../public',\n cacheDir: 'cache',\n envDir: '../',\n envPrefix: ENV_PREFIX,\n\n define: {\n // Baked into the client bundle for `ossido build --static` (the CLI sets\n // `OSSIDO_STATIC` for the react build). The router reads it to fetch the\n // pre-rendered `.json` data files instead of the extensionless\n // `/__ossido/data{path}` route, which has no server in a static export.\n __OSSIDO_STATIC__: JSON.stringify(process.env.OSSIDO_STATIC === 'true'),\n // Baked into both bundles so `<CriticalCss>` skips rendering its dev\n // stylesheet links when `dev.criticalCss: false` — the vite endpoint is\n // disabled in that case, so the links would otherwise be dead requests\n // that block rendering.\n __OSSIDO_CRITICAL_CSS__: JSON.stringify(\n ossidoConfig.dev?.criticalCss ?? true,\n ),\n // Baked into both bundles so the router wraps client navigations in\n // `document.startViewTransition` only when the app opted in via\n // `viewTransitions: true`. Off by default.\n __OSSIDO_VIEW_TRANSITIONS__: JSON.stringify(\n ossidoConfig.viewTransitions ?? false,\n ),\n },\n\n resolve: {\n alias: ossidoConfig.vite?.alias ?? {},\n },\n\n css: ossidoConfig.vite?.css,\n\n optimizeDeps: ossidoConfig.vite?.optimizeDeps,\n\n plugins: [\n ...(ossidoConfig.vite?.plugins ?? []),\n\n /**\n * even if `include` is not a valid option for this\n * plugin, we have to use it.\n * If not specified, when running `ossido dev`, the mdx\n * won't be compiled include any style in the page and it might\n * seem broken.\n */\n // @ts-expect-error see above comment\n react({ include: pluginFilesInclude }),\n\n OssidoReactPlugin({ criticalCss: ossidoConfig.dev?.criticalCss ?? true }),\n ],\n };\n\n // seems redundant but it's useful to log the value when debugging, until we have a logging infrastructure.\n return viteBaseConfig;\n}\n\nconst developmentSSRBundle = (): void => {\n blockingAsync(async () => {\n const config = await loadConfig();\n await build(\n mergeConfig<InlineConfig, InlineConfig>(\n createBaseViteConfigFromOssidoConfig(config),\n {\n // A dedicated cache dir, separate from the client dev server's\n // (`.ossido/cache`). Both run concurrently in dev, and this SSR bundle\n // is rebuilt on every `.tsx`/`.mdx`/CSS-module edit — sharing the cache\n // makes each rebuild invalidate the dev server's optimized-deps cache,\n // which triggers a spurious full-page reload (and data refetch) right\n // after the in-place Fast Refresh has already applied.\n cacheDir: 'cache-ssr',\n plugins: VITE_SSR_PLUGINS,\n build: {\n ssr: true,\n minify: false,\n outDir: 'server',\n emptyOutDir: true,\n rollupOptions: {\n input: SERVER_MAIN_ENTRY,\n output: {\n entryFileNames: 'dev-server.js',\n // ESM (not IIFE) so the SSR entry can use top-level `await`; the\n // Rust runtime compiles it as a V8 module (`Ssr::from_module`).\n // The build stays a single `dev-server.js` — the only file the\n // runtime reads — because the SSR build disables code splitting.\n format: 'es',\n },\n },\n },\n ssr: {\n target: 'webworker',\n noExternal: true,\n },\n },\n ),\n );\n });\n};\n\nconst developmentCSRWatch = (): void => {\n blockingAsync(async () => {\n const config = await loadConfig();\n\n const server = await createServer(\n mergeConfig<InlineConfig, InlineConfig>(\n createBaseViteConfigFromOssidoConfig(config),\n {\n // Entry point for the development vite proxy\n base: '/vite-server/',\n plugins: [ErrorOverlayVitePlugin],\n\n server: {\n host: config.server.host,\n port: config.server.port + 1,\n strictPort: true,\n },\n build: {\n manifest: true,\n emptyOutDir: true,\n rollupOptions: {\n input: CLIENT_MAIN_ENTRY,\n },\n },\n },\n ),\n );\n await server.listen();\n });\n};\n\nconst KB = 1024;\n\nconst formatKb = (bytes: number): string => `${(bytes / KB).toFixed(1)} kB`;\n\n/**\n * Print a compact per-asset size summary (raw + gzip) for the prod build, so\n * size regressions are visible on every `ossido build` without extra tooling.\n * Reads the emitted files directly — works regardless of vite's log level.\n */\nconst printBundleSummary = (): void => {\n interface AssetRow {\n label: string;\n size: number;\n gzip: number;\n }\n\n const collectDir = (dir: string, prefix: string): Array<AssetRow> => {\n try {\n return readdirSync(dir, { recursive: true, encoding: 'utf-8' })\n .filter((file) => /\\.(js|css)$/.test(file))\n .map((file) => {\n const content = readFileSync(join(dir, file));\n return {\n label: `${prefix}/${file}`,\n size: content.length,\n gzip: gzipSync(content).length,\n };\n });\n } catch {\n return [];\n }\n };\n\n const assets = [\n ...collectDir(resolve('out/client'), 'client'),\n ...collectDir(resolve('out/server'), 'server'),\n ].sort((a, b) => b.size - a.size);\n\n if (!assets.length) return;\n\n const labelWidth = Math.max(...assets.map((asset) => asset.label.length));\n console.log('');\n for (const asset of assets) {\n console.log(\n ` ${asset.label.padEnd(labelWidth)} ${formatKb(asset.size).padStart(10)} │ gzip: ${formatKb(asset.gzip).padStart(9)}`,\n );\n }\n const total = assets.reduce((sum, asset) => sum + asset.size, 0);\n const totalGzip = assets.reduce((sum, asset) => sum + asset.gzip, 0);\n console.log(\n ` ${'total'.padEnd(labelWidth)} ${formatKb(total).padStart(10)} │ gzip: ${formatKb(totalGzip).padStart(9)}`,\n );\n console.log('');\n};\n\nconst buildProd = (): void => {\n blockingAsync(async () => {\n const config = await loadConfig();\n\n // Generate the route tree once up front. Both builds' plugin instances\n // re-run the generator on `configResolved`, but it skips the write when\n // the content is unchanged — pre-generating removes the concurrent-write\n // hazard so the two builds (independent outputs) can run in parallel.\n await routeGenerator();\n\n await Promise.all([\n build(\n mergeConfig<InlineConfig, InlineConfig>(\n createBaseViteConfigFromOssidoConfig(config),\n {\n build: {\n manifest: true,\n emptyOutDir: true,\n outDir: '../out/client',\n rollupOptions: {\n input: CLIENT_MAIN_ENTRY,\n },\n },\n },\n ),\n ),\n build(\n mergeConfig<InlineConfig, InlineConfig>(\n createBaseViteConfigFromOssidoConfig(config),\n {\n plugins: VITE_SSR_PLUGINS,\n build: {\n ssr: true,\n minify: true,\n outDir: '../out/server',\n emptyOutDir: true,\n rollupOptions: {\n input: SERVER_MAIN_ENTRY,\n output: {\n entryFileNames: 'prod-server.js',\n // ESM (not IIFE) so the SSR entry can use top-level `await`;\n // the Rust runtime compiles it as a V8 module\n // (`Ssr::from_module`). The build stays a single\n // `prod-server.js` — the only file the runtime reads —\n // because the SSR build disables code splitting.\n format: 'es',\n },\n },\n },\n ssr: {\n target: 'webworker',\n noExternal: true,\n },\n },\n ),\n ),\n ]);\n\n printBundleSummary();\n });\n};\n\nconst buildConfig = (): void => {\n blockingAsync(async (): Promise<void> => {\n await build({\n root: '.ossido',\n // `'error'` (not `'silent'`) so config build failures are visible.\n logLevel: 'error',\n cacheDir: 'cache',\n envDir: '../',\n build: {\n ssr: true,\n outDir: 'config',\n emptyOutDir: true,\n rollupOptions: {\n input: './ossido.config.ts',\n output: {\n entryFileNames: 'config.mjs',\n },\n },\n },\n });\n\n const config = await loadConfig();\n await createJsonConfig(config);\n });\n};\n\n/**\n * Run a user-defined build lifecycle hook (`prebuild` / `postbuild`) from\n * `ossido.config.ts`. Invoked by the Rust CLI (`ossido-run-build-hook <name>`)\n * at the matching point of `ossido build`. No-ops when the hook is undefined.\n *\n * The output/public dirs and mode mirror what the build produces: static builds\n * deploy `out/static`, server builds `out`. `postbuild` gets the emitted-file\n * manifest; `prebuild` runs before anything is emitted, so its manifest is empty.\n */\nconst runBuildHook = (name: 'prebuild' | 'postbuild'): void =>\n blockingAsync(async () => {\n const config = await loadConfig();\n const hook = config.build?.[name];\n if (typeof hook !== 'function') return;\n\n const mode = process.env.OSSIDO_STATIC === 'true' ? 'static' : 'server';\n const outputDirectory = resolve(mode === 'static' ? 'out/static' : 'out');\n const publicDirectory = resolve('public');\n const manifest =\n name === 'postbuild' ? listFilesRecursive(outputDirectory) : [];\n\n // `loadConfig` returns the resolved `InternalOssidoConfig` (a structural\n // superset of the public `OssidoConfig` the hook context exposes).\n await hook({\n mode,\n outputDirectory,\n publicDirectory,\n manifest,\n config: config as OssidoConfig,\n });\n });\n\nexport {\n buildProd,\n buildConfig,\n developmentCSRWatch,\n developmentSSRBundle,\n runBuildHook,\n};\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
|
},
|