@plitzi/sdk-server 0.38.4 → 0.38.5
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 +333 -0
- package/README.md +126 -17
- package/dist/actions.d.ts +8 -5
- package/dist/actions.js +3 -1
- package/dist/adapters/jsonAdapters.d.ts +1 -1
- package/dist/adapters/jsonAdapters.js +16 -3
- package/dist/core/configSeam.js +20 -4
- package/dist/core/createServer.js +3 -2
- package/dist/core/handlers/socialRouteHandlers.js +6 -1
- package/dist/core/http/stages/devReload.d.ts +11 -0
- package/dist/core/http/stages/devReload.js +17 -0
- package/dist/core/server/baseServer.d.ts +2 -0
- package/dist/core/server/baseServer.js +3 -0
- package/dist/core/server/pageServer.js +18 -0
- package/dist/core/services/registry.js +2 -0
- package/dist/core/services/rsc.js +1 -1
- package/dist/core/services/spaceDocument.d.ts +2 -0
- package/dist/core/services/spaceDocument.js +11 -0
- package/dist/functions-api.d.ts +6 -1
- package/dist/functionsRunner.d.ts +5 -0
- package/dist/functionsRunner.js +2 -1
- package/dist/helpers/hydrationPayload.d.ts +25 -4
- package/dist/helpers/hydrationPayload.js +36 -30
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/modules/actions/index.d.ts +11 -2
- package/dist/modules/actions/index.js +29 -7
- package/dist/modules/actions/runtime/fileKv.d.ts +28 -0
- package/dist/modules/actions/runtime/fileKv.js +95 -0
- package/dist/modules/actions/runtime/fileKv.test.d.ts +1 -0
- package/dist/modules/actions/runtime/memoryKv.d.ts +23 -0
- package/dist/modules/actions/runtime/memoryKv.js +39 -24
- package/dist/modules/actions/runtime/namespaceKv.d.ts +2 -0
- package/dist/modules/actions/runtime/namespaceKv.js +5 -3
- package/dist/modules/actions/runtime/renderResolver.d.ts +3 -2
- package/dist/modules/actions/runtime/renderResolver.js +13 -9
- package/dist/modules/actions/runtime/renderShare.d.ts +7 -24
- package/dist/modules/actions/runtime/renderShare.js +47 -13
- package/dist/modules/actions/runtime/runAction.js +15 -1
- package/dist/modules/actions/runtime/scope.js +16 -1
- package/dist/modules/actions/runtime/signing.js +4 -1
- package/dist/modules/actions/runtime/taskContext.d.ts +7 -1
- package/dist/modules/actions/runtime/taskContext.js +6 -2
- package/dist/modules/actions/tasks/registry.d.ts +3 -1
- package/dist/modules/actions/tasks/registry.js +15 -1
- package/dist/modules/actions/types.d.ts +15 -2
- package/dist/modules/functions/config.d.ts +6 -0
- package/dist/modules/functions/context.d.ts +4 -2
- package/dist/modules/functions/context.js +43 -21
- package/dist/modules/functions/load.d.ts +8 -1
- package/dist/modules/functions/load.js +8 -4
- package/dist/modules/functions/manifest.d.ts +4 -1
- package/dist/modules/functions/manifest.js +14 -2
- package/dist/modules/functions/native.d.ts +2 -1
- package/dist/modules/functions/native.js +2 -2
- package/dist/modules/functions/plugins.test.d.ts +1 -0
- package/dist/modules/functions/routes.d.ts +8 -3
- package/dist/modules/functions/routes.js +41 -21
- package/dist/modules/functions/scope.d.ts +30 -0
- package/dist/modules/functions/scope.js +28 -0
- package/dist/modules/functions/space.d.ts +10 -5
- package/dist/modules/functions/space.js +37 -15
- package/dist/modules/rsc/connectorRscData.d.ts +3 -1
- package/dist/modules/rsc/connectorRscData.js +8 -4
- package/dist/modules/rsc/handler.d.ts +3 -16
- package/dist/modules/rsc/handler.js +10 -4
- package/dist/modules/rsc/publicFileResolver.d.ts +15 -12
- package/dist/modules/rsc/publicFileResolver.js +56 -24
- package/dist/modules/rsc/resolveRscData.d.ts +3 -1
- package/dist/modules/rsc/resolveRscData.js +8 -5
- package/dist/modules/runtime/bundle.d.ts +5 -0
- package/dist/modules/runtime/bundle.js +20 -5
- package/dist/modules/sqlite/kv.d.ts +34 -0
- package/dist/modules/sqlite/kv.js +99 -0
- package/dist/modules/sqlite/kv.test.d.ts +1 -0
- package/dist/modules/ssr/prepareRender.js +45 -6
- package/dist/modules/ssr/spaceDocument.d.ts +25 -0
- package/dist/modules/ssr/spaceDocument.js +72 -0
- package/dist/modules/ssr/spaceDocument.test.d.ts +1 -0
- package/dist/modules/ssr/testing/offlineData.d.ts +5 -0
- package/dist/modules/ssr/views/template.ejs +122 -20
- package/dist/plugins/compile.js +6 -21
- package/dist/plugins/compile.test.d.ts +1 -0
- package/dist/plugins/copy.d.ts +5 -0
- package/dist/plugins/copy.js +16 -1
- package/dist/plugins/copy.test.d.ts +1 -0
- package/dist/plugins/manager.d.ts +13 -0
- package/dist/plugins/manager.js +40 -11
- package/dist/plugins/watch.d.ts +12 -0
- package/dist/plugins/watch.js +79 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +2 -2
- package/dist/sqlite.d.ts +17 -0
- package/dist/sqlite.js +2 -0
- package/package.json +8 -3
- package/skills/plitzi-authoring/CHEATSHEET.md +4 -2
- package/skills/plitzi-authoring/SKILL.md +20 -19
- package/skills/plitzi-authoring/recipes/plugin.ts +1 -1
- package/skills/plitzi-authoring/recipes/server-data.ts +3 -3
- package/skills/plitzi-authoring/recipes/show-data.ts +3 -2
- package/skills/plitzi-authoring/recipes/typed-data.ts +1 -1
- package/skills/plitzi-authoring/reference/authoring-errors.md +8 -2
- package/skills/plitzi-authoring/reference/colours-and-motion.md +3 -1
- package/skills/plitzi-authoring/reference/data-and-visibility.md +24 -22
- package/skills/plitzi-authoring/reference/drawing.md +24 -0
- package/skills/plitzi-authoring/reference/efficiency.md +19 -2
- package/skills/plitzi-authoring/reference/flows.md +3 -0
- package/skills/plitzi-authoring/reference/layouts.md +8 -23
- package/skills/plitzi-authoring/reference/performance.md +2 -2
- package/skills/plitzi-authoring/reference/plugins.md +51 -38
- package/skills/plitzi-authoring/reference/review-checklist.md +2 -0
- package/skills/plitzi-authoring/reference/structure.md +32 -1
- package/skills/plitzi-authoring/reference/templates.md +5 -2
- package/skills/plitzi-authoring/reference/testing.md +9 -2
- package/skills/plitzi-authoring/reference/typed-sources.md +3 -1
- package/skills/plitzi-authoring/reference/validation.md +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createActionResolver } from "../actions/runtime/renderResolver.js";
|
|
2
2
|
import { createConnectorResolver } from "../connectors/resolver.js";
|
|
3
|
-
import { publicFileResolver } from "./publicFileResolver.js";
|
|
3
|
+
import { dataFileResolver, dataLookupResolver, publicFileResolver } from "./publicFileResolver.js";
|
|
4
4
|
import { resolveRscData } from "./resolveRscData.js";
|
|
5
5
|
//#region src/modules/rsc/connectorRscData.ts
|
|
6
6
|
/**
|
|
@@ -18,10 +18,12 @@ import { resolveRscData } from "./resolveRscData.js";
|
|
|
18
18
|
*
|
|
19
19
|
* A deployment that resolves server elements some other way still supplies its own `getRscData`, and that one wins.
|
|
20
20
|
*/
|
|
21
|
-
var connectorRscData = ({ connectors, actions, publicDir, elementTimeoutMs }) => {
|
|
21
|
+
var connectorRscData = ({ connectors, actions, publicDir, dataDir, elementTimeoutMs }) => {
|
|
22
22
|
const resolveConnector = connectors ? createConnectorResolver(connectors) : void 0;
|
|
23
23
|
const resolveAction = actions ? createActionResolver(actions.lookups, actions.module) : void 0;
|
|
24
24
|
const resolvePublicFile = publicDir ? publicFileResolver(publicDir) : void 0;
|
|
25
|
+
const dataLookup = actions?.lookups.getData;
|
|
26
|
+
const resolveDataFile = dataDir ? dataFileResolver(dataDir) : dataLookup ? dataLookupResolver((spaceId, at) => dataLookup(spaceId, at)) : void 0;
|
|
25
27
|
/**
|
|
26
28
|
* An element names ONE producer, and which one decides how its data is fetched.
|
|
27
29
|
*
|
|
@@ -33,9 +35,10 @@ var connectorRscData = ({ connectors, actions, publicDir, elementTimeoutMs }) =>
|
|
|
33
35
|
const attributes = context.element.attributes;
|
|
34
36
|
if (attributes.connector) return resolveConnector ? resolveConnector(context) : void 0;
|
|
35
37
|
if (attributes.action) return resolveAction ? resolveAction(context) : void 0;
|
|
36
|
-
|
|
38
|
+
const fromData = resolveDataFile ? await resolveDataFile(context) : void 0;
|
|
39
|
+
return fromData !== void 0 ? fromData : resolvePublicFile ? resolvePublicFile(context) : void 0;
|
|
37
40
|
};
|
|
38
|
-
return async ({ req, spaceId, environment, user, ids, loadOfflineData, flagOverrides }) => {
|
|
41
|
+
return async ({ req, spaceId, environment, user, ids, loadOfflineData, flagOverrides, signal }) => {
|
|
39
42
|
const offlineData = await loadOfflineData();
|
|
40
43
|
if (!offlineData || offlineData.schema.rsc?.enabled === false) return {};
|
|
41
44
|
return resolveRscData({
|
|
@@ -47,6 +50,7 @@ var connectorRscData = ({ connectors, actions, publicDir, elementTimeoutMs }) =>
|
|
|
47
50
|
ids,
|
|
48
51
|
resolveElement,
|
|
49
52
|
...flagOverrides ? { flagOverrides } : {},
|
|
53
|
+
...signal ? { signal } : {},
|
|
50
54
|
...elementTimeoutMs === void 0 ? {} : { timeoutMs: elementTimeoutMs }
|
|
51
55
|
});
|
|
52
56
|
};
|
|
@@ -1,21 +1,6 @@
|
|
|
1
1
|
import { TtlCache } from '../../helpers/cache';
|
|
2
2
|
import { PluginManager } from '../../plugins/manager';
|
|
3
3
|
import { SSRPageServerConfig, SSRRequest, SSRResponseHelpers } from '@plitzi/sdk-shared';
|
|
4
|
-
/**
|
|
5
|
-
* Rewrites the request to the page the browser is actually on.
|
|
6
|
-
*
|
|
7
|
-
* A refresh is issued against `/_rsc`, not against `/blog/my-post`, so resolving straight from `req.path` matches
|
|
8
|
-
* nothing in `schema.pages` and every client-side refresh silently returns no data. The SDK sends the visitor's
|
|
9
|
-
* location as `?location=`, and route params plus the page parameter are read from that instead.
|
|
10
|
-
*
|
|
11
|
-
* What a refresh asks for besides — the next page of a "load more", a search a provider was reloaded with — rides on
|
|
12
|
-
* the query string beside `location`, and is read over the location's own query. It used to be dropped with the rest
|
|
13
|
-
* of the endpoint's query, so a provider paged in place was answered its first page every time.
|
|
14
|
-
*
|
|
15
|
-
* Reflecting both is safe by construction: the location is matched against this space's own page list, and the rest
|
|
16
|
-
* is what the same visitor could have put in the page's own address; the cache key is the whole request, so no answer
|
|
17
|
-
* is served for a query it was not made for.
|
|
18
|
-
*/
|
|
19
4
|
export declare const withPageLocation: (req: SSRRequest) => SSRRequest;
|
|
20
5
|
/**
|
|
21
6
|
* Handles GET /_rsc requests.
|
|
@@ -30,4 +15,6 @@ export declare const withPageLocation: (req: SSRRequest) => SSRRequest;
|
|
|
30
15
|
* - Authenticated requests: Cache-Control: private, max-age=<ttl>
|
|
31
16
|
* - Unauthenticated requests: Cache-Control: public, max-age=<ttl>
|
|
32
17
|
*/
|
|
33
|
-
export declare const handleRsc: (req: SSRRequest, res: SSRResponseHelpers, config: SSRPageServerConfig, _pluginManager: PluginManager, cache?: TtlCache<string
|
|
18
|
+
export declare const handleRsc: (req: SSRRequest, res: SSRResponseHelpers, config: SSRPageServerConfig, _pluginManager: PluginManager, cache?: TtlCache<string>,
|
|
19
|
+
/** The request's own: a browser that stops waiting stops the elements it asked about. */
|
|
20
|
+
signal?: AbortSignal) => Promise<void>;
|
|
@@ -39,12 +39,16 @@ var ENDPOINT_PARAMS = /* @__PURE__ */ new Set(["location", "ids"]);
|
|
|
39
39
|
* is what the same visitor could have put in the page's own address; the cache key is the whole request, so no answer
|
|
40
40
|
* is served for a query it was not made for.
|
|
41
41
|
*/
|
|
42
|
+
/** What a refresh asked for beside the endpoint's own parameters. */
|
|
43
|
+
var refreshParams = (req) => Object.fromEntries(Object.entries(req.query).filter(([key]) => !ENDPOINT_PARAMS.has(key)));
|
|
42
44
|
var withPageLocation = (req) => {
|
|
43
45
|
const location = req.query.location;
|
|
44
46
|
if (!location || !location.startsWith("/") || location.length > MAX_PAGE_LOCATION) return req;
|
|
45
47
|
const url = new URL(location, `${req.protocol}://${req.hostname}`);
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
+
const query = {
|
|
49
|
+
...Object.fromEntries(url.searchParams.entries()),
|
|
50
|
+
...refreshParams(req)
|
|
51
|
+
};
|
|
48
52
|
const search = new URLSearchParams(query).toString();
|
|
49
53
|
return {
|
|
50
54
|
...req,
|
|
@@ -67,7 +71,7 @@ var withPageLocation = (req) => {
|
|
|
67
71
|
* - Authenticated requests: Cache-Control: private, max-age=<ttl>
|
|
68
72
|
* - Unauthenticated requests: Cache-Control: public, max-age=<ttl>
|
|
69
73
|
*/
|
|
70
|
-
var handleRsc = async (req, res, config, _pluginManager, cache) => {
|
|
74
|
+
var handleRsc = async (req, res, config, _pluginManager, cache, signal) => {
|
|
71
75
|
if (!config.adapters.getRscData) {
|
|
72
76
|
res.setStatus(501);
|
|
73
77
|
res.send(JSON.stringify({ error: "getRscData adapter not configured" }));
|
|
@@ -83,6 +87,7 @@ var handleRsc = async (req, res, config, _pluginManager, cache) => {
|
|
|
83
87
|
const ids = idsRaw ? idsRaw.split(",").filter(Boolean).slice(0, 50).map((id) => id.slice(0, 128)) : void 0;
|
|
84
88
|
const idsParam = ids?.join(",");
|
|
85
89
|
const pageRequest = withPageLocation(req);
|
|
90
|
+
req.ctx.rscParams = refreshParams(req);
|
|
86
91
|
/**
|
|
87
92
|
* Whether this refresh belongs to somebody looking at a draft.
|
|
88
93
|
*
|
|
@@ -130,7 +135,8 @@ var handleRsc = async (req, res, config, _pluginManager, cache) => {
|
|
|
130
135
|
user: req.ctx.user,
|
|
131
136
|
ids,
|
|
132
137
|
loadOfflineData,
|
|
133
|
-
flagOverrides
|
|
138
|
+
flagOverrides,
|
|
139
|
+
...signal ? { signal } : {}
|
|
134
140
|
});
|
|
135
141
|
} catch (err) {
|
|
136
142
|
serverLog.error("RSC", "getRscData error", err);
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
import { RscElementResolver } from './resolveRscData';
|
|
2
|
+
import { SpaceRevision } from '@plitzi/sdk-shared';
|
|
2
3
|
/**
|
|
3
|
-
* A server provider whose `query` is a file this server serves from `publicDir` — `/
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* A project with no backend keeps its data in `public/data/*.json`, and an `apiContainer` fetching it from the browser
|
|
7
|
-
* leaves the server-rendered page without those sections: a flash of nothing, no content for a crawler, and no
|
|
8
|
-
* anchor in place for `/#section` to land on. Read here, it is the same bytes the browser would have fetched.
|
|
9
|
-
*
|
|
10
|
-
* Only a plain path inside `publicDir`: a URL, one that climbs out of the folder, or one with `{{tokens}}` (resolved
|
|
11
|
-
* in the browser against the visitor's route and state) is not this resolver's, and the element is left as it was.
|
|
12
|
-
*
|
|
13
|
-
* Answered in the shape a browser request publishes — `{ status, data }`, the body under `data` — so a provider reads
|
|
14
|
-
* `<source>.data.<field>` whichever runtime it has, and moving it to the server changes no binding.
|
|
4
|
+
* A server provider whose `query` is a file this server serves from `publicDir` — `/hours.json` — read from disk, so
|
|
5
|
+
* the page arrives with the section in it rather than fetching in the browser what the server already has. The same
|
|
6
|
+
* bytes the browser would have fetched.
|
|
15
7
|
*/
|
|
16
8
|
export declare const publicFileResolver: (publicDir: string) => RscElementResolver;
|
|
9
|
+
/**
|
|
10
|
+
* A server provider whose `query` is `/data/<file>`, read from `dataDir` — the project's own data, which is never
|
|
11
|
+
* served as a file: nobody downloads the folder. What the provider reads is in the page it renders.
|
|
12
|
+
*/
|
|
13
|
+
export declare const dataFileResolver: (dataDir: string) => RscElementResolver;
|
|
14
|
+
/**
|
|
15
|
+
* A server provider whose `query` is `/data/<file>`, answered from the space's own data as Plitzi keeps it — of the
|
|
16
|
+
* version being rendered, read off the same deployment record an action's run is (`req.ctx.spaceDeployment`), so a
|
|
17
|
+
* published page reads the data it was published with. What `dataFileResolver` is for a folder, for a platform.
|
|
18
|
+
*/
|
|
19
|
+
export declare const dataLookupResolver: (getData: (spaceId: number, at: SpaceRevision) => Promise<Record<string, string> | undefined>) => RscElementResolver;
|
|
@@ -1,32 +1,28 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { PROJECT_DATA_PREFIX } from "@plitzi/sdk-shared/server/rsc/projectData";
|
|
3
4
|
//#region src/modules/rsc/publicFileResolver.ts
|
|
5
|
+
/** Where a provider's `query` is a path: a URL, a protocol-relative one or one with `{{tokens}}` is not. */
|
|
6
|
+
var pathOf = (query) => {
|
|
7
|
+
if (typeof query !== "string" || !query.startsWith("/") || query.startsWith("//") || query.includes("{{")) return;
|
|
8
|
+
try {
|
|
9
|
+
return decodeURIComponent(query.split(/[?#]/)[0]);
|
|
10
|
+
} catch {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
4
14
|
/**
|
|
5
|
-
* A server provider whose `query`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* leaves the server-rendered page without those sections: a flash of nothing, no content for a crawler, and no
|
|
10
|
-
* anchor in place for `/#section` to land on. Read here, it is the same bytes the browser would have fetched.
|
|
11
|
-
*
|
|
12
|
-
* Only a plain path inside `publicDir`: a URL, one that climbs out of the folder, or one with `{{tokens}}` (resolved
|
|
13
|
-
* in the browser against the visitor's route and state) is not this resolver's, and the element is left as it was.
|
|
14
|
-
*
|
|
15
|
-
* Answered in the shape a browser request publishes — `{ status, data }`, the body under `data` — so a provider reads
|
|
16
|
-
* `<source>.data.<field>` whichever runtime it has, and moving it to the server changes no binding.
|
|
15
|
+
* A server provider whose `query` names a JSON file under `prefix`, read from the same path inside `folder` and handed
|
|
16
|
+
* to the render — answered in the shape a browser request publishes, `{ status, data }`, so a provider reads
|
|
17
|
+
* `<source>.data.<field>` whichever runtime it has. A path outside `prefix`, or one that climbs out of the folder, is
|
|
18
|
+
* not this resolver's, and the element is left as it was; a file not there, or not JSON, is the provider's error state.
|
|
17
19
|
*/
|
|
18
|
-
var
|
|
19
|
-
const root = path.resolve(
|
|
20
|
+
var fileResolver = (folder, prefix) => {
|
|
21
|
+
const root = path.resolve(folder);
|
|
20
22
|
return async ({ element, signal }) => {
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
pathname = decodeURIComponent(query.split(/[?#]/)[0]);
|
|
26
|
-
} catch {
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
const file = path.resolve(root, `.${pathname}`);
|
|
23
|
+
const pathname = pathOf(element.attributes.query);
|
|
24
|
+
if (pathname === void 0 || !pathname.startsWith(prefix)) return;
|
|
25
|
+
const file = path.resolve(root, `.${pathname.slice(prefix.length - 1)}`);
|
|
30
26
|
if (!file.startsWith(`${root}${path.sep}`)) return;
|
|
31
27
|
try {
|
|
32
28
|
return {
|
|
@@ -41,5 +37,41 @@ var publicFileResolver = (publicDir) => {
|
|
|
41
37
|
}
|
|
42
38
|
};
|
|
43
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* A server provider whose `query` is a file this server serves from `publicDir` — `/hours.json` — read from disk, so
|
|
42
|
+
* the page arrives with the section in it rather than fetching in the browser what the server already has. The same
|
|
43
|
+
* bytes the browser would have fetched.
|
|
44
|
+
*/
|
|
45
|
+
var publicFileResolver = (publicDir) => fileResolver(publicDir, "/");
|
|
46
|
+
/**
|
|
47
|
+
* A server provider whose `query` is `/data/<file>`, read from `dataDir` — the project's own data, which is never
|
|
48
|
+
* served as a file: nobody downloads the folder. What the provider reads is in the page it renders.
|
|
49
|
+
*/
|
|
50
|
+
var dataFileResolver = (dataDir) => fileResolver(dataDir, PROJECT_DATA_PREFIX);
|
|
51
|
+
/**
|
|
52
|
+
* A server provider whose `query` is `/data/<file>`, answered from the space's own data as Plitzi keeps it — of the
|
|
53
|
+
* version being rendered, read off the same deployment record an action's run is (`req.ctx.spaceDeployment`), so a
|
|
54
|
+
* published page reads the data it was published with. What `dataFileResolver` is for a folder, for a platform.
|
|
55
|
+
*/
|
|
56
|
+
var dataLookupResolver = (getData) => {
|
|
57
|
+
return async ({ element, spaceId, environment, req }) => {
|
|
58
|
+
const pathname = pathOf(element.attributes.query);
|
|
59
|
+
if (pathname === void 0 || !pathname.startsWith(PROJECT_DATA_PREFIX)) return;
|
|
60
|
+
const deployment = req.ctx.spaceDeployment;
|
|
61
|
+
const text = (await getData(spaceId, {
|
|
62
|
+
environment: deployment?.environment ?? environment,
|
|
63
|
+
revision: deployment?.revision ?? 0
|
|
64
|
+
}))?.[pathname.slice(PROJECT_DATA_PREFIX.length)];
|
|
65
|
+
if (text === void 0) return null;
|
|
66
|
+
try {
|
|
67
|
+
return {
|
|
68
|
+
status: 200,
|
|
69
|
+
data: JSON.parse(text)
|
|
70
|
+
};
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
};
|
|
44
76
|
//#endregion
|
|
45
|
-
export { publicFileResolver };
|
|
77
|
+
export { dataFileResolver, dataLookupResolver, publicFileResolver };
|
|
@@ -39,6 +39,8 @@ export type ResolveRscDataOptions = {
|
|
|
39
39
|
* per deployment as `rsc.elementTimeoutMs`.
|
|
40
40
|
*/
|
|
41
41
|
timeoutMs?: number;
|
|
42
|
+
/** Whoever asked stopped waiting: every element still resolving is cancelled, as when its budget runs out. */
|
|
43
|
+
signal?: AbortSignal;
|
|
42
44
|
};
|
|
43
45
|
/**
|
|
44
46
|
* Resolves the server-side data slices for the page addressed by `req`.
|
|
@@ -47,4 +49,4 @@ export type ResolveRscDataOptions = {
|
|
|
47
49
|
* and the route params are what the browser will see. Each `runtime: 'server'` element resolves independently:
|
|
48
50
|
* a provider that fails or hangs costs its own slice and nothing else.
|
|
49
51
|
*/
|
|
50
|
-
export declare const resolveRscData: ({ schema, req, spaceId, environment, user, ids, resolveElement, flagOverrides, timeoutMs }: ResolveRscDataOptions) => Promise<SSRRscData>;
|
|
52
|
+
export declare const resolveRscData: ({ schema, req, spaceId, environment, user, ids, resolveElement, flagOverrides, timeoutMs, signal }: ResolveRscDataOptions) => Promise<SSRRscData>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { serverLog } from "../../helpers/serverLog.js";
|
|
2
|
+
import onAbort from "../../helpers/onAbort.js";
|
|
2
3
|
import { matchRscPage } from "./matchRscPage.js";
|
|
3
4
|
import { flagUserFromSSR, flagValues, resolveFlags } from "@plitzi/sdk-shared/flags";
|
|
4
5
|
import { collectServerElements } from "@plitzi/sdk-shared/schema/serverElements";
|
|
@@ -11,18 +12,20 @@ var DEFAULT_ELEMENT_TIMEOUT_MS = 5e3;
|
|
|
11
12
|
* the loser running to its own timeout — for actions, five more seconds of a held slot and an in-flight request
|
|
12
13
|
* per element, for a page that was answered without it.
|
|
13
14
|
*/
|
|
14
|
-
var withBudget = async (run, timeoutMs, label) => {
|
|
15
|
+
var withBudget = async (run, timeoutMs, label, asked) => {
|
|
15
16
|
const controller = new AbortController();
|
|
16
17
|
let timer;
|
|
18
|
+
const releaseAsked = asked ? onAbort(asked, () => controller.abort()) : void 0;
|
|
17
19
|
try {
|
|
18
20
|
return await Promise.race([run(controller.signal), new Promise((_resolve, reject) => {
|
|
19
21
|
timer = setTimeout(() => {
|
|
20
22
|
controller.abort();
|
|
21
|
-
reject(/* @__PURE__ */ new Error(`RSC resolution timed out after ${timeoutMs}ms for ${label}
|
|
23
|
+
reject(/* @__PURE__ */ new Error(`RSC resolution timed out after ${timeoutMs}ms for ${label} — its section arrives empty. A slower producer is given more with \`createServer({ rsc: { elementTimeoutMs } })\`.`));
|
|
22
24
|
}, timeoutMs);
|
|
23
25
|
})]);
|
|
24
26
|
} finally {
|
|
25
27
|
if (timer) clearTimeout(timer);
|
|
28
|
+
releaseAsked?.();
|
|
26
29
|
}
|
|
27
30
|
};
|
|
28
31
|
/**
|
|
@@ -32,7 +35,7 @@ var withBudget = async (run, timeoutMs, label) => {
|
|
|
32
35
|
* and the route params are what the browser will see. Each `runtime: 'server'` element resolves independently:
|
|
33
36
|
* a provider that fails or hangs costs its own slice and nothing else.
|
|
34
37
|
*/
|
|
35
|
-
var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, resolveElement, flagOverrides, timeoutMs = DEFAULT_ELEMENT_TIMEOUT_MS }) => {
|
|
38
|
+
var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, resolveElement, flagOverrides, timeoutMs = DEFAULT_ELEMENT_TIMEOUT_MS, signal }) => {
|
|
36
39
|
if (schema.rsc?.enabled === false) return {};
|
|
37
40
|
const match = matchRscPage(schema, req.path, user);
|
|
38
41
|
if (!match) return { serverData: {} };
|
|
@@ -58,10 +61,10 @@ var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, reso
|
|
|
58
61
|
environment,
|
|
59
62
|
user,
|
|
60
63
|
signal
|
|
61
|
-
}), timeoutMs, `element ${element.id}
|
|
64
|
+
}), timeoutMs, `element ${element.id}`, signal)
|
|
62
65
|
})))).reduce((acum, result, index) => {
|
|
63
66
|
if (result.status === "rejected") {
|
|
64
|
-
serverLog.error("RSC", `element ${targets[index].id} failed to resolve`, result.reason);
|
|
67
|
+
if (!signal?.aborted) serverLog.error("RSC", `element ${targets[index].id} failed to resolve`, result.reason);
|
|
65
68
|
return acum;
|
|
66
69
|
}
|
|
67
70
|
if (result.value.data !== void 0) acum[result.value.id] = result.value.data;
|
|
@@ -17,6 +17,11 @@ export declare const inspectRuntime: (bytes: Uint8Array) => {
|
|
|
17
17
|
entry: string;
|
|
18
18
|
files: string[];
|
|
19
19
|
};
|
|
20
|
+
/**
|
|
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 runtime`), with nothing to wire.
|
|
23
|
+
*/
|
|
24
|
+
export declare const loadRuntimeModule: (file: string) => Promise<SpaceRuntime | undefined>;
|
|
20
25
|
/**
|
|
21
26
|
* A packed runtime written into `dir` and imported: its module's default export. `dir` must be inside the host's own
|
|
22
27
|
* project, so the packages the bundle leaves to the host resolve to the host's — one copy of each.
|
|
@@ -85,6 +85,24 @@ var inspectRuntime = (bytes) => {
|
|
|
85
85
|
};
|
|
86
86
|
};
|
|
87
87
|
var isRuntime = (value) => isRecord(value) && typeof value.start === "function";
|
|
88
|
+
/** A module's default export, held to what a runtime is — or refused, saying how to write one. */
|
|
89
|
+
var runtimeOf = (loaded) => {
|
|
90
|
+
const runtime = isRecord(loaded) ? loaded.default : void 0;
|
|
91
|
+
if (!isRuntime(runtime)) throw new Error("A runtime module exports its runtime by default: export default defineRuntime({ start: … })");
|
|
92
|
+
return runtime;
|
|
93
|
+
};
|
|
94
|
+
/**
|
|
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 runtime`), with nothing to wire.
|
|
97
|
+
*/
|
|
98
|
+
var loadRuntimeModule = async (file) => {
|
|
99
|
+
try {
|
|
100
|
+
await fs.access(file);
|
|
101
|
+
} catch {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
return runtimeOf(await import(pathToFileURL(file).href));
|
|
105
|
+
};
|
|
88
106
|
/**
|
|
89
107
|
* A packed runtime written into `dir` and imported: its module's default export. `dir` must be inside the host's own
|
|
90
108
|
* project, so the packages the bundle leaves to the host resolve to the host's — one copy of each.
|
|
@@ -93,10 +111,7 @@ var loadRuntime = async (bytes, dir) => {
|
|
|
93
111
|
const { entry, files } = unpack(bytes);
|
|
94
112
|
await fs.mkdir(dir, { recursive: true });
|
|
95
113
|
await Promise.all(Object.entries(files).map(([name, content]) => fs.writeFile(path.join(dir, name), Buffer.from(content, "base64"))));
|
|
96
|
-
|
|
97
|
-
const runtime = isRecord(loaded) ? loaded.default : void 0;
|
|
98
|
-
if (!isRuntime(runtime)) throw new Error("A runtime module exports its runtime by default: export default defineRuntime({ start: … })");
|
|
99
|
-
return runtime;
|
|
114
|
+
return runtimeOf(await import(pathToFileURL(path.join(dir, entry)).href));
|
|
100
115
|
};
|
|
101
116
|
//#endregion
|
|
102
|
-
export { MAX_RUNTIME_BUNDLE_BYTES, inspectRuntime, loadRuntime, packRuntime, runtimeBundleId };
|
|
117
|
+
export { MAX_RUNTIME_BUNDLE_BYTES, inspectRuntime, loadRuntime, loadRuntimeModule, packRuntime, runtimeBundleId };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { ActionKvAdapter } from '../actions/types';
|
|
3
|
+
export type SqliteKvOptions = ({
|
|
4
|
+
/** The database file — created, with its folder, when it is not there. Opened in WAL mode. */
|
|
5
|
+
file: string;
|
|
6
|
+
db?: never;
|
|
7
|
+
} | {
|
|
8
|
+
/** A database the deployment opened and shares with its own tables (a job queue, an activity log). */
|
|
9
|
+
db: DatabaseSync;
|
|
10
|
+
file?: never;
|
|
11
|
+
}) & {
|
|
12
|
+
/** The table the keys live in, created when it is not there. Default `action_kv`. */
|
|
13
|
+
table?: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A key/value store in a SQLite file, over Node's own `node:sqlite` — nothing to install, nothing to run beside the
|
|
17
|
+
* server. For a deployment with no database that wants what `kv` keeps to survive a restart AND to be shared: every
|
|
18
|
+
* process pointed at the same file — replicas on one machine, a server started with `workers` — sees the same keys,
|
|
19
|
+
* the same counters and the same single-flight keys.
|
|
20
|
+
*
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { createSqliteKv } from '@plitzi/sdk-server/sqlite';
|
|
23
|
+
*
|
|
24
|
+
* createServer({ action: { kv: createSqliteKv({ file: 'data/kv.sqlite' }) } });
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Every operation is ONE statement, so it is atomic without a transaction: an increment is an upsert that adds in
|
|
28
|
+
* place, a swap is an update whose `WHERE` names the value it expects. Lifetimes are instants on the store's clock,
|
|
29
|
+
* enforced on every read, and expired rows are swept as writes go by.
|
|
30
|
+
*
|
|
31
|
+
* Needs a Node with `node:sqlite` unflagged (22.13 and later), which still prints an `ExperimentalWarning` once when
|
|
32
|
+
* it loads — the reason this is its own entry rather than the default.
|
|
33
|
+
*/
|
|
34
|
+
export declare const createSqliteKv: (options: SqliteKvOptions) => ActionKvAdapter;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
//#region src/modules/sqlite/kv.ts
|
|
5
|
+
/**
|
|
6
|
+
* The STORE's clock in epoch ms, read inside each statement: every process on the file agrees on what "now" is.
|
|
7
|
+
* `julianday()` with no argument is the current instant.
|
|
8
|
+
*/
|
|
9
|
+
var NOW_MS = "CAST((julianday() - 2440587.5) * 86400000 AS INTEGER)";
|
|
10
|
+
var open = (file) => {
|
|
11
|
+
fs.mkdirSync(path.dirname(path.resolve(file)), { recursive: true });
|
|
12
|
+
const db = new DatabaseSync(file);
|
|
13
|
+
db.exec("PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA synchronous = NORMAL;");
|
|
14
|
+
return db;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* A key/value store in a SQLite file, over Node's own `node:sqlite` — nothing to install, nothing to run beside the
|
|
18
|
+
* server. For a deployment with no database that wants what `kv` keeps to survive a restart AND to be shared: every
|
|
19
|
+
* process pointed at the same file — replicas on one machine, a server started with `workers` — sees the same keys,
|
|
20
|
+
* the same counters and the same single-flight keys.
|
|
21
|
+
*
|
|
22
|
+
* ```ts
|
|
23
|
+
* import { createSqliteKv } from '@plitzi/sdk-server/sqlite';
|
|
24
|
+
*
|
|
25
|
+
* createServer({ action: { kv: createSqliteKv({ file: 'data/kv.sqlite' }) } });
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* Every operation is ONE statement, so it is atomic without a transaction: an increment is an upsert that adds in
|
|
29
|
+
* place, a swap is an update whose `WHERE` names the value it expects. Lifetimes are instants on the store's clock,
|
|
30
|
+
* enforced on every read, and expired rows are swept as writes go by.
|
|
31
|
+
*
|
|
32
|
+
* Needs a Node with `node:sqlite` unflagged (22.13 and later), which still prints an `ExperimentalWarning` once when
|
|
33
|
+
* it loads — the reason this is its own entry rather than the default.
|
|
34
|
+
*/
|
|
35
|
+
var createSqliteKv = (options) => {
|
|
36
|
+
const table = options.table ?? "action_kv";
|
|
37
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) throw new Error(`createSqliteKv: "${table}" is not a table name (letters, digits and _, not starting with a digit).`);
|
|
38
|
+
const db = options.db ?? open(options.file);
|
|
39
|
+
db.exec(`CREATE TABLE IF NOT EXISTS ${table} (key TEXT PRIMARY KEY, value TEXT NOT NULL, expires_at INTEGER)`);
|
|
40
|
+
const live = `(expires_at IS NULL OR expires_at > ${NOW_MS})`;
|
|
41
|
+
const expiry = (ttlSeconds) => ttlSeconds === void 0 ? null : ttlSeconds * 1e3;
|
|
42
|
+
const expiresAt = `CASE WHEN ? IS NULL THEN NULL ELSE ${NOW_MS} + ? END`;
|
|
43
|
+
const statements = {
|
|
44
|
+
get: db.prepare(`SELECT value FROM ${table} WHERE key = ? AND ${live}`),
|
|
45
|
+
set: db.prepare(`INSERT INTO ${table} (key, value, expires_at) VALUES (?, ?, ${expiresAt})
|
|
46
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at`),
|
|
47
|
+
delete: db.prepare(`DELETE FROM ${table} WHERE key = ?`),
|
|
48
|
+
increment: db.prepare(`INSERT INTO ${table} (key, value, expires_at) VALUES (?, ?, NULL)
|
|
49
|
+
ON CONFLICT (key) DO UPDATE SET
|
|
50
|
+
value = CASE WHEN ${table}.expires_at IS NOT NULL AND ${table}.expires_at <= ${NOW_MS}
|
|
51
|
+
THEN excluded.value ELSE CAST(${table}.value + ? AS TEXT) END,
|
|
52
|
+
expires_at = CASE WHEN ${table}.expires_at IS NOT NULL AND ${table}.expires_at <= ${NOW_MS}
|
|
53
|
+
THEN NULL ELSE ${table}.expires_at END
|
|
54
|
+
RETURNING value`),
|
|
55
|
+
expire: db.prepare(`UPDATE ${table} SET expires_at = ${expiresAt} WHERE key = ? AND ${live}`),
|
|
56
|
+
claim: db.prepare(`INSERT INTO ${table} (key, value, expires_at) VALUES (?, ?, ${expiresAt})
|
|
57
|
+
ON CONFLICT (key) DO UPDATE SET value = excluded.value, expires_at = excluded.expires_at
|
|
58
|
+
WHERE ${table}.expires_at IS NOT NULL AND ${table}.expires_at <= ${NOW_MS}`),
|
|
59
|
+
replace: db.prepare(`UPDATE ${table} SET value = ?, expires_at = ${expiresAt} WHERE key = ? AND value = ? AND ${live}`),
|
|
60
|
+
sweep: db.prepare(`DELETE FROM ${table} WHERE expires_at IS NOT NULL AND expires_at <= ${NOW_MS}`)
|
|
61
|
+
};
|
|
62
|
+
/** Used-once keys (a delivery's idempotency key, a caller's window) expire and stay until swept: once a minute. */
|
|
63
|
+
let sweptAt = 0;
|
|
64
|
+
const sweep = () => {
|
|
65
|
+
if (Date.now() - sweptAt < 6e4) return;
|
|
66
|
+
sweptAt = Date.now();
|
|
67
|
+
statements.sweep.run();
|
|
68
|
+
};
|
|
69
|
+
const text = (row) => typeof row?.value === "string" ? row.value : void 0;
|
|
70
|
+
return {
|
|
71
|
+
get: (key) => Promise.resolve(text(statements.get.get(key))),
|
|
72
|
+
set: (key, value, ttlSeconds) => {
|
|
73
|
+
statements.set.run(key, value, expiry(ttlSeconds), expiry(ttlSeconds));
|
|
74
|
+
sweep();
|
|
75
|
+
return Promise.resolve();
|
|
76
|
+
},
|
|
77
|
+
delete: (key) => {
|
|
78
|
+
statements.delete.run(key);
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
},
|
|
81
|
+
increment: (key, amount) => {
|
|
82
|
+
const step = Number.isInteger(amount) ? BigInt(amount) : amount;
|
|
83
|
+
const value = text(statements.increment.get(key, String(amount), step));
|
|
84
|
+
sweep();
|
|
85
|
+
return Promise.resolve(Number(value ?? amount));
|
|
86
|
+
},
|
|
87
|
+
expire: (key, ttlSeconds) => {
|
|
88
|
+
statements.expire.run(expiry(ttlSeconds), expiry(ttlSeconds), key);
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
},
|
|
91
|
+
swap: (key, expected, next, ttlSeconds) => {
|
|
92
|
+
const written = expected === void 0 ? statements.claim.run(key, next, expiry(ttlSeconds), expiry(ttlSeconds)) : statements.replace.run(next, expiry(ttlSeconds), expiry(ttlSeconds), key, expected);
|
|
93
|
+
sweep();
|
|
94
|
+
return Promise.resolve(written.changes === 1);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
//#endregion
|
|
99
|
+
export { createSqliteKv };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -9,13 +9,14 @@ import { loadPluginComponents } from "./loadPluginComponents.js";
|
|
|
9
9
|
import "../../core/previewToken.js";
|
|
10
10
|
import { resolveActionEndpoint, resolveRscEndpoint } from "../../core/services/resolve.js";
|
|
11
11
|
import { createOfflineDataLoader } from "../../helpers/offlineDataLoader.js";
|
|
12
|
+
import { hydrationPayload, spaceDocument } from "../../helpers/hydrationPayload.js";
|
|
13
|
+
import { publishSpaceDocument } from "./spaceDocument.js";
|
|
12
14
|
import { registerExternalPlugins } from "./registerExternalPlugins.js";
|
|
13
15
|
import { reportMissingPlugins } from "./reportMissingPlugins.js";
|
|
14
16
|
import { resolvePageSeo } from "./resolvePageSeo.js";
|
|
15
17
|
import { imagesPathOf } from "../../core/http/stages/images.js";
|
|
16
18
|
import { sdkAssetVersion } from "../../core/sdkAssets.js";
|
|
17
19
|
import { buildServerInfo } from "../../helpers/buildServerInfo.js";
|
|
18
|
-
import { hydrationPayload } from "../../helpers/hydrationPayload.js";
|
|
19
20
|
import { resolveDebugMode } from "../../helpers/resolveDebugMode.js";
|
|
20
21
|
import { flagUserFromSSR, flagValues, resolveFlags } from "@plitzi/sdk-shared/flags";
|
|
21
22
|
import { hasServerElements } from "@plitzi/sdk-shared/schema/serverElements";
|
|
@@ -23,6 +24,8 @@ import { debugCookieName } from "@plitzi/sdk-shared/devTools";
|
|
|
23
24
|
import { themeFromCookies } from "@plitzi/sdk-shared/theme";
|
|
24
25
|
import { paintedKeys, paintedStateFor } from "@plitzi/sdk-shared/state/paintedState";
|
|
25
26
|
import { fontUrlResolver, fontsToHead } from "@plitzi/sdk-shared/style";
|
|
27
|
+
import { pluginDeclarationOf, pluginTypesOf } from "@plitzi/sdk-shared/plugins/declaration";
|
|
28
|
+
import { pageElementTypes } from "@plitzi/sdk-shared/schema/pageElements";
|
|
26
29
|
//#region src/modules/ssr/prepareRender.tsx
|
|
27
30
|
/** Last resort only: used for a page that declares no SEO title and a deployment that supplies none either. */
|
|
28
31
|
var DEFAULT_TITLE = "Plitzi App";
|
|
@@ -36,6 +39,14 @@ var declaredTheme = (style) => {
|
|
|
36
39
|
const declared = style?.theme?.default;
|
|
37
40
|
return declared && declared !== "system" ? declared : void 0;
|
|
38
41
|
};
|
|
42
|
+
var declarations = /* @__PURE__ */ new WeakMap();
|
|
43
|
+
var declarationOf = (component) => {
|
|
44
|
+
const known = declarations.get(component);
|
|
45
|
+
if (known) return known;
|
|
46
|
+
const declaration = pluginDeclarationOf(component);
|
|
47
|
+
declarations.set(component, declaration);
|
|
48
|
+
return declaration;
|
|
49
|
+
};
|
|
39
50
|
var prepareRender = async (req, config, spaceId, environment, revision, pluginManager, offlineDataCache, metrics, offlineDataOverride) => {
|
|
40
51
|
const m = (name, fn) => metrics ? metrics.measure(name, fn) : Promise.resolve(fn());
|
|
41
52
|
const offlineCacheKey = environment !== "main" ? buildOfflineDataCacheKey(spaceId, environment, revision, req.ctx.spaceDeployment?.flagsVersion) : void 0;
|
|
@@ -147,6 +158,13 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
147
158
|
* page, so it hydrates with the flags it was drawn with.
|
|
148
159
|
*/
|
|
149
160
|
const { server: serverFlags, qa: forcedFlags } = flagOverrides;
|
|
161
|
+
/**
|
|
162
|
+
* The space travels beside the page rather than in it, fetched while the scripts are — see `spaceDocument`.
|
|
163
|
+
*
|
|
164
|
+
* Not for a draft, which no other process could produce when the fetch reaches it, and not for a deployment's own
|
|
165
|
+
* template, which was written for the space inline; a page with no script needs no space at all.
|
|
166
|
+
*/
|
|
167
|
+
const spaceDocumentPath = offlineData !== void 0 && offlineDataOverride === void 0 && config.templateFn === void 0 && config.ssrOnly !== true ? publishSpaceDocument(spaceId, environment, spaceDocument(offlineData)) : void 0;
|
|
150
168
|
const offlineDataStr = hydrationPayload(offlineData, {
|
|
151
169
|
offlineMode: true,
|
|
152
170
|
environment,
|
|
@@ -160,7 +178,7 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
160
178
|
...actionRuns ? { actionRuns } : {},
|
|
161
179
|
...serverFlags ? { serverFlags } : {},
|
|
162
180
|
...forcedFlags ? { forcedFlags } : {}
|
|
163
|
-
});
|
|
181
|
+
}, { apart: spaceDocumentPath !== void 0 });
|
|
164
182
|
const pluginNames = req.ctx.spaceDeployment?.pluginNames ?? [];
|
|
165
183
|
const pluginSources = req.ctx.spaceDeployment?.pluginSources;
|
|
166
184
|
const pluginBaseNames = new Set(pluginNames.map((n) => n.replace(/@[^@]*$/, "")));
|
|
@@ -178,10 +196,29 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
178
196
|
const entries = allPluginNames.length > 0 ? await pluginManager.getEntries(allPluginNames) : [];
|
|
179
197
|
const pluginComponents = await m("plugins", () => loadPluginComponents(entries, pluginManager.getComponents()));
|
|
180
198
|
reportMissingPlugins(spaceId, schema, /* @__PURE__ */ new Set([...Object.keys(pluginComponents), ...entries.map((entry) => entry.keyName)]));
|
|
181
|
-
const
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
199
|
+
const templateEntries = entries.length > 0 ? entries : req.ctx.spaceDeployment?.templateProps?.plugins;
|
|
200
|
+
/**
|
|
201
|
+
* The plugins this page draws none of: the browser is sent what they declare, and loads one when a page that draws
|
|
202
|
+
* it is opened. Their code is what the first paint waited on — a page of the website downloaded the builder's.
|
|
203
|
+
*
|
|
204
|
+
* Only one the server holds the component of can wait, since its declaration is read from it; a page matched to
|
|
205
|
+
* nothing, which cannot say what it draws, waits for none.
|
|
206
|
+
*/
|
|
207
|
+
const pageTypes = schema !== void 0 && pageMatch !== void 0 ? pageElementTypes(schema, pageMatch.pageId) : void 0;
|
|
208
|
+
const deferredOf = (entry) => {
|
|
209
|
+
const plugin = pageTypes && entry.js ? pluginComponents[entry.keyName] : void 0;
|
|
210
|
+
if (!plugin) return;
|
|
211
|
+
const declaration = declarationOf(plugin.component);
|
|
212
|
+
return pluginTypesOf(entry.keyName, declaration).some((type) => pageTypes?.has(type)) ? void 0 : declaration;
|
|
213
|
+
};
|
|
214
|
+
const templatePlugins = templateEntries?.map((entry) => {
|
|
215
|
+
const deferred = deferredOf(entry);
|
|
216
|
+
return {
|
|
217
|
+
...entry,
|
|
218
|
+
ssr: entry.keyName in pluginComponents,
|
|
219
|
+
...deferred ? { deferred } : {}
|
|
220
|
+
};
|
|
221
|
+
});
|
|
185
222
|
const vendorJs = (debugAuthorized ? "/sdk-assets/plitzi-sdk-dev-vendor.js" : "/sdk-assets/plitzi-sdk-vendor.js") + v;
|
|
186
223
|
return {
|
|
187
224
|
componentProps: {
|
|
@@ -202,6 +239,8 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
202
239
|
title: DEFAULT_TITLE,
|
|
203
240
|
jsPath: `/sdk-assets/plitzi-sdk.js${v}`,
|
|
204
241
|
cssPath: `/sdk-assets/plitzi-sdk.css${v}`,
|
|
242
|
+
iconsCssPath: `/sdk-assets/plitzi-sdk-icons.css${v}`,
|
|
243
|
+
spaceDocumentPath,
|
|
205
244
|
react: vendorJs,
|
|
206
245
|
reactJsx: vendorJs,
|
|
207
246
|
reactDom: vendorJs,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { TtlCache } from '../../helpers/cache';
|
|
2
|
+
import { SpaceDocument } from '../../helpers/hydrationPayload';
|
|
3
|
+
import { Environment, SSRPageServerConfig, SSRRequest, SSRResponseHelpers } from '@plitzi/sdk-shared';
|
|
4
|
+
/**
|
|
5
|
+
* The whole space as a resource of its own, beside the page rather than inside it.
|
|
6
|
+
*
|
|
7
|
+
* Embedded, it was most of every document — megabytes of a big space, before the first word of the page — and it was
|
|
8
|
+
* downloaded again on every page and every visit. Apart, the page arrives at the size of what it shows, and the space
|
|
9
|
+
* is fetched while the scripts are, once: its address is its content's hash, so it is kept until it changes.
|
|
10
|
+
*
|
|
11
|
+
* It is still the whole space: the server draws the first page, and every page after it is the browser's, with no
|
|
12
|
+
* server to ask.
|
|
13
|
+
*/
|
|
14
|
+
export declare const SPACE_DOCUMENT_PATH = "/_plitzi/space/";
|
|
15
|
+
/** Where the page fetches `document` from — and the promise that this process can answer it. */
|
|
16
|
+
export declare const publishSpaceDocument: (spaceId: number, environment: Environment, document: SpaceDocument) => string;
|
|
17
|
+
/**
|
|
18
|
+
* Answers `/_plitzi/space/<hash>.json` for the space this host serves, behind the same gates as its pages.
|
|
19
|
+
*
|
|
20
|
+
* Every page of a space already carried all of it, so whoever may open one page may read this. A hash this process
|
|
21
|
+
* did not just publish is looked up from the space as it is now: the same text when another process published it,
|
|
22
|
+
* or the space as it changed since — answered all the same, since it is what the page will find on its next load,
|
|
23
|
+
* but not to be kept under a name that is not its own.
|
|
24
|
+
*/
|
|
25
|
+
export declare const handleSpaceDocument: (req: SSRRequest, res: SSRResponseHelpers, config: SSRPageServerConfig, offlineDataCache?: TtlCache<string>) => Promise<void>;
|