toiljs 0.0.60 → 0.0.62
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/.github/workflows/ci.yml +31 -0
- package/CHANGELOG.md +17 -0
- package/build/cli/.tsbuildinfo +1 -1
- package/build/cli/index.js +2 -2
- package/build/client/.tsbuildinfo +1 -1
- package/build/client/index.d.ts +1 -1
- package/build/client/index.js +1 -1
- package/build/client/routing/mount.js +11 -26
- package/build/client/ssr/markers.d.ts +1 -0
- package/build/client/ssr/markers.js +9 -2
- package/build/compiler/.tsbuildinfo +1 -1
- package/build/compiler/config.d.ts +21 -0
- package/build/compiler/config.js +35 -0
- package/build/compiler/docs.d.ts +2 -1
- package/build/compiler/docs.js +33 -304
- package/build/compiler/index.d.ts +13 -0
- package/build/compiler/index.js +113 -21
- package/build/compiler/template-build.d.ts +23 -3
- package/build/compiler/template-build.js +120 -30
- package/build/compiler/toil-docs.generated.d.ts +1 -0
- package/build/compiler/toil-docs.generated.js +20 -0
- package/build/devserver/.tsbuildinfo +1 -1
- package/build/devserver/daemon/catalog.d.ts +26 -0
- package/build/devserver/daemon/catalog.js +48 -0
- package/build/devserver/daemon/cron.d.ts +4 -0
- package/build/devserver/daemon/cron.js +50 -0
- package/build/devserver/daemon/host.d.ts +37 -0
- package/build/devserver/daemon/host.js +94 -0
- package/build/devserver/daemon/index.d.ts +34 -0
- package/build/devserver/daemon/index.js +241 -0
- package/build/devserver/db/catalog.d.ts +2 -1
- package/build/devserver/db/catalog.js +44 -44
- package/build/devserver/db/database.d.ts +27 -11
- package/build/devserver/db/database.js +539 -169
- package/build/devserver/db/index.d.ts +1 -1
- package/build/devserver/db/index.js +1 -1
- package/build/devserver/db/routeKinds.d.ts +8 -0
- package/build/devserver/db/routeKinds.js +139 -0
- package/build/devserver/db/types.d.ts +64 -1
- package/build/devserver/db/types.js +33 -1
- package/build/devserver/index.d.ts +10 -0
- package/build/devserver/index.js +7 -0
- package/build/devserver/mstore/store.d.ts +18 -0
- package/build/devserver/mstore/store.js +82 -0
- package/build/devserver/runtime/host.d.ts +6 -0
- package/build/devserver/runtime/host.js +45 -1
- package/build/devserver/runtime/module.d.ts +1 -0
- package/build/devserver/runtime/module.js +27 -1
- package/build/devserver/server.d.ts +6 -0
- package/build/devserver/server.js +59 -0
- package/build/devserver/ssr.d.ts +25 -0
- package/build/devserver/ssr.js +114 -0
- package/build/devserver/wasm/sections.d.ts +2 -0
- package/build/devserver/wasm/sections.js +42 -0
- package/build/devserver/wasm/surface.d.ts +18 -0
- package/build/devserver/wasm/surface.js +41 -0
- package/docs/README.md +4 -4
- package/docs/auth-todo.md +6 -6
- package/docs/caching.md +5 -5
- package/docs/cli.md +15 -0
- package/docs/client.md +40 -0
- package/docs/crypto.md +4 -4
- package/docs/data.md +6 -6
- package/docs/email.md +28 -28
- package/docs/environment.md +10 -10
- package/docs/index.md +26 -0
- package/docs/ratelimit.md +10 -10
- package/docs/routing.md +2 -2
- package/docs/server.md +61 -0
- package/docs/ssr.md +561 -113
- package/docs/styling.md +22 -0
- package/docs/time.md +1 -1
- package/eslint.config.js +10 -1
- package/examples/basic/client/components/Header.tsx +3 -0
- package/examples/basic/client/routes/features/actions.tsx +0 -2
- package/examples/basic/client/routes/hello.tsx +89 -19
- package/examples/basic/client/styles/main.css +48 -0
- package/examples/basic/server/SsrHelloRender.ts +97 -0
- package/examples/basic/server/main.ts +5 -0
- package/examples/basic/server/streams/Echo.ts +49 -0
- package/package.json +12 -10
- package/scripts/gen-toil-docs.mjs +96 -0
- package/src/cli/create.ts +2 -2
- package/src/client/index.ts +1 -1
- package/src/client/routing/mount.tsx +19 -31
- package/src/client/ssr/markers.tsx +33 -4
- package/src/compiler/config.ts +88 -2
- package/src/compiler/docs.ts +47 -308
- package/src/compiler/index.ts +236 -32
- package/src/compiler/ssr-codegen.ts +1 -1
- package/src/compiler/template-build.ts +271 -53
- package/src/compiler/toil-docs.generated.ts +26 -0
- package/src/devserver/daemon/catalog.ts +120 -0
- package/src/devserver/daemon/cron.ts +87 -0
- package/src/devserver/daemon/host.ts +224 -0
- package/src/devserver/daemon/index.ts +349 -0
- package/src/devserver/db/catalog.ts +61 -53
- package/src/devserver/db/database.ts +613 -149
- package/src/devserver/db/index.ts +1 -1
- package/src/devserver/db/routeKinds.ts +147 -0
- package/src/devserver/db/types.ts +65 -2
- package/src/devserver/index.ts +12 -0
- package/src/devserver/mstore/store.ts +121 -0
- package/src/devserver/runtime/host.ts +92 -1
- package/src/devserver/runtime/module.ts +35 -1
- package/src/devserver/server.ts +101 -0
- package/src/devserver/ssr.ts +166 -0
- package/src/devserver/wasm/sections.ts +59 -0
- package/src/devserver/wasm/surface.ts +88 -0
- package/test/daemon-build.test.ts +198 -0
- package/test/daemon-catalog.test.ts +265 -0
- package/test/daemon-emulation.test.ts +216 -0
- package/test/devserver-database.test.ts +396 -5
- package/test/email-preview.test.ts +6 -1
- package/test/fixtures/daemon-app.ts +56 -0
- package/test/global-setup.ts +17 -0
- package/test/ssr-hydration.test.tsx +107 -0
- package/test/ssr-render.test.ts +96 -27
- package/test/ssr-template.test.tsx +47 -2
- package/vitest.config.ts +3 -0
package/build/compiler/docs.js
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { TOIL_DOCS } from './toil-docs.generated.js';
|
|
4
|
+
export { TOIL_DOCS };
|
|
5
|
+
const DOC_SUMMARIES = {
|
|
6
|
+
'index.md': 'overview and project layout',
|
|
7
|
+
'getting-started.md': 'the two halves, build/dev, and the request lifecycle',
|
|
8
|
+
'routing.md': 'file-based routing, nested layouts, loading / error files',
|
|
9
|
+
'client.md': 'the `Toil` global, Link / NavLink, router hooks',
|
|
10
|
+
'styling.md': 'CSS / Sass / Less / Stylus / Tailwind (via `toiljs configure`)',
|
|
11
|
+
'server.md': 'the toilscript server target, `@data` / `@remote` / `@rest`',
|
|
12
|
+
'ssr.md': 'server-side rendering (`ssr = true`, hole markers, the server `render`)',
|
|
13
|
+
'rpc.md': '`@service` / `@remote` and the generated typed client',
|
|
14
|
+
'data.md': 'the `@data` decorator and the binary codec',
|
|
15
|
+
'caching.md': 'the `@cache` decorator and `Response.cache(...)`',
|
|
16
|
+
'ratelimit.md': 'the `@ratelimit` decorator',
|
|
17
|
+
'auth.md': '`@auth` guards, `@user`, sessions, the client half',
|
|
18
|
+
'environment.md': '`Environment.get` / `getSecure` config + secrets',
|
|
19
|
+
'email.md': '`EmailService`, templates, provider config',
|
|
20
|
+
'cookies.md': 'the `Cookie` builder, `SecureCookies`, signing / encryption',
|
|
21
|
+
'crypto.md': 'the synchronous `crypto` global and `crypto.subtle`',
|
|
22
|
+
'time.md': '`Time.nowMillis()` / `Time.nowSeconds()`',
|
|
23
|
+
'cli.md': 'toiljs CLI commands',
|
|
24
|
+
};
|
|
25
|
+
function docTitle(content) {
|
|
26
|
+
const m = content.match(/^#\s+(.+)$/m);
|
|
27
|
+
return m ? m[1].trim() : '';
|
|
28
|
+
}
|
|
29
|
+
const DOC_POINTERS = Object.keys(TOIL_DOCS)
|
|
30
|
+
.map((name) => {
|
|
31
|
+
const summary = DOC_SUMMARIES[name] ?? docTitle(TOIL_DOCS[name]);
|
|
32
|
+
return `- \`.toil/docs/${name}\`${summary ? `, ${summary}` : ''}`;
|
|
33
|
+
})
|
|
34
|
+
.join('\n');
|
|
3
35
|
const POINTER_BODY = `# toiljs, AI assistant guide
|
|
4
36
|
|
|
5
37
|
This is a **toiljs** project, a full-stack React framework (React + Vite client, file-based
|
|
@@ -8,13 +40,7 @@ routing, and a toilscript→WebAssembly server).
|
|
|
8
40
|
**Before editing this project, read the generated documentation in \`.toil/docs/\`.** It describes
|
|
9
41
|
the conventions you must follow:
|
|
10
42
|
|
|
11
|
-
|
|
12
|
-
- \`.toil/docs/routing.md\`, file-based routing, nested layouts, loading / error files
|
|
13
|
-
- \`.toil/docs/client.md\`, the \`Toil\` global, Link / NavLink, router hooks
|
|
14
|
-
- \`.toil/docs/styling.md\`, CSS / Sass / Less / Stylus / Tailwind (via \`toiljs configure\`)
|
|
15
|
-
- \`.toil/docs/server.md\`, the toilscript server target
|
|
16
|
-
- \`.toil/docs/ssr.md\`, server-side rendering (\`ssr = true\`, hole markers, the server \`render\`)
|
|
17
|
-
- \`.toil/docs/cli.md\`, toiljs CLI commands
|
|
43
|
+
${DOC_POINTERS}
|
|
18
44
|
|
|
19
45
|
\`.toil/docs/\` is regenerated by toiljs; do not edit it by hand. This pointer file is yours to edit.
|
|
20
46
|
`;
|
|
@@ -43,303 +69,6 @@ export function aiHelperFiles(ids) {
|
|
|
43
69
|
}
|
|
44
70
|
return out;
|
|
45
71
|
}
|
|
46
|
-
function doc(lines) {
|
|
47
|
-
return lines.join('\n') + '\n';
|
|
48
|
-
}
|
|
49
|
-
export const TOIL_DOCS = {
|
|
50
|
-
'index.md': doc([
|
|
51
|
-
'# toiljs',
|
|
52
|
-
'',
|
|
53
|
-
'A full-stack React framework: a Vite-bundled client SPA with file-based routing, plus a',
|
|
54
|
-
'toilscript-to-WebAssembly server target.',
|
|
55
|
-
'',
|
|
56
|
-
'## Project layout',
|
|
57
|
-
'',
|
|
58
|
-
'- `client/`, the app: `routes/` (file-based), `layout.tsx`, `components/`, `styles/`,',
|
|
59
|
-
' `public/`, and `toil.tsx` (the entry that calls `Toil.mount`).',
|
|
60
|
-
'- `server/`, the toilscript → WASM target (`@main` entry), compiled by `toilscript`.',
|
|
61
|
-
' `@data`/`@remote`/`@service` here generate the typed client `Server` API (see `server.md`).',
|
|
62
|
-
'- `toil.config.ts`, configuration via `defineConfig` (`toiljs.config.ts` also works).',
|
|
63
|
-
'- Generated, gitignored, do not edit: `.toil/` (working dir), `toil-env.d.ts` (ambient',
|
|
64
|
-
' globals), `toil-routes.d.ts` (typed routes), `shared/server.ts` (the typed RPC module,',
|
|
65
|
-
' emitted by the server build; import `@data` classes from `shared/server`).',
|
|
66
|
-
'',
|
|
67
|
-
'## Key ideas',
|
|
68
|
-
'',
|
|
69
|
-
'- `Toil` is a native global (no import): `Toil.Link`, `Toil.useRouter`, `Toil.useLoaderData`,',
|
|
70
|
-
' etc. The IO classes (`FastMap`, `FastSet`, `DataWriter`, `DataReader`), `parseError`, and the',
|
|
71
|
-
' generated `Server` RPC surface are globals too.',
|
|
72
|
-
'- Scripts: `npm run dev` (HMR), `npm run build` (→ `build/client` + `build/server`),',
|
|
73
|
-
' `npm start` (self-host the build).',
|
|
74
|
-
'',
|
|
75
|
-
'See `routing.md`, `client.md`, `styling.md`, `server.md`, `ssr.md`, `cli.md`.',
|
|
76
|
-
]),
|
|
77
|
-
'routing.md': doc([
|
|
78
|
-
'# Routing',
|
|
79
|
-
'',
|
|
80
|
-
'File-based, under `client/routes/`. A file path maps to a URL.',
|
|
81
|
-
'',
|
|
82
|
-
'## Route files',
|
|
83
|
-
'',
|
|
84
|
-
'- `index.tsx` → `/`, `about.tsx` → `/about`, `blog/index.tsx` → `/blog`',
|
|
85
|
-
'- `[id].tsx` → dynamic `/:id`, read with `Toil.useParams<{ id: string }>()`',
|
|
86
|
-
'- `[...slug].tsx` → catch-all (1+ segments); `[[...slug]].tsx` → optional catch-all (0+)',
|
|
87
|
-
'- `(group)/` → route group: groups files / scopes a layout, adds no URL segment',
|
|
88
|
-
'',
|
|
89
|
-
'## Special files',
|
|
90
|
-
'',
|
|
91
|
-
'- `layout.tsx`, wraps the routes beneath it. Root `client/layout.tsx` wraps everything;',
|
|
92
|
-
' nested `routes/**/layout.tsx` compose inside it.',
|
|
93
|
-
'- `loading.tsx`, Suspense fallback shown while a route (chunk + loader) loads',
|
|
94
|
-
'- `error.tsx`, error boundary; receives `{ error, reset }` (`Toil.RouteErrorProps`)',
|
|
95
|
-
'- `client/404.tsx`, shown when no route matches',
|
|
96
|
-
'',
|
|
97
|
-
'## Data loaders',
|
|
98
|
-
'',
|
|
99
|
-
'Export `loader` from a route; it runs on navigation, in parallel with the chunk, and the',
|
|
100
|
-
'page suspends until it resolves (so `loading.tsx` shows). Read it with `useLoaderData`:',
|
|
101
|
-
'',
|
|
102
|
-
' export const loader = async ({ params, searchParams }: Toil.LoaderArgs) => {',
|
|
103
|
-
' return fetch(`/api/post/${params.id}`).then((r) => r.json());',
|
|
104
|
-
' };',
|
|
105
|
-
' export default function Post() {',
|
|
106
|
-
' const post = Toil.useLoaderData<{ title: string }>();',
|
|
107
|
-
' return <h1>{post.title}</h1>;',
|
|
108
|
-
' }',
|
|
109
|
-
'',
|
|
110
|
-
'`Toil.useRouter().refresh()` re-runs loaders.',
|
|
111
|
-
'',
|
|
112
|
-
'## Server rendering',
|
|
113
|
-
'',
|
|
114
|
-
'Add `export const ssr = true` to render a route on the server (real first-paint HTML + SEO,',
|
|
115
|
-
'then hydration) with near-static speed. See `ssr.md`.',
|
|
116
|
-
'',
|
|
117
|
-
'## Navigation',
|
|
118
|
-
'',
|
|
119
|
-
'- `Toil.Link` / `Toil.NavLink` (adds an active class) / `Toil.useRouter()`',
|
|
120
|
-
' (push / replace / back / forward / refresh / prefetch)',
|
|
121
|
-
'- `Toil.useParams`, `usePathname`, `useSearchParams`, `useNavigationPending`',
|
|
122
|
-
'- Hrefs are type-checked against your routes, a typo is a compile error.',
|
|
123
|
-
]),
|
|
124
|
-
'client.md': doc([
|
|
125
|
-
'# Client runtime',
|
|
126
|
-
'',
|
|
127
|
-
'Everything is on the `Toil` global, no imports needed in route files.',
|
|
128
|
-
'',
|
|
129
|
-
'## Entry',
|
|
130
|
-
'',
|
|
131
|
-
'`client/toil.tsx` imports the route table + global styles and mounts the app:',
|
|
132
|
-
'',
|
|
133
|
-
' import { routes, layout, notFound } from "toiljs/routes";',
|
|
134
|
-
' import "./styles/main.css";',
|
|
135
|
-
' Toil.mount(routes, layout, notFound);',
|
|
136
|
-
'',
|
|
137
|
-
'## API (on `Toil`)',
|
|
138
|
-
'',
|
|
139
|
-
'- Components: `Link`, `NavLink`, `Head`',
|
|
140
|
-
'- Navigation: `navigate`, `useRouter`, `useNavigate`',
|
|
141
|
-
'- Location: `usePathname`, `useSearchParams`, `useParams`, `useNavigationPending`',
|
|
142
|
-
'- Data: `useLoaderData` (see `routing.md`)',
|
|
143
|
-
'- Head: `useHead`, `useTitle`, `<Head>`, set the `<title>` / meta per route',
|
|
144
|
-
'- Realtime: `useChannel`, `connectChannel` (WebSocket to the backend at `/_toil`)',
|
|
145
|
-
'- IO globals (no `Toil.` prefix): `FastMap`, `FastSet`, `DataWriter`, `DataReader`',
|
|
146
|
-
'- `parseError(err)` global: message from an unknown caught value (handy in `catch`)',
|
|
147
|
-
'- `Server` global: the typed RPC surface generated from the server (see `server.md`)',
|
|
148
|
-
'- `Server.REST.<controller>.<route>(args)`: a working, typed `fetch` client for your',
|
|
149
|
-
' `@rest` controllers, e.g. `await Server.REST.todos.getTodo({ params: { id } })` or',
|
|
150
|
-
' `await Server.REST.todos.add({ body: new AddTodo("milk") })`. `args` is',
|
|
151
|
-
' `{ params?, body?, query?, headers? }`; returns are typed (`@data` classes are parsed for',
|
|
152
|
-
' you). The REST client attaches when you import from `shared/server`.',
|
|
153
|
-
'',
|
|
154
|
-
'## Head example',
|
|
155
|
-
'',
|
|
156
|
-
' Toil.useHead({',
|
|
157
|
-
' title: "Blog",',
|
|
158
|
-
' titleTemplate: "%s, MyApp",',
|
|
159
|
-
' meta: [{ name: "description", content: "..." }],',
|
|
160
|
-
' });',
|
|
161
|
-
]),
|
|
162
|
-
'styling.md': doc([
|
|
163
|
-
'# Styling',
|
|
164
|
-
'',
|
|
165
|
-
'The app imports one stylesheet from `client/toil.tsx` (e.g. `./styles/main.css`).',
|
|
166
|
-
'',
|
|
167
|
-
'## Preprocessors & Tailwind',
|
|
168
|
-
'',
|
|
169
|
-
'Pick a CSS preprocessor (none / Sass / Less / Stylus) and optionally Tailwind at',
|
|
170
|
-
'`toiljs create`, or change it later on an existing project:',
|
|
171
|
-
'',
|
|
172
|
-
' toiljs configure # interactive',
|
|
173
|
-
' toiljs configure --tailwind # add Tailwind',
|
|
174
|
-
' toiljs configure --style sass # switch preprocessor',
|
|
175
|
-
'',
|
|
176
|
-
'`configure` installs/removes the right packages and rewrites the imports. Tailwind lives',
|
|
177
|
-
'in its own `styles/tailwind.css` (`@import "tailwindcss";`).',
|
|
178
|
-
'',
|
|
179
|
-
'## Imports',
|
|
180
|
-
'',
|
|
181
|
-
'`.css` / `.scss` / `.sass` / `.less` / `.styl` and image imports (`.svg`, `.png`, …) are',
|
|
182
|
-
'typed via `toil-env.d.ts`.',
|
|
183
|
-
]),
|
|
184
|
-
'server.md': doc([
|
|
185
|
-
'# Server (toilscript → WebAssembly)',
|
|
186
|
-
'',
|
|
187
|
-
'`server/` is the toilscript source, compiled to WebAssembly by `toilscript`.',
|
|
188
|
-
'',
|
|
189
|
-
'- `server/main.ts`, the `@main` entry, exported as the WASM `main`.',
|
|
190
|
-
'- `server/index.ts`, your functions.',
|
|
191
|
-
'- `server/tsconfig.json`, extends `toilscript/std/assembly.json` (AssemblyScript/toilscript',
|
|
192
|
-
' globals like `i32`, not the DOM), so editors resolve server types correctly.',
|
|
193
|
-
'- `npm run build:server` (or `npm run build`) emits `build/server/release.wasm` and',
|
|
194
|
-
' regenerates `shared/server.ts` (the typed client RPC module).',
|
|
195
|
-
'',
|
|
196
|
-
'## Typed RPC (`@data` / `@remote` / `@service`)',
|
|
197
|
-
'',
|
|
198
|
-
'Tag server code and the build generates a typed client `Server` surface:',
|
|
199
|
-
'',
|
|
200
|
-
'- `@data class X {}`, a serializable struct. Generates a client class with the same fields',
|
|
201
|
-
' plus `encode`/`decode`; construct it on the client: `import { X } from "shared/server"`.',
|
|
202
|
-
'- `@remote function f(a: T): R`, a client-callable endpoint, becomes `Server.f(a)`.',
|
|
203
|
-
'- `@service class S { @remote m(...) {} }`, namespaces methods: `Server.s.m(...)`.',
|
|
204
|
-
'',
|
|
205
|
-
'On the client, `Server` is a global (no import) and fully typed; every call is async',
|
|
206
|
-
'(`Promise<R>`). Inputs/outputs are scalars, arrays, or `@data` classes, both directions.',
|
|
207
|
-
'',
|
|
208
|
-
'Note: the client↔server transport is not wired yet, so calling a `Server` method throws',
|
|
209
|
-
'until it lands; the typed surface + codec are generated and ready.',
|
|
210
|
-
'',
|
|
211
|
-
'## HTTP REST (`@rest` / `@route`)',
|
|
212
|
-
'',
|
|
213
|
-
'Tag a class `@rest` and its methods with a verb to expose a real HTTP API. Unlike RPC,',
|
|
214
|
-
'the generated client is working `fetch` code (it is just HTTP).',
|
|
215
|
-
'',
|
|
216
|
-
'- `@rest("api") class Todos {}`, mounts the controller at `/api` (bare `@rest` → `/`).',
|
|
217
|
-
'- `@get("/todos/:id")` / `@post` / `@put` / `@del` / `@patch` / `@head` / `@options`, verb',
|
|
218
|
-
' shortcuts; or `@route({ method: Methods.GET, path: "/todos", stream: DataStream.JSON })`.',
|
|
219
|
-
'- A method takes an optional `@data` body + an optional `ctx: RouteContext` (path params via',
|
|
220
|
-
' `ctx.param("id")`, `ctx.query(...)`, `ctx.header(...)`). It returns either a `@data` type,',
|
|
221
|
-
' which the compiler encodes per `stream` (`DataStream.JSON` default, or `DataStream.Binary`,',
|
|
222
|
-
' lossless for large `u64`/bignum), or a `Response` for full control - custom status and',
|
|
223
|
-
' headers, e.g. `Response.json(value.toJSON().toString()).setHeader("cache-control", "no-store")`',
|
|
224
|
-
' or `Response.notFound()`. (The editor sees the compiler-injected `@data` `toJSON`/`encode`',
|
|
225
|
-
' members via the toilscript plugin, so serializing into a `Response` is editor-clean.)',
|
|
226
|
-
'',
|
|
227
|
-
'Each `@rest` class self-registers; dispatch them from your handler - it composes, it never',
|
|
228
|
-
'takes over `handle()`:',
|
|
229
|
-
'',
|
|
230
|
-
'```ts',
|
|
231
|
-
'import { ToilHandler, Request, Response, Rest } from "toiljs/server/runtime";',
|
|
232
|
-
'export class App extends ToilHandler {',
|
|
233
|
-
' public handle(req: Request): Response {',
|
|
234
|
-
' const hit = Rest.dispatch(req); // try every @rest controller',
|
|
235
|
-
' if (hit != null) return hit;',
|
|
236
|
-
' return Response.notFound(); // your own logic / static fallback',
|
|
237
|
-
' }',
|
|
238
|
-
'}',
|
|
239
|
-
'```',
|
|
240
|
-
'',
|
|
241
|
-
'For a REST-only project, `Server.handler = () => new RestHandler()` does the same with no',
|
|
242
|
-
'boilerplate. On the client: `Server.REST.todos.getTodo({ params: { id } })` (see client.md).',
|
|
243
|
-
]),
|
|
244
|
-
'ssr.md': doc([
|
|
245
|
-
'# Server-side rendering (SSR)',
|
|
246
|
-
'',
|
|
247
|
-
'Opt a route into SSR with `export const ssr = true`. toiljs renders the page ONCE at build',
|
|
248
|
-
'time into a template with holes; at request time the server fills only the dynamic holes and',
|
|
249
|
-
'serves the page, then the browser hydrates it in place. The page is never re-rendered per',
|
|
250
|
-
'request, so an SSR route is served about as fast as a static file while still delivering real',
|
|
251
|
-
'first-paint HTML and SEO.',
|
|
252
|
-
'',
|
|
253
|
-
'## Marking the holes',
|
|
254
|
-
'',
|
|
255
|
-
'Wrap the dynamic bits of the page in hole markers from `toiljs/client`. They are transparent',
|
|
256
|
-
'in the browser (they just render their children); only the build and the server treat them',
|
|
257
|
-
'specially, so the same component is your normal client UI.',
|
|
258
|
-
'',
|
|
259
|
-
'- `<Hole id="name">{value}</Hole>`, a text hole (the value is HTML-escaped for you).',
|
|
260
|
-
'- `<RawHtml id="bio" html={s} />`, a raw-HTML block (you own sanitisation, like',
|
|
261
|
-
' `dangerouslySetInnerHTML`).',
|
|
262
|
-
'- `<Repeat id="rows" each={items}>{(item) => <li>...</li>}</Repeat>`, a repeated region (a',
|
|
263
|
-
' list); the row markup is captured once and stamped per item.',
|
|
264
|
-
'- `<Island>{...}</Island>`, a client-only escape hatch: empty in the server HTML, rendered',
|
|
265
|
-
' after hydration (so it gets no first paint or SEO). Put router-hook-driven or otherwise',
|
|
266
|
-
' non-server-safe bits here.',
|
|
267
|
-
'',
|
|
268
|
-
' import { Hole, Repeat, RawHtml, useLoaderData } from "toiljs/client";',
|
|
269
|
-
' export const ssr = true;',
|
|
270
|
-
' export const loader = ({ params }: Toil.LoaderArgs) => loadProfile(params.name);',
|
|
271
|
-
' export default function Profile() {',
|
|
272
|
-
' const d = useLoaderData<typeof loader>();',
|
|
273
|
-
' return (',
|
|
274
|
-
' <main>',
|
|
275
|
-
' <h1>@<Hole id="username">{d.username}</Hole></h1>',
|
|
276
|
-
' <RawHtml id="bio" html={d.bioHtml} />',
|
|
277
|
-
' <ul>',
|
|
278
|
-
' <Repeat id="posts" each={d.posts}>',
|
|
279
|
-
' {(p) => <li><Hole id="title">{p.title}</Hole></li>}',
|
|
280
|
-
' </Repeat>',
|
|
281
|
-
' </ul>',
|
|
282
|
-
' </main>',
|
|
283
|
-
' );',
|
|
284
|
-
' }',
|
|
285
|
-
'',
|
|
286
|
-
'## The server render',
|
|
287
|
-
'',
|
|
288
|
-
'For each SSR route the build emits a typed `Slot` enum and a coherence hash (a',
|
|
289
|
-
'`<route>.slots.ts` module). In the server you write a `render(req)` that fills each slot from',
|
|
290
|
-
'the request and data, using the `SlotValues` API, and register it with `Ssr`:',
|
|
291
|
-
'',
|
|
292
|
-
'```ts',
|
|
293
|
-
'import { Request, SlotValues, HtmlBuilder, Ssr } from "toiljs/server/runtime";',
|
|
294
|
-
'import { Slot, HASH } from "./profile.slots";',
|
|
295
|
-
'',
|
|
296
|
-
'function renderProfile(req: Request): SlotValues | null {',
|
|
297
|
-
' if (!req.path.startsWith("/u/")) return null; // not this route',
|
|
298
|
-
' const v = new SlotValues(HASH);',
|
|
299
|
-
' v.setText(Slot.username, usernameFor(req)); // escaped',
|
|
300
|
-
' v.setRaw(Slot.bio, bioHtml); // verbatim',
|
|
301
|
-
' const rows = new HtmlBuilder();',
|
|
302
|
-
' for (let i = 0; i < posts.length; i++) rows.raw("<li>").text(posts[i]).raw("</li>");',
|
|
303
|
-
' v.setRepeat(Slot.posts, rows); // stamped rows',
|
|
304
|
-
' return v;',
|
|
305
|
-
'}',
|
|
306
|
-
'Ssr.register(renderProfile);',
|
|
307
|
-
'```',
|
|
308
|
-
'',
|
|
309
|
-
'`SlotValues`: `setText` (escaped), `setRaw` (verbatim), `setAttr` (attribute value),',
|
|
310
|
-
'`setRepeat` (a stamped `HtmlBuilder`), plus `setHeader` and `setStatus`. The hole values you',
|
|
311
|
-
'return are filled into the template and the page is served; the browser then hydrates against',
|
|
312
|
-
'the same data, so the markup matches with no client re-render.',
|
|
313
|
-
'',
|
|
314
|
-
'## Rules of thumb',
|
|
315
|
-
'',
|
|
316
|
-
'- An SSR route (and the layouts above it) must render under static markup. Use the hole',
|
|
317
|
-
' markers and `useLoaderData`; move anything that needs router hooks or browser-only APIs into',
|
|
318
|
-
' an `<Island>`. A route that cannot render this way is skipped at build (with a warning) and',
|
|
319
|
-
' simply falls back to normal client rendering.',
|
|
320
|
-
'- Hole values are HTML-escaped exactly as React escapes them, so hydration is byte-for-byte',
|
|
321
|
-
" clean. Keep a repeat row's structure the same across items (only the leaf hole values vary).",
|
|
322
|
-
'- Build output for an SSR route lands in `build/client/_ssr/` (the template + its manifest)',
|
|
323
|
-
' alongside the generated `Slot` module; routes without `ssr = true` are unaffected.',
|
|
324
|
-
]),
|
|
325
|
-
'cli.md': doc([
|
|
326
|
-
'# CLI',
|
|
327
|
-
'',
|
|
328
|
-
'- `toiljs create [name]`, scaffold a project. Flags: `--template app|minimal`,',
|
|
329
|
-
' `--style css|sass|less|stylus`, `--tailwind`, `--no-ai`, `-y`/`--yes`.',
|
|
330
|
-
'- `toiljs dev`, dev server with HMR (`--port`, `--root`). With a `toilconfig.json` it builds',
|
|
331
|
-
' the server first, then rebuilds it whenever a `server/` file changes (regenerating',
|
|
332
|
-
' `shared/server.ts`, which Vite HMRs into the client); client-only edits just HMR the client.',
|
|
333
|
-
'- `toiljs build`, production build. With a `toilconfig.json` it builds the server (toilscript,',
|
|
334
|
-
' regenerating `shared/server.ts`) first, then the client → `build/client`. `--server` builds',
|
|
335
|
-
' only the server. Every `server/` file declaring a surface (`@data`/`@rest`/...) is compiled.',
|
|
336
|
-
'- `toiljs start`, self-host the built app (hyper-express) with a `/_toil` WebSocket channel.',
|
|
337
|
-
'- `toiljs configure`, toggle styling features on an existing project (see `styling.md`).',
|
|
338
|
-
'- `toiljs doctor`, diagnose project setup (`--json` for CI). `--fix` auto-wires the typed-RPC',
|
|
339
|
-
' setup (build scripts, tsconfig `shared` + alias, `.gitignore`, toilscript version, and the',
|
|
340
|
-
' toilscript prettier plugin) so an existing project upgrades in one command.',
|
|
341
|
-
]),
|
|
342
|
-
};
|
|
343
72
|
export function writeDocs(toilDir) {
|
|
344
73
|
const dir = path.join(toilDir, 'docs');
|
|
345
74
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { type ViteDevServer } from 'vite';
|
|
2
2
|
import type { RunningBackend } from 'toiljs/backend';
|
|
3
|
+
export declare const SURFACE_DECORATOR: RegExp;
|
|
4
|
+
export declare function buildServer(root: string): Promise<void>;
|
|
5
|
+
interface SurfaceSplit {
|
|
6
|
+
readonly hasDaemon: boolean;
|
|
7
|
+
readonly cold: string[];
|
|
8
|
+
readonly hot: string[];
|
|
9
|
+
}
|
|
10
|
+
export declare function splitSurfaceFiles(root: string, files: string[]): SurfaceSplit;
|
|
11
|
+
export interface ServerArtifacts {
|
|
12
|
+
readonly hot: string;
|
|
13
|
+
readonly cold: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function serverArtifacts(root: string): ServerArtifacts;
|
|
3
16
|
export interface ToilCommandOptions {
|
|
4
17
|
readonly root?: string;
|
|
5
18
|
readonly port?: number;
|
package/build/compiler/index.js
CHANGED
|
@@ -9,9 +9,9 @@ import { loadConfig } from './config.js';
|
|
|
9
9
|
import { renderEmails } from './emails.js';
|
|
10
10
|
import { generate, TOIL_SERVER_ENV_DTS } from './generate.js';
|
|
11
11
|
import { prerenderStaticParams } from './ssg.js';
|
|
12
|
-
import { extractTemplates } from './template-build.js';
|
|
12
|
+
import { extractDevSsrTemplates, extractServerSlots, extractTemplates, } from './template-build.js';
|
|
13
13
|
import { createViteConfig } from './vite.js';
|
|
14
|
-
const SURFACE_DECORATOR = /^[ \t]*@(data|rest|service|remote)\b/m;
|
|
14
|
+
export const SURFACE_DECORATOR = /^[ \t]*@(data|rest|service|remote|stream|daemon|scheduled)\b/m;
|
|
15
15
|
function toilconfigEntries(root) {
|
|
16
16
|
try {
|
|
17
17
|
const cfg = JSON.parse(fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8'));
|
|
@@ -76,7 +76,7 @@ function serverEntryFiles(root) {
|
|
|
76
76
|
visit(dir, 0);
|
|
77
77
|
return [...result].sort();
|
|
78
78
|
}
|
|
79
|
-
async function buildServer(root) {
|
|
79
|
+
export async function buildServer(root) {
|
|
80
80
|
if (!fs.existsSync(path.join(root, 'toilconfig.json')))
|
|
81
81
|
return;
|
|
82
82
|
for (const dir of serverDirs(root)) {
|
|
@@ -87,37 +87,83 @@ async function buildServer(root) {
|
|
|
87
87
|
catch {
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
|
+
const binJs = resolveToilscriptBin(root);
|
|
91
|
+
const files = serverEntryFiles(root);
|
|
92
|
+
const split = splitSurfaceFiles(root, files);
|
|
93
|
+
if (split.hasDaemon) {
|
|
94
|
+
const artifacts = serverArtifacts(root);
|
|
95
|
+
await runToilscriptPass(root, binJs, split.cold, {
|
|
96
|
+
mode: 'cold',
|
|
97
|
+
outFile: artifacts.cold,
|
|
98
|
+
withRpc: false,
|
|
99
|
+
});
|
|
100
|
+
if (split.hot.length > 0)
|
|
101
|
+
await runToilscriptPass(root, binJs, split.hot, {
|
|
102
|
+
mode: 'hot',
|
|
103
|
+
outFile: serverWasmFile(root),
|
|
104
|
+
withRpc: true,
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
await runToilscriptPass(root, binJs, files, { mode: null, outFile: null, withRpc: true });
|
|
109
|
+
}
|
|
110
|
+
function resolveToilscriptBin(root) {
|
|
90
111
|
const require = createRequire(path.join(root, 'package.json'));
|
|
91
|
-
let binJs;
|
|
92
112
|
try {
|
|
93
113
|
const pkgPath = require.resolve('toilscript/package.json');
|
|
94
114
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
|
|
95
115
|
const binRel = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.toilscript;
|
|
96
116
|
if (!binRel)
|
|
97
117
|
throw new Error('toilscript declares no bin');
|
|
98
|
-
|
|
118
|
+
return path.join(path.dirname(pkgPath), binRel);
|
|
99
119
|
}
|
|
100
120
|
catch {
|
|
101
121
|
throw new Error("toiljs: this project has a server target (toilconfig.json) but 'toilscript' is not " +
|
|
102
122
|
'installed. Run `npm i -D toilscript`, or remove toilconfig.json for a client-only build.');
|
|
103
123
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
'
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
}
|
|
125
|
+
const COLD_DECORATOR = /^[ \t]*@(daemon|scheduled)\b/m;
|
|
126
|
+
const HOT_DECORATOR = /^[ \t]*@(rest|route|stream|service|remote)\b/m;
|
|
127
|
+
export function splitSurfaceFiles(root, files) {
|
|
128
|
+
let hasDaemon = false;
|
|
129
|
+
const cold = [];
|
|
130
|
+
const hot = [];
|
|
131
|
+
for (const rel of files) {
|
|
132
|
+
let src = '';
|
|
133
|
+
try {
|
|
134
|
+
src = fs.readFileSync(path.join(root, rel), 'utf8');
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
cold.push(rel);
|
|
138
|
+
hot.push(rel);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const isCold = COLD_DECORATOR.test(src);
|
|
142
|
+
const isHot = HOT_DECORATOR.test(src);
|
|
143
|
+
if (isCold)
|
|
144
|
+
hasDaemon ||= /^[ \t]*@daemon\b/m.test(src);
|
|
145
|
+
if (!(isCold && !isHot))
|
|
146
|
+
hot.push(rel);
|
|
147
|
+
if (!(isHot && !isCold))
|
|
148
|
+
cold.push(rel);
|
|
149
|
+
}
|
|
150
|
+
return { hasDaemon, cold, hot };
|
|
151
|
+
}
|
|
152
|
+
function runToilscriptPass(root, binJs, files, opts) {
|
|
153
|
+
const args = [binJs, ...files, '--target', 'release'];
|
|
154
|
+
if (opts.mode !== null)
|
|
155
|
+
args.push('--targetMode', opts.mode);
|
|
156
|
+
if (opts.outFile !== null)
|
|
157
|
+
args.push('--outFile', opts.outFile);
|
|
158
|
+
if (opts.withRpc)
|
|
159
|
+
args.push('--rpcModule', 'shared/server.ts');
|
|
160
|
+
args.push('--disableWarning', '235');
|
|
161
|
+
return new Promise((resolve, reject) => {
|
|
116
162
|
const child = spawn(process.execPath, args, { cwd: root, stdio: 'inherit' });
|
|
117
163
|
child.on('error', reject);
|
|
118
164
|
child.on('close', (code) => code === 0
|
|
119
165
|
? resolve()
|
|
120
|
-
: reject(new Error(`toilscript
|
|
166
|
+
: reject(new Error(`toilscript ${opts.mode ?? 'release'} build failed (exit ${String(code)})`)));
|
|
121
167
|
});
|
|
122
168
|
}
|
|
123
169
|
function watchServer(cfg, watcher) {
|
|
@@ -209,6 +255,27 @@ function serverWasmFile(root) {
|
|
|
209
255
|
}
|
|
210
256
|
return path.resolve(root, outFile);
|
|
211
257
|
}
|
|
258
|
+
export function serverArtifacts(root) {
|
|
259
|
+
let out = 'build/server/release.wasm';
|
|
260
|
+
let hot;
|
|
261
|
+
let cold;
|
|
262
|
+
try {
|
|
263
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(root, 'toilconfig.json'), 'utf8'));
|
|
264
|
+
out = cfg.targets?.release?.outFile ?? out;
|
|
265
|
+
hot = cfg.targets?.release?.hotFile;
|
|
266
|
+
cold = cfg.targets?.release?.coldFile;
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
}
|
|
270
|
+
const ins = (mode) => {
|
|
271
|
+
const ext = path.extname(out);
|
|
272
|
+
return out.slice(0, ext ? -ext.length : undefined) + '-' + mode + (ext || '.wasm');
|
|
273
|
+
};
|
|
274
|
+
return {
|
|
275
|
+
hot: path.resolve(root, hot ?? ins('hot')),
|
|
276
|
+
cold: path.resolve(root, cold ?? ins('cold')),
|
|
277
|
+
};
|
|
278
|
+
}
|
|
212
279
|
async function freeLoopbackPort() {
|
|
213
280
|
return new Promise((resolve, reject) => {
|
|
214
281
|
const probe = net.createServer();
|
|
@@ -242,10 +309,12 @@ export async function dev(opts = {}) {
|
|
|
242
309
|
if (hasServer)
|
|
243
310
|
process.stdout.write(pc.dim(' building the server (toilscript)…') + '\n');
|
|
244
311
|
await renderEmails(cfg);
|
|
312
|
+
generate(cfg);
|
|
313
|
+
if (hasServer)
|
|
314
|
+
await extractServerSlots(cfg);
|
|
245
315
|
await buildServer(cfg.root);
|
|
246
316
|
if (hasServer)
|
|
247
317
|
process.stdout.write(pc.green(' ✓ ') + pc.dim('server built') + '\n');
|
|
248
|
-
generate(cfg);
|
|
249
318
|
if (!hasServer) {
|
|
250
319
|
const server = await createServer(await createViteConfig(cfg));
|
|
251
320
|
await server.listen();
|
|
@@ -260,13 +329,31 @@ export async function dev(opts = {}) {
|
|
|
260
329
|
});
|
|
261
330
|
const server = await createServer(viteConfig);
|
|
262
331
|
await server.listen();
|
|
332
|
+
let ssrTemplates = [];
|
|
333
|
+
try {
|
|
334
|
+
const rawIndex = fs.readFileSync(path.join(cfg.toilDir, 'index.html'), 'utf8');
|
|
335
|
+
const devShell = await server.transformIndexHtml('/', rawIndex);
|
|
336
|
+
ssrTemplates = await extractDevSsrTemplates(cfg, devShell);
|
|
337
|
+
if (ssrTemplates.length > 0) {
|
|
338
|
+
process.stdout.write(pc.green(' ✓ ') +
|
|
339
|
+
pc.dim(`edge SSR: ${String(ssrTemplates.length)} route(s) server-rendered`) +
|
|
340
|
+
'\n');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
catch (e) {
|
|
344
|
+
process.stdout.write(pc.yellow(' ! ') + pc.dim(`SSR dev extraction skipped: ${String(e)}`) + '\n');
|
|
345
|
+
}
|
|
263
346
|
const { startDevServer } = await import('toiljs/devserver');
|
|
264
347
|
const front = await startDevServer({
|
|
265
348
|
root: cfg.root,
|
|
266
349
|
port: cfg.port,
|
|
267
350
|
wasmFile: serverWasmFile(cfg.root),
|
|
351
|
+
coldWasmFile: serverArtifacts(cfg.root).cold,
|
|
352
|
+
nodeMode: cfg.nodeMode,
|
|
353
|
+
daemon: cfg.daemon,
|
|
268
354
|
vite: { host: '127.0.0.1', port: vitePort },
|
|
269
355
|
email: cfg.email ?? undefined,
|
|
356
|
+
ssrTemplates,
|
|
270
357
|
});
|
|
271
358
|
server.httpServer?.once('close', () => {
|
|
272
359
|
void front.close();
|
|
@@ -293,15 +380,20 @@ export async function build(opts = {}) {
|
|
|
293
380
|
if (hasServer && !opts.serverOnly)
|
|
294
381
|
process.stdout.write(pc.dim(' building the server (toilscript)…') + '\n');
|
|
295
382
|
await renderEmails(cfg);
|
|
383
|
+
generate(cfg);
|
|
384
|
+
const priorServerSlots = hasServer ? await extractServerSlots(cfg) : new Map();
|
|
296
385
|
await buildServer(cfg.root);
|
|
297
386
|
if (opts.serverOnly)
|
|
298
387
|
return;
|
|
299
388
|
if (hasServer)
|
|
300
389
|
process.stdout.write(pc.green(' ✓ ') + pc.dim('server built; building the client (vite)…') + '\n');
|
|
301
|
-
generate(cfg);
|
|
302
390
|
await viteBuild(await createViteConfig(cfg));
|
|
303
391
|
await prerenderStaticParams(cfg);
|
|
304
|
-
await extractTemplates(cfg);
|
|
392
|
+
const ssr = await extractTemplates(cfg, 'edge', priorServerSlots);
|
|
393
|
+
if (ssr.serverSlotsChanged) {
|
|
394
|
+
process.stdout.write(pc.dim(' SSR template changed; recompiling the server with the new hash…') + '\n');
|
|
395
|
+
await buildServer(cfg.root);
|
|
396
|
+
}
|
|
305
397
|
}
|
|
306
398
|
export async function start(opts = {}) {
|
|
307
399
|
const cfg = await loadConfig(opts);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type ComponentType, type Context, createElement, type ReactNode } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { renderToString } from 'react-dom/server';
|
|
3
3
|
import { type ResolvedToilConfig } from './config.js';
|
|
4
4
|
export interface RouteRenderInput {
|
|
5
5
|
name: string;
|
|
@@ -12,7 +12,7 @@ export interface RouteRenderInput {
|
|
|
12
12
|
setSsrBuild: (on: boolean) => void;
|
|
13
13
|
shell: string;
|
|
14
14
|
createElement?: typeof createElement;
|
|
15
|
-
|
|
15
|
+
renderToString?: typeof renderToString;
|
|
16
16
|
}
|
|
17
17
|
export interface TemplateArtifacts {
|
|
18
18
|
name: string;
|
|
@@ -28,5 +28,25 @@ export declare function assembleRouteElement(Page: ComponentType, layouts: Compo
|
|
|
28
28
|
export declare function injectIntoShell(shell: string, routeHtml: string): string;
|
|
29
29
|
export declare function extractRouteTemplate(input: RouteRenderInput): TemplateArtifacts;
|
|
30
30
|
export declare function writeTemplateArtifacts(ssrDir: string, art: TemplateArtifacts): void;
|
|
31
|
+
export declare function serverSlotsDir(root: string): string;
|
|
32
|
+
export declare function writeServerSlotsModules(root: string, modules: {
|
|
33
|
+
name: string;
|
|
34
|
+
slotsModule: string;
|
|
35
|
+
}[]): Map<string, string>;
|
|
36
|
+
export declare function extractServerSlots(cfg: ResolvedToilConfig): Promise<Map<string, string>>;
|
|
37
|
+
export interface DevSsrTemplate {
|
|
38
|
+
pattern: string;
|
|
39
|
+
name: string;
|
|
40
|
+
tmpl: Buffer;
|
|
41
|
+
entries: {
|
|
42
|
+
id: number;
|
|
43
|
+
offset: number;
|
|
44
|
+
}[];
|
|
45
|
+
}
|
|
46
|
+
export declare function extractDevSsrTemplates(cfg: ResolvedToilConfig, shell: string): Promise<DevSsrTemplate[]>;
|
|
31
47
|
export declare function routeTemplateName(pattern: string): string;
|
|
32
|
-
export
|
|
48
|
+
export interface ExtractResult {
|
|
49
|
+
readonly generated: string[];
|
|
50
|
+
readonly serverSlotsChanged: boolean;
|
|
51
|
+
}
|
|
52
|
+
export declare function extractTemplates(cfg: ResolvedToilConfig, hostName?: string, priorServerSlots?: Map<string, string>): Promise<ExtractResult>;
|