@plitzi/sdk-server 0.38.7 → 0.38.9
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/CHANGELOG.md +73 -0
- package/README.md +10 -5
- package/dist/core/createServer.notFound.test.d.ts +1 -0
- package/dist/core/oauth/consentPage.js +1 -9
- package/dist/core/unusedPort.d.ts +6 -0
- package/dist/core/unusedPort.js +20 -0
- package/dist/helpers/compress.js +1 -1
- package/dist/helpers/escapeHtml.d.ts +2 -0
- package/dist/helpers/escapeHtml.js +13 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -1
- package/dist/modules/actions/runtime/errors.d.ts +6 -0
- package/dist/modules/actions/runtime/errors.js +7 -1
- package/dist/modules/actions/runtime/projectData.js +1 -1
- package/dist/modules/actions/runtime/runAction.js +2 -2
- package/dist/modules/functions/routes.js +2 -2
- package/dist/modules/project/plugins.d.ts +1 -1
- package/dist/modules/project/plugins.js +2 -2
- package/dist/modules/project/serveProject.js +40 -12
- package/dist/modules/project/spaceFailure.d.ts +8 -0
- package/dist/modules/project/spaceFailure.js +76 -0
- package/dist/modules/runtime/bundle.d.ts +1 -1
- package/dist/modules/runtime/bundle.js +1 -1
- package/dist/modules/runtime/runner.d.ts +1 -0
- package/dist/modules/runtime/runner.js +2 -3
- package/dist/modules/ssr/applySSRResult.d.ts +11 -0
- package/dist/modules/ssr/applySSRResult.js +14 -1
- package/dist/modules/ssr/buildBody.js +2 -0
- package/dist/modules/ssr/handler.js +2 -2
- package/dist/modules/ssr/prepareRender.d.ts +2 -0
- package/dist/modules/ssr/prepareRender.js +9 -3
- package/dist/modules/ssr/streamBody.js +3 -2
- package/package.json +3 -3
- package/skills/plitzi-authoring/SKILL.md +34 -149
- package/skills/plitzi-authoring/recipes/detail-page.ts +4 -2
- package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
- package/skills/plitzi-authoring/reference/accessibility.md +3 -2
- package/skills/plitzi-authoring/reference/authoring-errors.md +4 -0
- package/skills/plitzi-authoring/reference/drawing.md +1 -1
- package/skills/plitzi-authoring/reference/flows.md +6 -4
- package/skills/plitzi-authoring/reference/index.md +33 -0
- package/skills/plitzi-authoring/reference/layouts.md +18 -0
- package/skills/plitzi-authoring/reference/plugins.md +2 -2
- package/skills/plitzi-authoring/reference/recipes.md +25 -0
- package/skills/plitzi-authoring/reference/rules.md +61 -0
- package/skills/plitzi-authoring/reference/snippets-and-export.md +2 -2
- package/skills/plitzi-authoring/reference/structure.md +1 -1
- package/skills/plitzi-authoring/reference/testing.md +1 -1
- package/skills/plitzi-authoring/reference/validation.md +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,78 @@
|
|
|
1
1
|
# @plitzi/sdk-server
|
|
2
2
|
|
|
3
|
+
## 0.38.9
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 64f4cd6: - **`unusedPort()`** (`@plitzi/sdk-server`): a port the system has just handed out, for a test to listen on. The
|
|
8
|
+
package's own tests and `@plitzi/sdk-mcp`'s listened on fixed ports in the ephemeral range (393xx), which any
|
|
9
|
+
outgoing connection of a test running beside them could take: CI failed with `EADDRINUSE` on runs with nothing
|
|
10
|
+
wrong. Every one now listens on a port the system gives it.
|
|
11
|
+
- **Brotli at quality 5** (`@plitzi/sdk-server`): `compression.brotliQuality`, for a page rendered for one request, is
|
|
12
|
+
5 (was 2). At 2 a 220 KB page came out larger than gzip — 36 KB against 31 — and at 5 it is 29 KB, for the CPU gzip
|
|
13
|
+
spends; on half a core ~0.1 ms a page.
|
|
14
|
+
- **`verify` checks the pages for signed-in visitors** (`@plitzi/cli`): a page that sends the browser to sign in is
|
|
15
|
+
checked again signed in as the account `.env` names — `PLITZI_CHECK_USER`, `PLITZI_CHECK_PASSWORD` (the environment
|
|
16
|
+
wins over `.env`). With none named, `verify` says how. `page check --as` reads the password from `.env` too, and
|
|
17
|
+
`page check` takes several pages, in one browser.
|
|
18
|
+
- **An address that shows nothing answers 404** (`@plitzi/sdk-shared`, `@plitzi/plitzi-sdk`, `@plitzi/sdk-server`):
|
|
19
|
+
one no page answers was sent to the home page with a 302 — every typo read as the home page. A page whose slug is
|
|
20
|
+
`'*'` is now the space's page for it, sent with status 404 (a folder may have its own; the deepest wins), and a page
|
|
21
|
+
gated off by a flag shows it too. Never kept in the page cache: an address asked for at random filled it. Only a slug
|
|
22
|
+
that IS `'*'`: a page answering a subtree by its own (`spaces/:spaceId/update/*`) stays a page like any other.
|
|
23
|
+
- **`authorSpace` gives a space with no `'*'` page a plain one** (`@plitzi/sdk-authoring`), in the home page's layout,
|
|
24
|
+
and says so: suggestion `not-found-page`. The project templates carry their own.
|
|
25
|
+
- **`notFound` on a server provider** (`@plitzi/sdk-elements`, `@plitzi/sdk-server`): an expression against its answer
|
|
26
|
+
(`{{ source.found == false }}`); when it is true the page is sent with status 404, rendered as written. Refused on a
|
|
27
|
+
browser provider (`not-found-in-browser`) and when it is not one expression (`not-found-not-a-template`).
|
|
28
|
+
- **`page check` and `verify` know a page sent with 404** (`@plitzi/cli`): it is said, not failed; the space's `'*'`
|
|
29
|
+
page is checked at an address no page has, and fails if it is not sent with 404.
|
|
30
|
+
- **A form field draws no background of its own** (`@plitzi/sdk-elements`): the `<input>` inside a form control's box
|
|
31
|
+
(`field`) took `background-color: inherit`, and painted a translucent box colour a second time — a darker box inside
|
|
32
|
+
the padding (Tremor's search). It is `transparent`: the box shows through it.
|
|
33
|
+
- **`space lint` offers `quiet` only where there is an element to put it on** (`@plitzi/cli`): a suggestion about what
|
|
34
|
+
the space lacks — `not-found-page` — is answered by writing it.
|
|
35
|
+
- **`feedback --previous`** (`@plitzi/cli`): the brief says what to do when an earlier report's link no longer opens —
|
|
36
|
+
say so, and ask the person for the highest id it had.
|
|
37
|
+
- **`start:prod` runs in production by itself** (`@plitzi/cli`): `NODE_ENV=production node …`. Without the variable a
|
|
38
|
+
deployed project was a development server — dev tools on, and every public action answering its `steps` and `trace`
|
|
39
|
+
(each step's result, what the `output` was meant to leave out). `doctor` says a `start:prod` that does not
|
|
40
|
+
(`start-prod-not-production`); `plitzi upgrade --write` rewrites the script.
|
|
41
|
+
- **A project's `ActionRefusal` reaches the flow** (`@plitzi/sdk-server`): a functions bundle carries its own copy of the
|
|
42
|
+
class, so `instanceof` never knew it and the run lost its reason (`{{ x.error }}` empty). Told apart by name now.
|
|
43
|
+
- **Plugins are not remounted after hydration** (`@plitzi/sdk-elements`): the registry wrapped each plugin again when
|
|
44
|
+
the host handed it over again, a new component type every time — the server's HTML thrown away, entrances replayed,
|
|
45
|
+
effects run twice. Each plugin component is wrapped once.
|
|
46
|
+
- **`start:dev` survives a space that does not author** (`@plitzi/sdk-server`): a restart while the space had an error
|
|
47
|
+
died, and fixing the space woke nothing. It comes up answering every page with what is wrong (503), and loads the
|
|
48
|
+
space the first time a save authors.
|
|
49
|
+
- **`page check` sees what is cut off at the screen's edge** (`@plitzi/sdk-authoring`, `@plitzi/cli`): a link or words an
|
|
50
|
+
ancestor hides past the viewport, which no sideways scroll shows (`cut-off`).
|
|
51
|
+
- **`page shot --click <element...> --sheet`** (`@plitzi/cli`): elements clicked in order — a name or a CSS selector,
|
|
52
|
+
inside a plugin too — pictured after each (`--frames`/`--every`), with how much changed, and every picture on one
|
|
53
|
+
contact sheet. The dev tools' badge is no longer in a picture: its shadow root is marked (`@plitzi/sdk-dev-tools`).
|
|
54
|
+
- **`formControl` is optional unless it says `required: true`** (`@plitzi/sdk-elements`), as an HTML field is. A field
|
|
55
|
+
left without `required` in an older space was required, and is not now.
|
|
56
|
+
- **`text({ decorative })`, `list({ label })`** (`@plitzi/sdk-elements`); **`writing-mode`, `text-orientation`** in the
|
|
57
|
+
CSS vocabulary (`@plitzi/sdk-shared`).
|
|
58
|
+
- **`unused-token` reads the plugins' stylesheets** (`@plitzi/sdk-authoring`): `authorSpace`'s `stylesheets`, which
|
|
59
|
+
`projectAuthoring` fills with every `.css` under `src/plugins/` — a token only a plugin used was said to be unused.
|
|
60
|
+
- **`id-taken` on a provider placed on two pages points at the layout they share** (`@plitzi/sdk-authoring`), not at
|
|
61
|
+
`scope()`, which renamed its source and every template reading it.
|
|
62
|
+
- **The welcome template's tokens keep their names** (`@plitzi/sdk-authoring`): `satisfies`, not an annotation, so
|
|
63
|
+
`tokens(variables).card` typechecks as the cheatsheet teaches.
|
|
64
|
+
- Updated dependencies [64f4cd6]
|
|
65
|
+
- @plitzi/sdk-shared@0.38.9
|
|
66
|
+
- @plitzi/plitzi-sdk@0.38.9
|
|
67
|
+
|
|
68
|
+
## 0.38.8
|
|
69
|
+
|
|
70
|
+
### Patch Changes
|
|
71
|
+
|
|
72
|
+
- Updated dependencies [1b45aae]
|
|
73
|
+
- @plitzi/sdk-shared@0.38.8
|
|
74
|
+
- @plitzi/plitzi-sdk@0.38.8
|
|
75
|
+
|
|
3
76
|
## 0.38.7
|
|
4
77
|
|
|
5
78
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -159,7 +159,7 @@ serve it — worth running over anything that arrives as a file.
|
|
|
159
159
|
A space on Plitzi comes out whole, as a project wired to this server: `npx @plitzi/cli create my-site --from <space>`
|
|
160
160
|
writes its pages as authoring code, its actions as `defineAction` code, its functions, the source of its plugins and
|
|
161
161
|
runtime, and its files into `public/` — a draft, or any published snapshot (`--environment`, `--revision`) — and
|
|
162
|
-
`plitzi pull` keeps it in step, and `plitzi push` puts what changed in it back on the space. See
|
|
162
|
+
`plitzi space pull` keeps it in step, and `plitzi space push` puts what changed in it back on the space. See
|
|
163
163
|
`docs/en/projects-from-spaces.md` in the workspace.
|
|
164
164
|
|
|
165
165
|
## Projects
|
|
@@ -221,7 +221,7 @@ What it wires from the project:
|
|
|
221
221
|
- **Its files.** `public/` served as it is, `src/data/` read and never served (`dataDir`), the `kv` in `state/kv.json`
|
|
222
222
|
(`createFileKv`), `PLITZI_SIGNING_SECRET` what `ctx.sign` signs with.
|
|
223
223
|
- **Where it is.** `/health` answers with the space's permanent URL (or the cloud `name`), and `tmp/dev-server.json`
|
|
224
|
-
records the name, the port and the URL for `plitzi check`, `shot` and `visual`. A signal closes it (`closeOnSignals`).
|
|
224
|
+
records the name, the port and the URL for `plitzi page check`, `shot` and `visual`. A signal closes it (`closeOnSignals`).
|
|
225
225
|
|
|
226
226
|
While developing — `NODE_ENV` other than `production` — `devMode` and `devReload` are on, and:
|
|
227
227
|
|
|
@@ -400,6 +400,11 @@ Beside the page adapters it offers `getSchema` and `getStyle`, read from the sam
|
|
|
400
400
|
path, or a function that may answer one — `saveSchema` and `saveStyle`, each writing its document back into the file. So an MCP server
|
|
401
401
|
([`@plitzi/sdk-mcp`](../mcp/README.md#adapters)) can run over it, with a `getGrant` of yours beside it.
|
|
402
402
|
|
|
403
|
+
In a test, listen on `await unusedPort()`: a port the system has just handed out, never a fixed number — one in the
|
|
404
|
+
system's ephemeral range can be taken by any outgoing connection of a test running beside it, and the listen fails with
|
|
405
|
+
`EADDRINUSE` on a run with nothing wrong. (`freePort(8080)` is the development server's: the next free one from a port
|
|
406
|
+
people expect.)
|
|
407
|
+
|
|
403
408
|
### `JsonAdaptersConfig`
|
|
404
409
|
|
|
405
410
|
| Option | Type | Description |
|
|
@@ -541,7 +546,7 @@ createServer({ dataDir: path.join(PROJECT_ROOT, 'src/data'), adapters: { ... } }
|
|
|
541
546
|
```
|
|
542
547
|
|
|
543
548
|
The data is read through one lookup, `action.lookups.getData(spaceId, at)` — the space's own data as its files' text by
|
|
544
|
-
path, as of the version being rendered or run: what a cloud deployment keeps for a space (`plitzi push`). Given none,
|
|
549
|
+
path, as of the version being rendered or run: what a cloud deployment keeps for a space (`plitzi space push`). Given none,
|
|
545
550
|
`createServer` derives it from `dataDir` (its `.json` files, read again once they change), so the folder and the
|
|
546
551
|
platform are read the same way.
|
|
547
552
|
|
|
@@ -573,7 +578,7 @@ createServer({ compression: false, adapters });
|
|
|
573
578
|
|---|---|---|---|
|
|
574
579
|
| `encodings` | `('br' \| 'gzip')[]` | `['br', 'gzip']` | What this server offers, most preferred first; the first one the client accepts wins. `[]` disables compression. |
|
|
575
580
|
| `threshold` | `number` | `1024` | Responses smaller than this many bytes go out uncompressed. |
|
|
576
|
-
| `brotliQuality` | `number` | `
|
|
581
|
+
| `brotliQuality` | `number` | `5` | Brotli quality, 0–11, for a body compressed on every request (a page rendered for that request alone). The first quality under gzip's size for the CPU gzip spends: at 2 a 220 KB page came out larger than gzip (36 KB against 31; 29 at 5). On half a core it costs ~0.1 ms a page — 5–8% of the pages a second of a 6 KB page. |
|
|
577
582
|
| `keptBrotliQuality` | `number` | `6` | Brotli quality for a body compressed once and kept: a cached page, a static file such as the SDK bundle. Paid once; the bundle comes out 10% smaller than at 4, for the same memory. 9 needs ~40 MB more to compress the bundle, which a 128 MB server does not have. |
|
|
578
583
|
| `gzipLevel` | `number` | `6` | Gzip level, 0–9. |
|
|
579
584
|
|
|
@@ -642,7 +647,7 @@ All fields are optional and combined with AND logic. Calling `invalidate()` with
|
|
|
642
647
|
|
|
643
648
|
Plugins are React component bundles that extend the Plitzi schema renderer. They are defined globally at server config level, compiled or copied on first use, and cached on disk for one week. The `getSpaceDeployment` adapter controls which plugins each space gets access to via `pluginNames`.
|
|
644
649
|
|
|
645
|
-
A compiled plugin is one module and its stylesheet, built as `plitzi pack
|
|
650
|
+
A compiled plugin is one module and its stylesheet, built as `plitzi plugin pack` builds it: React and the SDK kept out, and everything else inside — images and fonts as data URIs, and a file imported whole the way Vite imports it (`worker.js?raw` for its text, `engine.wasm?inline` for a data URI).
|
|
646
651
|
|
|
647
652
|
Whatever stylesheet the server writes for a plugin — compiled, copied or downloaded — goes into the `plitzi-sdk-plugin` cascade layer (`inPluginLayer` from `@plitzi/sdk-shared/style`), below the space's own: a space's classes and `customCss` win over what the plugin shipped, whatever the specificity. A `css` given as a web path (`/builder-assets/…`) is linked as it is.
|
|
648
653
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,13 +1,5 @@
|
|
|
1
|
+
import { escapeHtml } from "../../helpers/escapeHtml.js";
|
|
1
2
|
//#region src/core/oauth/consentPage.ts
|
|
2
|
-
var escapeHtml = (value) => value.replace(/[&<>"']/gu, (character) => {
|
|
3
|
-
switch (character) {
|
|
4
|
-
case "&": return "&";
|
|
5
|
-
case "<": return "<";
|
|
6
|
-
case ">": return ">";
|
|
7
|
-
case "\"": return """;
|
|
8
|
-
default: return "'";
|
|
9
|
-
}
|
|
10
|
-
});
|
|
11
3
|
var STYLES = `
|
|
12
4
|
:root { color-scheme: light dark; --bg: #f6f7f9; --panel: #ffffff; --ink: #16181d; --muted: #6b7280;
|
|
13
5
|
--line: #e2e5ea; --accent: #2563eb; --danger: #b91c1c; }
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A port the system has just handed out and nobody holds — what a test listens on. Never a fixed number: one in the
|
|
3
|
+
* system's ephemeral range (above 32768 on Linux) can be taken by any outgoing connection of a test running beside it,
|
|
4
|
+
* and the listen fails with `EADDRINUSE` on a run that has nothing wrong with it.
|
|
5
|
+
*/
|
|
6
|
+
export declare const unusedPort: (host?: string) => Promise<number>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { createServer } from "node:net";
|
|
2
|
+
//#region src/core/unusedPort.ts
|
|
3
|
+
/**
|
|
4
|
+
* A port the system has just handed out and nobody holds — what a test listens on. Never a fixed number: one in the
|
|
5
|
+
* system's ephemeral range (above 32768 on Linux) can be taken by any outgoing connection of a test running beside it,
|
|
6
|
+
* and the listen fails with `EADDRINUSE` on a run that has nothing wrong with it.
|
|
7
|
+
*/
|
|
8
|
+
var unusedPort = (host = "127.0.0.1") => new Promise((resolve, reject) => {
|
|
9
|
+
const probe = createServer();
|
|
10
|
+
probe.once("error", reject);
|
|
11
|
+
probe.listen(0, host, () => {
|
|
12
|
+
const address = probe.address();
|
|
13
|
+
probe.close(() => {
|
|
14
|
+
if (typeof address === "object" && address) resolve(address.port);
|
|
15
|
+
else reject(/* @__PURE__ */ new Error(`The system gave no port on ${host}`));
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
//#endregion
|
|
20
|
+
export { unusedPort };
|
package/dist/helpers/compress.js
CHANGED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region src/helpers/escapeHtml.ts
|
|
2
|
+
/** Text set inside HTML as text: never markup, whatever it holds. */
|
|
3
|
+
var escapeHtml = (value) => value.replace(/[&<>"']/gu, (character) => {
|
|
4
|
+
switch (character) {
|
|
5
|
+
case "&": return "&";
|
|
6
|
+
case "<": return "<";
|
|
7
|
+
case ">": return ">";
|
|
8
|
+
case "\"": return """;
|
|
9
|
+
default: return "'";
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
//#endregion
|
|
13
|
+
export { escapeHtml };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { createServer, resolveServices } from './core/createServer';
|
|
|
2
2
|
export { closeOnSignals } from './core/server/closeOnSignals';
|
|
3
3
|
export { loadFunctions, loadFunctionsSource, readFunctionsDir } from './modules/functions/load';
|
|
4
4
|
export { freePort } from './core/freePort';
|
|
5
|
+
export { unusedPort } from './core/unusedPort';
|
|
5
6
|
export { registerHealthCheck, buildHealthPayload } from './core/health';
|
|
6
7
|
export { consoleLogger, isLogged, logLevelOf, renderLogEvent, serverLog } from './helpers/serverLog';
|
|
7
8
|
export type { HealthCheckApp, HealthIdentity } from './core/health';
|
package/dist/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { buildHealthPayload, registerHealthCheck } from "./core/health.js";
|
|
|
11
11
|
import { createServer } from "./core/createServer.js";
|
|
12
12
|
import { closeOnSignals } from "./core/server/closeOnSignals.js";
|
|
13
13
|
import { freePort } from "./core/freePort.js";
|
|
14
|
+
import { unusedPort } from "./core/unusedPort.js";
|
|
14
15
|
import { createCloudAdapters } from "./adapters/cloudAdapters.js";
|
|
15
16
|
import { createJsonAdapters } from "./adapters/jsonAdapters.js";
|
|
16
17
|
import { createAuthAdapters } from "./adapters/authAdapters.js";
|
|
@@ -19,4 +20,4 @@ import { authoringPreview, fixedSpace, presentsCredential, verifiedDomain, wildc
|
|
|
19
20
|
import { createSpaceAdapters } from "./adapters/space/index.js";
|
|
20
21
|
import { createLocalFontStore, hostedPathsOf } from "./core/fontStore.js";
|
|
21
22
|
import { createRedisPubSub } from "./modules/realtime/redisPubSub.js";
|
|
22
|
-
export { PREVIEW_TOKEN_PARAM, authoringPreview, buildHealthPayload, closeOnSignals, consoleLogger, createAuthAdapters, createCloudAdapters, createConnectorResolver, createJsonAdapters, createLocalFontStore, createMemoryCache, createMemoryDraftStore, createMemoryPubSub, createRedisPubSub, createServer, createSpaceAdapters, fetchConnectorRecords, fixedSpace, freePort, hostedPathsOf, isLogged, loadFunctions, loadFunctionsSource, logLevelOf, presentsCredential, readFunctionsDir, rebaseMedia, refuse, registerHealthCheck, renderLogEvent, resolveRscData, resolveServices, serverLog, takeDraftOverride, verifiedDomain, wildcardSubdomain, writeConnectorRecord };
|
|
23
|
+
export { PREVIEW_TOKEN_PARAM, authoringPreview, buildHealthPayload, closeOnSignals, consoleLogger, createAuthAdapters, createCloudAdapters, createConnectorResolver, createJsonAdapters, createLocalFontStore, createMemoryCache, createMemoryDraftStore, createMemoryPubSub, createRedisPubSub, createServer, createSpaceAdapters, fetchConnectorRecords, fixedSpace, freePort, hostedPathsOf, isLogged, loadFunctions, loadFunctionsSource, logLevelOf, presentsCredential, readFunctionsDir, rebaseMedia, refuse, registerHealthCheck, renderLogEvent, resolveRscData, resolveServices, serverLog, takeDraftOverride, unusedPort, verifiedDomain, wildcardSubdomain, writeConnectorRecord };
|
|
@@ -26,3 +26,9 @@ export declare class ActionRunError extends Error {
|
|
|
26
26
|
export declare class ActionRefusal extends Error {
|
|
27
27
|
constructor(message: string);
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Whether an error is a refusal — told apart by its NAME, never by `instanceof`: a functions bundle carries its own copy
|
|
31
|
+
* of the class (`functions/build.ts`), so a project's code loaded from one throws a refusal this class never made, and
|
|
32
|
+
* its reason was dropped on the way to the page.
|
|
33
|
+
*/
|
|
34
|
+
export declare const isActionRefusal: (error: unknown) => error is Error;
|
|
@@ -33,5 +33,11 @@ var ActionRefusal = class extends Error {
|
|
|
33
33
|
this.name = "ActionRefusal";
|
|
34
34
|
}
|
|
35
35
|
};
|
|
36
|
+
/**
|
|
37
|
+
* Whether an error is a refusal — told apart by its NAME, never by `instanceof`: a functions bundle carries its own copy
|
|
38
|
+
* of the class (`functions/build.ts`), so a project's code loaded from one throws a refusal this class never made, and
|
|
39
|
+
* its reason was dropped on the way to the page.
|
|
40
|
+
*/
|
|
41
|
+
var isActionRefusal = (error) => error instanceof Error && error.name === "ActionRefusal";
|
|
36
42
|
//#endregion
|
|
37
|
-
export { ActionRefusal, ActionRunError };
|
|
43
|
+
export { ActionRefusal, ActionRunError, isActionRefusal };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
//#region src/modules/actions/runtime/projectData.ts
|
|
4
|
-
/** What a project keeps in its data folder: JSON, the only files `plitzi push` sends as the space's data. */
|
|
4
|
+
/** What a project keeps in its data folder: JSON, the only files `plitzi space push` sends as the space's data. */
|
|
5
5
|
var DATA_EXTENSION = ".json";
|
|
6
6
|
/**
|
|
7
7
|
* A data folder as the lookup a deployment would otherwise write — what a self-hosted server derives from `dataDir`, so
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { serverLog } from "../../../helpers/serverLog.js";
|
|
2
|
-
import {
|
|
2
|
+
import { ActionRunError, isActionRefusal } from "./errors.js";
|
|
3
3
|
import { runCancelKey } from "./guards.js";
|
|
4
4
|
import { createKvStore } from "./kvStore.js";
|
|
5
5
|
import { resolveLimits } from "./limits.js";
|
|
@@ -464,7 +464,7 @@ var createActionRunner = (config, registryFor, baseFetch = fetch) => {
|
|
|
464
464
|
});
|
|
465
465
|
} else {
|
|
466
466
|
failure = error instanceof Error ? error.message : String(error);
|
|
467
|
-
if (error
|
|
467
|
+
if (isActionRefusal(error)) refusal = redact(error.message);
|
|
468
468
|
trace.push({
|
|
469
469
|
node: errorNode("error"),
|
|
470
470
|
status: "failed",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { serverLog } from "../../helpers/serverLog.js";
|
|
2
|
-
import {
|
|
2
|
+
import { isActionRefusal } from "../actions/runtime/errors.js";
|
|
3
3
|
import { pluginOfPath } from "./scope.js";
|
|
4
4
|
import { functionContextFor } from "./context.js";
|
|
5
5
|
import { functionLimitsFor } from "./config.js";
|
|
@@ -69,7 +69,7 @@ var withoutCookies = (response) => {
|
|
|
69
69
|
* of anything else the visitor gets that it failed, and the server's log gets why.
|
|
70
70
|
*/
|
|
71
71
|
var failed = (visit, key, error) => {
|
|
72
|
-
if (error
|
|
72
|
+
if (isActionRefusal(error) || error instanceof FunctionFailure && error.reason === "refused") return Response.json({ error: error.message }, { status: 400 });
|
|
73
73
|
serverLog.error("Functions", `route ${key} of space ${String(visit.spaceId)} failed`, error);
|
|
74
74
|
const stopped = error instanceof FunctionFailure && error.reason !== "error";
|
|
75
75
|
return Response.json({ error: stopped ? "This route ran out of what it may spend" : "This route failed" }, { status: stopped ? 503 : 500 });
|
|
@@ -23,7 +23,7 @@ export declare const projectPlugins: (root: string) => Promise<ProjectPlugins>;
|
|
|
23
23
|
/**
|
|
24
24
|
* A plugin edited while developing is built again by the server, and the open pages swap it where it is drawn — the
|
|
25
25
|
* rest of the page, its state included, stays as it was (`devReload`). Its server half (`functions/`) is loaded again
|
|
26
|
-
* here, in place. A plugin ADDED — a new folder with an `index.ts`, what `plitzi add
|
|
26
|
+
* here, in place. A plugin ADDED — a new folder with an `index.ts`, what `plitzi plugin add` writes — is registered
|
|
27
27
|
* here, and the pages load again to render it; one removed is turned off. A folder the server cannot build, or whose
|
|
28
28
|
* server half has no entry, is said in the terminal as the layout check says it at boot — once, until it changes — and
|
|
29
29
|
* the server goes on. Answers what stops watching.
|
|
@@ -39,7 +39,7 @@ var folderFunctions = async (root, folder) => {
|
|
|
39
39
|
const functions = path.join(root, PLUGINS_DIR, folder, PLUGIN_FUNCTIONS_DIR);
|
|
40
40
|
return existsSync(path.join(functions, "index.ts")) ? (await loadFunctions(functions)).at(0) : void 0;
|
|
41
41
|
};
|
|
42
|
-
/** A plugin of `vendor/plugins`, from the manifest `plitzi pack
|
|
42
|
+
/** A plugin of `vendor/plugins`, from the manifest `plitzi plugin pack` wrote beside its bundle when it was published. */
|
|
43
43
|
var builtPlugin = (root, type) => {
|
|
44
44
|
const read = readVendorPlugin(root, type);
|
|
45
45
|
if ("problem" in read) throw new Error(read.problem.message);
|
|
@@ -88,7 +88,7 @@ var projectPlugins = async (root) => {
|
|
|
88
88
|
/**
|
|
89
89
|
* A plugin edited while developing is built again by the server, and the open pages swap it where it is drawn — the
|
|
90
90
|
* rest of the page, its state included, stays as it was (`devReload`). Its server half (`functions/`) is loaded again
|
|
91
|
-
* here, in place. A plugin ADDED — a new folder with an `index.ts`, what `plitzi add
|
|
91
|
+
* here, in place. A plugin ADDED — a new folder with an `index.ts`, what `plitzi plugin add` writes — is registered
|
|
92
92
|
* here, and the pages load again to render it; one removed is turned off. A folder the server cannot build, or whose
|
|
93
93
|
* server half has no entry, is said in the terminal as the layout check says it at boot — once, until it changes — and
|
|
94
94
|
* the server goes on. Answers what stops watching.
|
|
@@ -10,12 +10,15 @@ import { createFileKv } from "../actions/runtime/fileKv.js";
|
|
|
10
10
|
import { loadRuntime, loadRuntimeModule } from "../runtime/bundle.js";
|
|
11
11
|
import { serveRuntime } from "../runtime/stages.js";
|
|
12
12
|
import { projectPlugins, watchProjectPlugins } from "./plugins.js";
|
|
13
|
+
import { createSpaceFailure } from "./spaceFailure.js";
|
|
13
14
|
import { watchSpace } from "./watchSpace.js";
|
|
14
15
|
import path from "node:path";
|
|
15
16
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
17
|
import { assertProjectLayout, layoutFindingText } from "@plitzi/sdk-shared/project/layout";
|
|
17
18
|
import { DATA_DIR, DEV_SERVER_FILE, FUNCTIONS_DIR, KV_FILE, PROJECT_TMP, PUBLIC_DIR, RUNTIME_BUNDLE, RUNTIME_ENTRY } from "@plitzi/sdk-shared/project/paths";
|
|
18
19
|
import { projectModule, projectRoot } from "@plitzi/sdk-shared/project/root";
|
|
20
|
+
import { EMPTY_SCHEMA } from "@plitzi/sdk-shared/schema/schemaConstants";
|
|
21
|
+
import { EMPTY_STYLE_SCHEMA } from "@plitzi/sdk-shared/style/styleConstants";
|
|
19
22
|
//#region src/modules/project/serveProject.ts
|
|
20
23
|
/**
|
|
21
24
|
* The errors whose message is the whole report — every problem, where it is and what to write instead — and whose
|
|
@@ -63,14 +66,32 @@ var actionLookups = (actions, connectors) => ({
|
|
|
63
66
|
getConnector: (_spaceId, connectorId) => Promise.resolve(connectors.get(connectorId)),
|
|
64
67
|
listScheduledSpaces: () => Promise.resolve([SPACE_ID])
|
|
65
68
|
});
|
|
66
|
-
/**
|
|
67
|
-
|
|
69
|
+
/**
|
|
70
|
+
* The space this project serves, authored from its own source — or Plitzi's, named for `/health`.
|
|
71
|
+
*
|
|
72
|
+
* While developing, one that does not author is not the end of the server: it comes up saying why (`spaceFailure`),
|
|
73
|
+
* named after the project's folder, and serves the space the first time a save authors. A deployment refuses to start.
|
|
74
|
+
*/
|
|
75
|
+
var spaceToServe = async ({ space, cloud }, developing, root) => {
|
|
68
76
|
if (!space) return { name: cloud.name };
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
try {
|
|
78
|
+
const authored = await space();
|
|
79
|
+
return {
|
|
80
|
+
space: authored,
|
|
81
|
+
name: authored.schema.definition.permanentUrl
|
|
82
|
+
};
|
|
83
|
+
} catch (error) {
|
|
84
|
+
if (!developing) throw error;
|
|
85
|
+
return {
|
|
86
|
+
failure: isReport(error) ? error.message : error instanceof Error ? error.stack ?? error.message : String(error),
|
|
87
|
+
name: path.basename(root)
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
/** What is served of a space that has not authored yet: nothing — every page answers with why (`spaceFailure`). */
|
|
92
|
+
var NOTHING_AUTHORED = {
|
|
93
|
+
schema: EMPTY_SCHEMA.schema,
|
|
94
|
+
style: EMPTY_STYLE_SCHEMA
|
|
74
95
|
};
|
|
75
96
|
var startProject = async (options) => {
|
|
76
97
|
const { actions, connectors = /* @__PURE__ */ new Map(), serverOptions = {} } = options;
|
|
@@ -85,16 +106,22 @@ var startProject = async (options) => {
|
|
|
85
106
|
const port = process.env.PORT ? Number(process.env.PORT) : developing ? await freePort(8080, host) : 8080;
|
|
86
107
|
/** Where people reach this server: `PUBLIC_URL` behind a proxy, its own address otherwise. */
|
|
87
108
|
const publicUrl = (process.env.PUBLIC_URL ?? `http://127.0.0.1:${String(port)}`).replace(/\/+$/, "");
|
|
88
|
-
const { space, name } = await spaceToServe(options);
|
|
109
|
+
const { space, failure, name } = await spaceToServe(options, developing, root);
|
|
89
110
|
const plugins = await projectPlugins(root);
|
|
90
|
-
const held = space
|
|
111
|
+
const held = options.space ? { ...space ? { documents: {
|
|
91
112
|
schema: space.schema,
|
|
92
113
|
style: space.style
|
|
93
|
-
} };
|
|
114
|
+
} } : {} } : void 0;
|
|
115
|
+
const spaceFailure = createSpaceFailure();
|
|
116
|
+
if (failure !== void 0) {
|
|
117
|
+
spaceFailure.fail(failure);
|
|
118
|
+
console.error(`[author] ${failure}`);
|
|
119
|
+
console.error("[author] The server is up: fix the space and save, and the page loads it.");
|
|
120
|
+
}
|
|
94
121
|
const speaks = !isFleetWorker();
|
|
95
122
|
for (const warning of speaks ? space?.warnings ?? [] : []) console.warn(`[author] ${warning.message}`);
|
|
96
123
|
const adapters = held ? createJsonAdapters({
|
|
97
|
-
offlineData: () => held.documents,
|
|
124
|
+
offlineData: () => held.documents ?? NOTHING_AUTHORED,
|
|
98
125
|
deployment: {
|
|
99
126
|
spaceId: SPACE_ID,
|
|
100
127
|
environment: "main",
|
|
@@ -132,7 +159,7 @@ var startProject = async (options) => {
|
|
|
132
159
|
...serverOptions.action,
|
|
133
160
|
...actions ? { lookups: actionLookups(actions, connectors) } : {}
|
|
134
161
|
}
|
|
135
|
-
}, { preAuth: runtime ? [runtime.stage] : [] });
|
|
162
|
+
}, { preAuth: [spaceFailure.stage, ...runtime ? [runtime.stage] : []] });
|
|
136
163
|
server.listen(port, host);
|
|
137
164
|
const url = `http://127.0.0.1:${String(port)}`;
|
|
138
165
|
if (speaks) {
|
|
@@ -146,6 +173,7 @@ var startProject = async (options) => {
|
|
|
146
173
|
}
|
|
147
174
|
const watching = developing ? [...held ? [watchSpace(root, (authored) => {
|
|
148
175
|
held.documents = authored;
|
|
176
|
+
spaceFailure.clear();
|
|
149
177
|
server.reloadPages();
|
|
150
178
|
})] : [], watchProjectPlugins(root, server, plugins)] : [];
|
|
151
179
|
const close = async () => {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { Stage } from '../../core/http/types';
|
|
2
|
+
export declare const createSpaceFailure: () => {
|
|
3
|
+
stage: Stage;
|
|
4
|
+
/** The space did not author: every page says why, until it does. */
|
|
5
|
+
fail: (message: string) => void;
|
|
6
|
+
/** It authored: the pages are the space's again. */
|
|
7
|
+
clear: () => void;
|
|
8
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { DEV_RELOAD_PATH } from "../../core/http/stages/devReload.js";
|
|
2
|
+
import { escapeHtml } from "../../helpers/escapeHtml.js";
|
|
3
|
+
//#region src/modules/project/spaceFailure.ts
|
|
4
|
+
/**
|
|
5
|
+
* The page a project's server answers with while its space does not author — while developing only.
|
|
6
|
+
*
|
|
7
|
+
* A `start:dev` that restarted on a save to the server's code (`src/functions/`, `src/config/`…) while the space had an
|
|
8
|
+
* error of its own died on it: Node then waits for a change to the server's code, never to `src/space/`, so fixing the
|
|
9
|
+
* space woke nothing and the page was gone with no word in the browser. Up instead, it answers every page with what is
|
|
10
|
+
* wrong, and loads again the moment a save authors — the same reload a page showing the space waits for — or when the
|
|
11
|
+
* server it reached restarts.
|
|
12
|
+
*/
|
|
13
|
+
var pageOf = (message) => `<!doctype html>
|
|
14
|
+
<html lang="en">
|
|
15
|
+
<head>
|
|
16
|
+
<meta charset="utf-8">
|
|
17
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
18
|
+
<title>The space does not author</title>
|
|
19
|
+
<style>
|
|
20
|
+
:root { color-scheme: light dark; }
|
|
21
|
+
body { margin: 0; padding: 48px 24px; font: 15px/1.5 ui-sans-serif, system-ui, sans-serif; }
|
|
22
|
+
main { max-width: 860px; margin: 0 auto; display: flex; flex-direction: column; gap: 12px; }
|
|
23
|
+
pre { margin: 0; padding: 16px; border-radius: 8px; background: color-mix(in srgb, currentColor 8%, transparent);
|
|
24
|
+
white-space: pre-wrap; overflow-wrap: anywhere; font: 13px/1.5 ui-monospace, monospace; }
|
|
25
|
+
</style>
|
|
26
|
+
</head>
|
|
27
|
+
<body>
|
|
28
|
+
<main>
|
|
29
|
+
<h1>The space does not author</h1>
|
|
30
|
+
<p>Fix it and save: this page loads again the moment it authors. The server is still running.</p>
|
|
31
|
+
<pre>${escapeHtml(message)}</pre>
|
|
32
|
+
</main>
|
|
33
|
+
<script>
|
|
34
|
+
(() => {
|
|
35
|
+
let boot;
|
|
36
|
+
const events = new EventSource(${JSON.stringify(DEV_RELOAD_PATH)});
|
|
37
|
+
events.addEventListener('reload', () => location.reload());
|
|
38
|
+
events.addEventListener('hello', event => {
|
|
39
|
+
const next = JSON.parse(event.data).boot;
|
|
40
|
+
if (boot !== undefined && next !== boot) {
|
|
41
|
+
location.reload();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
boot = next;
|
|
45
|
+
});
|
|
46
|
+
})();
|
|
47
|
+
<\/script>
|
|
48
|
+
</body>
|
|
49
|
+
</html>
|
|
50
|
+
`;
|
|
51
|
+
/** A request for a page, as a browser asks for one — not for the server's data, its assets or its reload stream. */
|
|
52
|
+
var asksForPage = (method, accept) => method === "GET" && [accept].flat().some((value) => value?.includes("text/html"));
|
|
53
|
+
var createSpaceFailure = () => {
|
|
54
|
+
let failure;
|
|
55
|
+
const stage = (ctx) => {
|
|
56
|
+
if (failure === void 0 || !asksForPage(ctx.req.method, ctx.req.headers.accept)) return Promise.resolve(false);
|
|
57
|
+
ctx.res.setStatus(503);
|
|
58
|
+
ctx.res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
59
|
+
ctx.res.setHeader("Cache-Control", "no-store");
|
|
60
|
+
ctx.res.send(pageOf(failure));
|
|
61
|
+
return Promise.resolve(true);
|
|
62
|
+
};
|
|
63
|
+
return {
|
|
64
|
+
stage,
|
|
65
|
+
/** The space did not author: every page says why, until it does. */
|
|
66
|
+
fail: (message) => {
|
|
67
|
+
failure = message;
|
|
68
|
+
},
|
|
69
|
+
/** It authored: the pages are the space's again. */
|
|
70
|
+
clear: () => {
|
|
71
|
+
failure = void 0;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { createSpaceFailure };
|
|
@@ -19,7 +19,7 @@ export declare const inspectRuntime: (bytes: Uint8Array) => {
|
|
|
19
19
|
};
|
|
20
20
|
/**
|
|
21
21
|
* A project's runtime module, as a server of its own runs it — `src/runtime/index.ts`, or what `build` compiled it to —
|
|
22
|
-
* or nothing when the project has none: a project starts one by writing it (`plitzi add
|
|
22
|
+
* or nothing when the project has none: a project starts one by writing it (`plitzi runtime add`), with nothing to wire.
|
|
23
23
|
*/
|
|
24
24
|
export declare const loadRuntimeModule: (file: string) => Promise<SpaceRuntime | undefined>;
|
|
25
25
|
/**
|
|
@@ -93,7 +93,7 @@ var runtimeOf = (loaded) => {
|
|
|
93
93
|
};
|
|
94
94
|
/**
|
|
95
95
|
* A project's runtime module, as a server of its own runs it — `src/runtime/index.ts`, or what `build` compiled it to —
|
|
96
|
-
* or nothing when the project has none: a project starts one by writing it (`plitzi add
|
|
96
|
+
* or nothing when the project has none: a project starts one by writing it (`plitzi runtime add`), with nothing to wire.
|
|
97
97
|
*/
|
|
98
98
|
var loadRuntimeModule = async (file) => {
|
|
99
99
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FunctionsDefinition } from '../functions/contract';
|
|
2
2
|
import { FunctionRunner } from '../functions/protocol';
|
|
3
|
+
/** A refusal, however it was made: the platform's class, or a bundle's own copy of it — known by its name. */
|
|
3
4
|
/**
|
|
4
5
|
* A runtime's functions as a runner: the same protocol as the sandbox's, answered by code loaded in this process. The
|
|
5
6
|
* driver is the sandbox's own (`createFunctionsDriver`), so a task behaves the same in both — its `ctx` is calls to the
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import { ActionRefusal } from "../actions/runtime/errors.js";
|
|
1
|
+
import { ActionRefusal, isActionRefusal } from "../actions/runtime/errors.js";
|
|
2
2
|
import { changeKv } from "../actions/runtime/kvChange.js";
|
|
3
3
|
import { requestFromWire, responseOf, wireResponseFrom, wireResponseOf } from "../functions/capabilities.js";
|
|
4
4
|
import { FunctionFailure } from "../functions/protocol.js";
|
|
5
5
|
import { createFunctionsDriver, describeFunctions } from "../functions/driver.js";
|
|
6
6
|
//#region src/modules/runtime/runner.ts
|
|
7
7
|
/** A refusal, however it was made: the platform's class, or a bundle's own copy of it — known by its name. */
|
|
8
|
-
var isRefusal = (error) => error instanceof Error && error.name === "ActionRefusal";
|
|
9
8
|
/**
|
|
10
9
|
* A runtime's functions as a runner: the same protocol as the sandbox's, answered by code loaded in this process. The
|
|
11
10
|
* driver is the sandbox's own (`createFunctionsDriver`), so a task behaves the same in both — its `ctx` is calls to the
|
|
@@ -29,7 +28,7 @@ var createRuntimeRunner = (definition = {}) => ({
|
|
|
29
28
|
try {
|
|
30
29
|
return await driver.invoke(definition, invocation);
|
|
31
30
|
} catch (error) {
|
|
32
|
-
if (
|
|
31
|
+
if (isActionRefusal(error)) throw new FunctionFailure("refused", error.message);
|
|
33
32
|
throw error;
|
|
34
33
|
}
|
|
35
34
|
}
|
|
@@ -1,2 +1,13 @@
|
|
|
1
1
|
import { SSRRenderResult, SSRResponseHelpers } from '@plitzi/sdk-shared';
|
|
2
2
|
export declare const applySSRResult: (res: SSRResponseHelpers, result: SSRRenderResult) => boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Whether a render may be kept and served again: only a page that answered 200. A cached page is sent without the
|
|
5
|
+
* status it was rendered with, and a "not found" kept per address would let anybody asking for addresses at random
|
|
6
|
+
* push every real page out of the cache.
|
|
7
|
+
*/
|
|
8
|
+
export declare const isReusable: (result: SSRRenderResult) => boolean;
|
|
9
|
+
/**
|
|
10
|
+
* A page a server provider said shows nothing (`notFound`) answers 404 — unless the render decided something of its
|
|
11
|
+
* own first: a redirect, or a status the navigation set.
|
|
12
|
+
*/
|
|
13
|
+
export declare const withNotFound: (result: SSRRenderResult, notFound: boolean) => void;
|
|
@@ -10,5 +10,18 @@ var applySSRResult = (res, result) => {
|
|
|
10
10
|
if (result.status !== void 0) res.setStatus(result.status);
|
|
11
11
|
return false;
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* Whether a render may be kept and served again: only a page that answered 200. A cached page is sent without the
|
|
15
|
+
* status it was rendered with, and a "not found" kept per address would let anybody asking for addresses at random
|
|
16
|
+
* push every real page out of the cache.
|
|
17
|
+
*/
|
|
18
|
+
var isReusable = (result) => result.status === void 0 || result.status === 200;
|
|
19
|
+
/**
|
|
20
|
+
* A page a server provider said shows nothing (`notFound`) answers 404 — unless the render decided something of its
|
|
21
|
+
* own first: a redirect, or a status the navigation set.
|
|
22
|
+
*/
|
|
23
|
+
var withNotFound = (result, notFound) => {
|
|
24
|
+
if (notFound && result.redirect === void 0 && result.status === void 0) result.status = 404;
|
|
25
|
+
};
|
|
13
26
|
//#endregion
|
|
14
|
-
export { applySSRResult };
|
|
27
|
+
export { applySSRResult, isReusable, withNotFound };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { withNotFound } from "./applySSRResult.js";
|
|
1
2
|
import Component from "./Component.js";
|
|
2
3
|
import { prepareRender } from "./prepareRender.js";
|
|
3
4
|
import { renderToString } from "react-dom/server";
|
|
@@ -12,6 +13,7 @@ var buildBody = async (req, config, spaceId, environment, revision, renderFn, pl
|
|
|
12
13
|
ssrResult: result
|
|
13
14
|
})).trim();
|
|
14
15
|
metrics?.record("react", Math.round(performance.now() - reactStart));
|
|
16
|
+
withNotFound(result, prep.notFound);
|
|
15
17
|
if (result.redirect !== void 0) return {
|
|
16
18
|
result,
|
|
17
19
|
cacheable: prep.cacheable
|