@plitzi/sdk-server 0.33.1 → 0.34.0
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 +53 -0
- package/README.md +283 -8
- package/dist/actions.d.ts +62 -0
- package/dist/actions.js +12 -0
- package/dist/adapters/cloudAdapters.d.ts +72 -0
- package/dist/adapters/cloudAdapters.js +303 -0
- package/dist/adapters/space/index.d.ts +93 -0
- package/dist/adapters/space/index.js +122 -0
- package/dist/adapters/space/resolvers.d.ts +69 -0
- package/dist/adapters/space/resolvers.js +79 -0
- package/dist/adapters/space/space.test.d.ts +1 -0
- package/dist/adapters/space/types.d.ts +64 -0
- package/dist/adapters/space/types.js +41 -0
- package/dist/auth.d.ts +50 -0
- package/dist/auth.js +19 -0
- package/dist/core/auth/api.d.ts +400 -11
- package/dist/core/auth/api.js +1044 -50
- package/dist/core/auth/createAuth.d.ts +135 -30
- package/dist/core/auth/createAuth.js +96 -24
- package/dist/core/auth/credentials.d.ts +9 -3
- package/dist/core/auth/credentials.js +10 -4
- package/dist/core/auth/csrf.d.ts +89 -0
- package/dist/core/auth/csrf.js +234 -0
- package/dist/core/auth/csrf.test.d.ts +1 -0
- package/dist/core/auth/identity.d.ts +6 -0
- package/dist/core/auth/identity.js +37 -8
- package/dist/core/auth/lifecycle.test.d.ts +1 -0
- package/dist/core/auth/oauth/index.d.ts +8 -0
- package/dist/core/auth/oauth/index.js +13 -15
- package/dist/core/auth/passwords.d.ts +15 -0
- package/dist/core/auth/passwords.js +32 -0
- package/dist/core/auth/redirects.d.ts +18 -0
- package/dist/core/auth/redirects.js +31 -0
- package/dist/core/auth/redirects.test.d.ts +1 -0
- package/dist/core/auth/routes.d.ts +4 -2
- package/dist/core/auth/routes.js +234 -9
- package/dist/core/auth/session.d.ts +11 -0
- package/dist/core/auth/session.js +1 -1
- package/dist/core/auth/spaceTokens.d.ts +38 -2
- package/dist/core/auth/spaceTokens.js +54 -2
- package/dist/core/auth/throttle.d.ts +5 -0
- package/dist/core/auth/throttle.js +81 -0
- package/dist/core/auth/throttle.test.d.ts +1 -0
- package/dist/core/auth/tokens.d.ts +83 -12
- package/dist/core/auth/tokens.js +51 -9
- package/dist/core/auth/totp.d.ts +48 -0
- package/dist/core/auth/totp.js +118 -0
- package/dist/core/auth/totp.test.d.ts +1 -0
- package/dist/core/createServer.d.ts +0 -6
- package/dist/core/createServer.js +35 -3
- package/dist/core/fontStore.d.ts +33 -0
- package/dist/core/fontStore.js +53 -0
- package/dist/core/fontStore.test.d.ts +1 -0
- package/dist/core/handlers/authMiddleware.js +1 -1
- package/dist/core/handlers/authRouteHandlers.d.ts +9 -1
- package/dist/core/handlers/authRouteHandlers.js +4 -3
- package/dist/core/handlers/csrfMiddleware.d.ts +20 -0
- package/dist/core/handlers/csrfMiddleware.js +32 -0
- package/dist/core/handlers/oauthRouteHandlers.d.ts +46 -0
- package/dist/core/handlers/oauthRouteHandlers.js +159 -0
- package/dist/core/handlers/socialRouteHandlers.d.ts +40 -0
- package/dist/core/handlers/socialRouteHandlers.js +129 -0
- package/dist/core/handlers/socialRouteHandlers.test.d.ts +1 -0
- package/dist/core/handlers/types.d.ts +22 -5
- package/dist/core/health.d.ts +11 -1
- package/dist/core/health.js +27 -3
- package/dist/core/http/dispatcher.d.ts +1 -1
- package/dist/core/http/dispatcher.js +20 -1
- package/dist/core/http/stages/authApi.js +1 -1
- package/dist/core/http/stages/fontAssets.d.ts +9 -0
- package/dist/core/http/stages/fontAssets.js +27 -0
- package/dist/core/http/stages/oauth.d.ts +19 -0
- package/dist/core/http/stages/oauth.js +118 -0
- package/dist/core/http/types.d.ts +12 -0
- package/dist/core/oauth/authorize.d.ts +22 -0
- package/dist/core/oauth/authorize.js +254 -0
- package/dist/core/oauth/authorize.test.d.ts +1 -0
- package/dist/core/oauth/challenge.d.ts +11 -0
- package/dist/core/oauth/challenge.js +33 -0
- package/dist/core/oauth/consentPage.d.ts +12 -0
- package/dist/core/oauth/consentPage.js +131 -0
- package/dist/core/oauth/metadata.d.ts +41 -0
- package/dist/core/oauth/metadata.js +71 -0
- package/dist/core/oauth/params.d.ts +5 -0
- package/dist/core/oauth/params.js +5 -0
- package/dist/core/oauth/pkce.d.ts +6 -0
- package/dist/core/oauth/pkce.js +15 -0
- package/dist/core/oauth/records.d.ts +72 -0
- package/dist/core/oauth/records.js +49 -0
- package/dist/core/oauth/register.d.ts +5 -0
- package/dist/core/oauth/register.js +55 -0
- package/dist/core/oauth/respond.d.ts +21 -0
- package/dist/core/oauth/respond.js +59 -0
- package/dist/core/oauth/token.d.ts +14 -0
- package/dist/core/oauth/token.js +130 -0
- package/dist/core/previewToken.d.ts +20 -0
- package/dist/core/previewToken.js +29 -1
- package/dist/core/server/pageServer.js +4 -1
- package/dist/core/services/action.d.ts +6 -4
- package/dist/core/services/action.js +103 -10
- package/dist/core/services/registry.js +2 -0
- package/dist/core/services/resolve.d.ts +8 -0
- package/dist/core/services/resolve.js +12 -1
- package/dist/core/staticFiles.js +1 -2
- package/dist/handlers.d.ts +7 -1
- package/dist/handlers.js +4 -1
- package/dist/helpers/buildResponseHelpers.d.ts +8 -0
- package/dist/helpers/buildResponseHelpers.js +9 -1
- package/dist/helpers/buildServerInfo.d.ts +8 -3
- package/dist/helpers/buildServerInfo.js +18 -14
- package/dist/helpers/cache/keys.d.ts +13 -0
- package/dist/helpers/cache/keys.js +12 -1
- package/dist/helpers/onAbort.d.ts +12 -0
- package/dist/helpers/onAbort.js +22 -0
- package/dist/helpers/onAbort.test.d.ts +1 -0
- package/dist/helpers/outboundGuard.d.ts +27 -0
- package/dist/helpers/outboundGuard.js +49 -0
- package/dist/helpers/outboundGuard.test.d.ts +1 -0
- package/dist/helpers/serverLog.d.ts +6 -3
- package/dist/helpers/serverLog.js +20 -3
- package/dist/index.d.ts +6 -0
- package/dist/index.js +9 -4
- package/dist/kernel.d.ts +5 -22
- package/dist/kernel.js +1 -14
- package/dist/modules/actions/{handler.js → connectorWrite.js} +5 -2
- package/dist/modules/actions/connectorWrite.test.d.ts +1 -0
- package/dist/modules/actions/index.d.ts +40 -0
- package/dist/modules/actions/index.js +46 -0
- package/dist/modules/actions/moduleFor.d.ts +3 -0
- package/dist/modules/actions/moduleFor.js +35 -0
- package/dist/modules/actions/moduleFor.test.d.ts +1 -0
- package/dist/modules/actions/runtime/check.d.ts +24 -0
- package/dist/modules/actions/runtime/check.js +111 -0
- package/dist/modules/actions/runtime/check.test.d.ts +1 -0
- package/dist/modules/actions/runtime/errors.d.ts +11 -0
- package/dist/modules/actions/runtime/errors.js +17 -0
- package/dist/modules/actions/runtime/guards.d.ts +126 -0
- package/dist/modules/actions/runtime/guards.js +246 -0
- package/dist/modules/actions/runtime/guards.test.d.ts +1 -0
- package/dist/modules/actions/runtime/kvStore.d.ts +27 -0
- package/dist/modules/actions/runtime/kvStore.js +41 -0
- package/dist/modules/actions/runtime/kvStore.test.d.ts +1 -0
- package/dist/modules/actions/runtime/limits.d.ts +11 -0
- package/dist/modules/actions/runtime/limits.js +35 -0
- package/dist/modules/actions/runtime/memoryKv.d.ts +2 -0
- package/dist/modules/actions/runtime/memoryKv.js +77 -0
- package/dist/modules/actions/runtime/namespaceKv.d.ts +9 -0
- package/dist/modules/actions/runtime/namespaceKv.js +19 -0
- package/dist/modules/actions/runtime/precheck.d.ts +27 -0
- package/dist/modules/actions/runtime/precheck.js +45 -0
- package/dist/modules/actions/runtime/renderResolver.d.ts +15 -0
- package/dist/modules/actions/runtime/renderResolver.js +130 -0
- package/dist/modules/actions/runtime/renderResolver.test.d.ts +1 -0
- package/dist/modules/actions/runtime/renderShare.d.ts +29 -0
- package/dist/modules/actions/runtime/renderShare.js +36 -0
- package/dist/modules/actions/runtime/renderShare.test.d.ts +1 -0
- package/dist/modules/actions/runtime/report.d.ts +14 -0
- package/dist/modules/actions/runtime/report.js +22 -0
- package/dist/modules/actions/runtime/runAction.d.ts +12 -0
- package/dist/modules/actions/runtime/runAction.js +405 -0
- package/dist/modules/actions/runtime/runAction.test.d.ts +1 -0
- package/dist/modules/actions/runtime/runLogger.d.ts +31 -0
- package/dist/modules/actions/runtime/runLogger.js +66 -0
- package/dist/modules/actions/runtime/runLogger.test.d.ts +1 -0
- package/dist/modules/actions/runtime/schedule.d.ts +35 -0
- package/dist/modules/actions/runtime/schedule.js +76 -0
- package/dist/modules/actions/runtime/schedule.test.d.ts +1 -0
- package/dist/modules/actions/runtime/scope.d.ts +40 -0
- package/dist/modules/actions/runtime/scope.js +103 -0
- package/dist/modules/actions/runtime/triggers.d.ts +12 -0
- package/dist/modules/actions/runtime/triggers.js +14 -0
- package/dist/modules/actions/taskCatalog.d.ts +26 -0
- package/dist/modules/actions/taskCatalog.js +24 -0
- package/dist/modules/actions/taskCatalog.test.d.ts +1 -0
- package/dist/modules/actions/tasks/auth.d.ts +2 -0
- package/dist/modules/actions/tasks/auth.js +40 -0
- package/dist/modules/actions/tasks/builtins.d.ts +8 -0
- package/dist/modules/actions/tasks/builtins.js +24 -0
- package/dist/modules/actions/tasks/connector.d.ts +2 -0
- package/dist/modules/actions/tasks/connector.js +141 -0
- package/dist/modules/actions/tasks/db.d.ts +2 -0
- package/dist/modules/actions/tasks/db.js +59 -0
- package/dist/modules/actions/tasks/db.test.d.ts +1 -0
- package/dist/modules/actions/tasks/flow.d.ts +3 -0
- package/dist/modules/actions/tasks/flow.js +109 -0
- package/dist/modules/actions/tasks/helpers.d.ts +16 -0
- package/dist/modules/actions/tasks/helpers.js +30 -0
- package/dist/modules/actions/tasks/http.d.ts +2 -0
- package/dist/modules/actions/tasks/http.js +111 -0
- package/dist/modules/actions/tasks/http.test.d.ts +1 -0
- package/dist/modules/actions/tasks/kv.d.ts +2 -0
- package/dist/modules/actions/tasks/kv.js +102 -0
- package/dist/modules/actions/tasks/kv.test.d.ts +1 -0
- package/dist/modules/actions/tasks/registry.d.ts +10 -0
- package/dist/modules/actions/tasks/registry.js +36 -0
- package/dist/modules/actions/tasks/transform.d.ts +2 -0
- package/dist/modules/actions/tasks/transform.js +34 -0
- package/dist/modules/actions/transport/callHandler.d.ts +24 -0
- package/dist/modules/actions/transport/callHandler.js +233 -0
- package/dist/modules/actions/transport/callHandler.test.d.ts +1 -0
- package/dist/modules/actions/transport/cancelHandler.d.ts +23 -0
- package/dist/modules/actions/transport/cancelHandler.js +23 -0
- package/dist/modules/actions/transport/catalogHandler.d.ts +18 -0
- package/dist/modules/actions/transport/catalogHandler.js +21 -0
- package/dist/modules/actions/transport/stream.d.ts +23 -0
- package/dist/modules/actions/transport/stream.js +100 -0
- package/dist/modules/actions/transport/verifySignature.d.ts +16 -0
- package/dist/modules/actions/transport/verifySignature.js +72 -0
- package/dist/modules/actions/transport/webhookHandler.d.ts +25 -0
- package/dist/modules/actions/transport/webhookHandler.js +206 -0
- package/dist/modules/actions/transport/webhookHandler.test.d.ts +1 -0
- package/dist/modules/actions/types.d.ts +234 -0
- package/dist/modules/connectors/engine.js +4 -1
- package/dist/modules/connectors/resolver.d.ts +10 -2
- package/dist/modules/connectors/resolver.js +8 -3
- package/dist/modules/mysql/accounts.d.ts +5 -0
- package/dist/modules/mysql/accounts.js +365 -0
- package/dist/modules/mysql/accounts.test.d.ts +1 -0
- package/dist/modules/mysql/admin.d.ts +58 -0
- package/dist/modules/mysql/admin.js +133 -0
- package/dist/modules/mysql/config.d.ts +82 -0
- package/dist/modules/mysql/config.js +30 -0
- package/dist/modules/mysql/identities.d.ts +35 -0
- package/dist/modules/mysql/identities.js +43 -0
- package/dist/modules/mysql/index.d.ts +67 -0
- package/dist/modules/mysql/index.js +62 -0
- package/dist/modules/mysql/pool.d.ts +3 -0
- package/dist/modules/mysql/pool.js +88 -0
- package/dist/modules/mysql/query.d.ts +12 -0
- package/dist/modules/mysql/query.js +21 -0
- package/dist/modules/mysql/schema.d.ts +43 -0
- package/dist/modules/mysql/schema.js +340 -0
- package/dist/modules/mysql/schema.test.d.ts +1 -0
- package/dist/modules/mysql/sessions.d.ts +33 -0
- package/dist/modules/mysql/sessions.js +46 -0
- package/dist/modules/mysql/spaceTokens.d.ts +11 -0
- package/dist/modules/mysql/spaceTokens.js +59 -0
- package/dist/modules/mysql/store.integration.test.d.ts +1 -0
- package/dist/modules/rsc/connectorRscData.d.ts +25 -0
- package/dist/modules/rsc/connectorRscData.js +51 -0
- package/dist/modules/rsc/connectorRscData.test.d.ts +1 -0
- package/dist/modules/rsc/handler.js +16 -3
- package/dist/modules/rsc/matchRscPage.d.ts +14 -0
- package/dist/modules/rsc/matchRscPage.js +26 -0
- package/dist/modules/rsc/resolveRscData.d.ts +14 -1
- package/dist/modules/rsc/resolveRscData.js +23 -34
- package/dist/modules/ssr/Component.d.ts +6 -2
- package/dist/modules/ssr/Component.js +3 -1
- package/dist/modules/ssr/draftSession.test.d.ts +1 -0
- package/dist/modules/ssr/handler.js +15 -2
- package/dist/modules/ssr/loadPluginComponents.js +22 -5
- package/dist/modules/ssr/prepareRender.js +87 -8
- package/dist/modules/ssr/prepareRender.test.d.ts +1 -0
- package/dist/modules/ssr/preview.d.ts +21 -8
- package/dist/modules/ssr/preview.js +38 -14
- package/dist/modules/ssr/registerExternalPlugins.js +2 -2
- package/dist/modules/ssr/resolvePageSeo.d.ts +18 -0
- package/dist/modules/ssr/resolvePageSeo.js +26 -0
- package/dist/modules/ssr/resolvePageSeo.test.d.ts +1 -0
- package/dist/modules/ssr/template.test.d.ts +1 -0
- package/dist/modules/ssr/views/template.ejs +71 -5
- package/dist/mysql.d.ts +26 -0
- package/dist/mysql.js +4 -0
- package/dist/oauth.d.ts +24 -0
- package/dist/oauth.js +10 -0
- package/dist/plugins/compile.d.ts +1 -0
- package/dist/plugins/compile.js +37 -3
- package/dist/plugins/manager.d.ts +40 -0
- package/dist/plugins/manager.js +113 -13
- package/dist/plugins/manager.test.d.ts +1 -0
- package/package.json +43 -14
- package/skills/plitzi-authoring/SKILL.md +252 -0
- /package/dist/{modules/actions/handler.test.d.ts → adapters/cloudAdapters.test.d.ts} +0 -0
- /package/dist/modules/actions/{handler.d.ts → connectorWrite.d.ts} +0 -0
|
@@ -10,6 +10,14 @@ export type RscResolveContext = {
|
|
|
10
10
|
spaceId: number;
|
|
11
11
|
environment: Environment;
|
|
12
12
|
user: SSRUser | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* Aborted when this element's budget runs out, so a resolver can stop the work nobody is waiting for.
|
|
15
|
+
*
|
|
16
|
+
* The timeout below only ever decided when to STOP WAITING — the losing side of a race keeps running — so a
|
|
17
|
+
* provider that took longer than its budget went on holding a run slot and an outbound connection for a page
|
|
18
|
+
* that had already been sent. What is being cancelled is not the request, it is the work behind it.
|
|
19
|
+
*/
|
|
20
|
+
signal: AbortSignal;
|
|
13
21
|
};
|
|
14
22
|
/** Produces the data slice for a single server element. Returning undefined leaves the element out of the payload. */
|
|
15
23
|
export type RscElementResolver = (context: RscResolveContext) => Promise<unknown>;
|
|
@@ -22,7 +30,12 @@ export type ResolveRscDataOptions = {
|
|
|
22
30
|
/** Restricts resolution to these element ids (partial refresh). Undefined resolves every server element. */
|
|
23
31
|
ids?: string[];
|
|
24
32
|
resolveElement: RscElementResolver;
|
|
25
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Per-element budget: one slow provider must not hold the whole payload, and must not go on working once it
|
|
35
|
+
* has. It is the PAGE's ceiling and it wins over the producer's own — an action allowed ten seconds of its own
|
|
36
|
+
* is still cut off here, because a section is worth waiting for only as long as the visitor is. Configurable
|
|
37
|
+
* per deployment as `rsc.elementTimeoutMs`.
|
|
38
|
+
*/
|
|
26
39
|
timeoutMs?: number;
|
|
27
40
|
};
|
|
28
41
|
/**
|
|
@@ -1,28 +1,23 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { matchRscPage } from "./matchRscPage.js";
|
|
2
|
+
import { collectServerElements } from "@plitzi/sdk-shared/schema/serverElements";
|
|
2
3
|
//#region src/modules/rsc/resolveRscData.ts
|
|
3
4
|
var DEFAULT_ELEMENT_TIMEOUT_MS = 5e3;
|
|
4
|
-
/**
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const element = flat[id];
|
|
14
|
-
if (!element) continue;
|
|
15
|
-
collected.push(element);
|
|
16
|
-
const { items } = element.definition;
|
|
17
|
-
if (items) pending.push(...items);
|
|
18
|
-
}
|
|
19
|
-
return collected;
|
|
20
|
-
};
|
|
21
|
-
var withTimeout = async (promise, timeoutMs, label) => {
|
|
5
|
+
/**
|
|
6
|
+
* Runs one element's resolution against its budget, and CANCELS it when the budget is gone.
|
|
7
|
+
*
|
|
8
|
+
* The race decides what the payload waits for; the abort decides what the server keeps doing. Racing alone left
|
|
9
|
+
* the loser running to its own timeout — for actions, five more seconds of a held slot and an in-flight request
|
|
10
|
+
* per element, for a page that was answered without it.
|
|
11
|
+
*/
|
|
12
|
+
var withBudget = async (run, timeoutMs, label) => {
|
|
13
|
+
const controller = new AbortController();
|
|
22
14
|
let timer;
|
|
23
15
|
try {
|
|
24
|
-
return await Promise.race([
|
|
25
|
-
timer = setTimeout(() =>
|
|
16
|
+
return await Promise.race([run(controller.signal), new Promise((_resolve, reject) => {
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
controller.abort();
|
|
19
|
+
reject(/* @__PURE__ */ new Error(`RSC resolution timed out after ${timeoutMs}ms for ${label}`));
|
|
20
|
+
}, timeoutMs);
|
|
26
21
|
})]);
|
|
27
22
|
} finally {
|
|
28
23
|
if (timer) clearTimeout(timer);
|
|
@@ -37,21 +32,14 @@ var withTimeout = async (promise, timeoutMs, label) => {
|
|
|
37
32
|
*/
|
|
38
33
|
var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, resolveElement, timeoutMs = DEFAULT_ELEMENT_TIMEOUT_MS }) => {
|
|
39
34
|
if (schema.rsc?.enabled === false) return {};
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}, {});
|
|
45
|
-
const paths = getPaths(pages, schema.pageFolders, !!user);
|
|
46
|
-
const { pageId, pathMatch } = matchRoutePath(paths, req.path, !!user);
|
|
47
|
-
if (!pageId) return { serverData: {} };
|
|
48
|
-
const routeParams = pathMatch?.params ?? {};
|
|
49
|
-
const requested = ids ? new Set(ids) : void 0;
|
|
50
|
-
const targets = collectSubtree(schema.flat, pageId).filter((element) => element.definition.runtime === "server" && (!requested || requested.has(element.id)));
|
|
35
|
+
const match = matchRscPage(schema, req.path, user);
|
|
36
|
+
if (!match) return { serverData: {} };
|
|
37
|
+
const { pageId, routeParams } = match;
|
|
38
|
+
const targets = collectServerElements(schema, pageId, ids);
|
|
51
39
|
if (targets.length === 0) return { serverData: {} };
|
|
52
40
|
return { serverData: (await Promise.allSettled(targets.map(async (element) => ({
|
|
53
41
|
id: element.id,
|
|
54
|
-
data: await
|
|
42
|
+
data: await withBudget((signal) => resolveElement({
|
|
55
43
|
element,
|
|
56
44
|
flat: schema.flat,
|
|
57
45
|
routeParams,
|
|
@@ -59,7 +47,8 @@ var resolveRscData = async ({ schema, req, spaceId, environment, user, ids, reso
|
|
|
59
47
|
req,
|
|
60
48
|
spaceId,
|
|
61
49
|
environment,
|
|
62
|
-
user
|
|
50
|
+
user,
|
|
51
|
+
signal
|
|
63
52
|
}), timeoutMs, `element ${element.id}`)
|
|
64
53
|
})))).reduce((acum, result, index) => {
|
|
65
54
|
if (result.status === "rejected") {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { OfflineDataRaw, Environment, RenderMode, Server, SSRPlugin, SSRRenderResult } from '@plitzi/sdk-shared';
|
|
1
|
+
import { OfflineDataRaw, Environment, RenderMode, Server, SSRPlugin, SSRRenderResult, Theme } from '@plitzi/sdk-shared';
|
|
2
2
|
export type ComponentProps = {
|
|
3
3
|
server: Partial<Server>;
|
|
4
4
|
renderMode?: Extract<RenderMode, 'raw'>;
|
|
@@ -12,6 +12,10 @@ export type ComponentProps = {
|
|
|
12
12
|
/** Forced on when the metering adapter degrades this render; otherwise left to the SDK's own default so the
|
|
13
13
|
* markup here matches what the browser hydrates with. */
|
|
14
14
|
branding?: boolean;
|
|
15
|
+
/** The same degraded render, as the reason: the account behind this space is over its quota. */
|
|
16
|
+
overQuota?: boolean;
|
|
17
|
+
/** The theme this document was rendered with, from the visitor's cookie. See `prepareRender`. */
|
|
18
|
+
theme?: Theme;
|
|
15
19
|
};
|
|
16
|
-
declare const Component: ({ server, renderMode, previewMode, offlineData, environment, plugins, ssrResult, sdkDevToolsStylePath, debugMode, branding }: ComponentProps) => import("react").JSX.Element;
|
|
20
|
+
declare const Component: ({ server, renderMode, previewMode, offlineData, environment, plugins, ssrResult, sdkDevToolsStylePath, debugMode, branding, overQuota, theme }: ComponentProps) => import("react").JSX.Element;
|
|
17
21
|
export default Component;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import PlitziSdk from "@plitzi/plitzi-sdk";
|
|
2
2
|
import { jsx } from "react/jsx-runtime";
|
|
3
3
|
//#region src/modules/ssr/Component.tsx
|
|
4
|
-
var Component = ({ server, renderMode = "raw", previewMode = true, offlineData, environment = "main", plugins, ssrResult, sdkDevToolsStylePath, debugMode = false, branding }) => {
|
|
4
|
+
var Component = ({ server, renderMode = "raw", previewMode = true, offlineData, environment = "main", plugins, ssrResult, sdkDevToolsStylePath, debugMode = false, branding, overQuota, theme }) => {
|
|
5
5
|
const serverWithResult = {
|
|
6
6
|
...server,
|
|
7
7
|
ssr: {
|
|
@@ -19,6 +19,8 @@ var Component = ({ server, renderMode = "raw", previewMode = true, offlineData,
|
|
|
19
19
|
sdkDevToolsStylePath,
|
|
20
20
|
debugMode,
|
|
21
21
|
...branding === void 0 ? {} : { branding },
|
|
22
|
+
...overQuota === void 0 ? {} : { overQuota },
|
|
23
|
+
...theme === void 0 ? {} : { theme },
|
|
22
24
|
children: plugins && Object.keys(plugins).map((key) => /* @__PURE__ */ jsx(PlitziSdk.Plugin, {
|
|
23
25
|
renderType: key,
|
|
24
26
|
component: plugins[key].component,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { buildHtmlCacheKey } from "../../helpers/cache/keys.js";
|
|
2
|
-
import { takeDraftOverride } from "./preview.js";
|
|
2
|
+
import { draftSessionCookie, takeDraftOverride } from "./preview.js";
|
|
3
3
|
import { applySSRResult } from "./applySSRResult.js";
|
|
4
4
|
import { buildBody } from "./buildBody.js";
|
|
5
5
|
import { streamBody } from "./streamBody.js";
|
|
@@ -24,7 +24,20 @@ var renderSSR = async (req, res, config, renderFn, pluginManager, caches) => {
|
|
|
24
24
|
const { environment = "main", spaceId = 1, revision = 0 } = req.ctx.spaceDeployment || {};
|
|
25
25
|
const devMode = config.devMode ?? false;
|
|
26
26
|
res.setHeader("Content-Type", "text/html; charset=utf-8");
|
|
27
|
-
const
|
|
27
|
+
const draft = await takeDraftOverride(req, config);
|
|
28
|
+
const offlineDataOverride = draft?.data;
|
|
29
|
+
if (draft) {
|
|
30
|
+
/**
|
|
31
|
+
* Unsaved work, told not to travel.
|
|
32
|
+
*
|
|
33
|
+
* `no-store` because every layer between here and the browser would otherwise be free to hand this render to
|
|
34
|
+
* somebody else, and `noindex` because a draft URL that reaches a crawler puts unpublished copy in a search
|
|
35
|
+
* result. Neither is a nicety: the whole point of a draft is that it is not the site yet.
|
|
36
|
+
*/
|
|
37
|
+
res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
|
|
38
|
+
res.setHeader("X-Robots-Tag", "noindex, nofollow");
|
|
39
|
+
if (draft.reusable) res.setHeader("Set-Cookie", draftSessionCookie(req, draft.token, config.preview?.sessionTtlMs ?? 9e5));
|
|
40
|
+
}
|
|
28
41
|
const htmlCache = environment !== "main" && !offlineDataOverride ? caches.html : void 0;
|
|
29
42
|
const cacheKey = htmlCache ? buildHtmlCacheKey(req.ctx.user?.token, spaceId, environment, revision, req) : void 0;
|
|
30
43
|
if (htmlCache && cacheKey) {
|
|
@@ -1,7 +1,23 @@
|
|
|
1
1
|
import { pathToFileURL } from "node:url";
|
|
2
2
|
//#region src/modules/ssr/loadPluginComponents.ts
|
|
3
|
-
/** Module-level cache:
|
|
3
|
+
/** Module-level cache: the bundle's stamped identity → loaded React component. */
|
|
4
4
|
var componentCache = /* @__PURE__ */ new Map();
|
|
5
|
+
/**
|
|
6
|
+
* When the bundle behind an entry was built, taken from the URL the page is told to load.
|
|
7
|
+
*
|
|
8
|
+
* The manager stamps that URL for the browser's benefit — the assets are served `immutable`, so a rebuilt plugin
|
|
9
|
+
* needs a new URL to reach anyone — and the server needs exactly the same fact for exactly the same reason: Node's
|
|
10
|
+
* ESM registry caches a module by URL for the life of the process, and a rebuild lands back on the same path. Reading
|
|
11
|
+
* the stamp the manager already publishes keeps the two sides on one answer instead of inventing a second one.
|
|
12
|
+
*
|
|
13
|
+
* Without it a dev server renders the component it imported when it started while the browser loads the rebuilt one,
|
|
14
|
+
* and the two disagree about the markup — which React reports as a hydration mismatch and answers by throwing away
|
|
15
|
+
* the whole tree, taking the panels beside it with it.
|
|
16
|
+
*/
|
|
17
|
+
var stampOf = (entry) => {
|
|
18
|
+
const at = entry.js?.indexOf("?v=") ?? -1;
|
|
19
|
+
return at === -1 ? "" : entry.js.slice(at + 3);
|
|
20
|
+
};
|
|
5
21
|
/** Bumped on invalidation and appended to the import URL. Node's ESM registry caches a module by URL for the
|
|
6
22
|
* life of the process — clearing the Map above is not enough, since a rebuilt plugin lands back on the same
|
|
7
23
|
* path and `import()` would hand back the module loaded before it. A changed URL is the only way to re-read.
|
|
@@ -9,7 +25,7 @@ var componentCache = /* @__PURE__ */ new Map();
|
|
|
9
25
|
* subsequent one hits Node's cache. Superseded generations stay resident: the registry has no eviction, which
|
|
10
26
|
* is the price of reloading at all and why this is driven by explicit invalidation rather than per-request. */
|
|
11
27
|
var generation = 0;
|
|
12
|
-
var importUrl = (filePath) => generation === 0 ? filePath : `${pathToFileURL(filePath).href}?g=${generation}`;
|
|
28
|
+
var importUrl = (filePath, stamp) => generation === 0 && !stamp ? filePath : `${pathToFileURL(filePath).href}?v=${stamp}&g=${generation}`;
|
|
13
29
|
/**
|
|
14
30
|
* Plugins that failed to import (e.g. browser-only code like `document`).
|
|
15
31
|
* These are permanently skipped on the server and fall through to client-side rendering.
|
|
@@ -33,11 +49,12 @@ var loadPluginComponents = async (entries, inlineComponents) => {
|
|
|
33
49
|
await Promise.all(entries.filter((e) => e.filePath).map(async (e) => {
|
|
34
50
|
const filePath = e.filePath;
|
|
35
51
|
if (failedImports.has(filePath)) return;
|
|
36
|
-
|
|
52
|
+
const cacheKey = importUrl(filePath, stampOf(e));
|
|
53
|
+
let component = componentCache.get(cacheKey);
|
|
37
54
|
if (!component) try {
|
|
38
|
-
const mod = await import(
|
|
55
|
+
const mod = await import(cacheKey);
|
|
39
56
|
component = mod.default ?? mod;
|
|
40
|
-
componentCache.set(
|
|
57
|
+
componentCache.set(cacheKey, component);
|
|
41
58
|
} catch (err) {
|
|
42
59
|
console.warn(`[SSR] Plugin "${e.keyName}" cannot be imported server-side, falling back to client rendering:`, err.message);
|
|
43
60
|
failedImports.add(filePath);
|
|
@@ -1,13 +1,23 @@
|
|
|
1
|
+
import { matchRscPage } from "../rsc/matchRscPage.js";
|
|
1
2
|
import { buildOfflineDataCacheKey } from "../../helpers/cache/keys.js";
|
|
2
3
|
import { loadPluginComponents } from "./loadPluginComponents.js";
|
|
4
|
+
import "../../core/previewToken.js";
|
|
5
|
+
import { resolveActionEndpoint, resolveRscEndpoint } from "../../core/services/resolve.js";
|
|
3
6
|
import { createOfflineDataLoader } from "../../helpers/offlineDataLoader.js";
|
|
4
7
|
import { registerExternalPlugins } from "./registerExternalPlugins.js";
|
|
8
|
+
import { resolvePageSeo } from "./resolvePageSeo.js";
|
|
5
9
|
import { sdkAssetVersion } from "../../core/sdkAssets.js";
|
|
6
10
|
import { buildServerInfo } from "../../helpers/buildServerInfo.js";
|
|
7
11
|
import { escapeJson } from "../../helpers/escapeJson.js";
|
|
8
12
|
import { readCookie } from "../../helpers/readCookie.js";
|
|
9
13
|
import { resolveDebugMode } from "../../helpers/resolveDebugMode.js";
|
|
14
|
+
import { hasServerElements } from "@plitzi/sdk-shared/schema/serverElements";
|
|
15
|
+
import { themeFromCookies } from "@plitzi/sdk-shared/theme";
|
|
16
|
+
import { debugCookieName } from "@plitzi/sdk-shared/devTools";
|
|
17
|
+
import { fontUrlResolver, fontsToHead } from "@plitzi/sdk-shared/style";
|
|
10
18
|
//#region src/modules/ssr/prepareRender.tsx
|
|
19
|
+
/** Last resort only: used for a page that declares no SEO title and a deployment that supplies none either. */
|
|
20
|
+
var DEFAULT_TITLE = "Plitzi App";
|
|
11
21
|
var prepareRender = async (req, config, spaceId, environment, revision, pluginManager, offlineDataCache, metrics, offlineDataOverride) => {
|
|
12
22
|
const m = (name, fn) => metrics ? metrics.measure(name, fn) : Promise.resolve(fn());
|
|
13
23
|
const offlineCacheKey = environment !== "main" ? buildOfflineDataCacheKey(spaceId, environment, revision) : void 0;
|
|
@@ -17,18 +27,63 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
17
27
|
if (cachedOfflineStr) return Promise.resolve(JSON.parse(cachedOfflineStr));
|
|
18
28
|
return m("schema", () => config.adapters.getOfflineData(spaceId, environment, revision));
|
|
19
29
|
});
|
|
20
|
-
const
|
|
30
|
+
const offlineData = await loadOfflineData();
|
|
31
|
+
const rscPath = resolveRscEndpoint(config);
|
|
32
|
+
const schema = offlineData?.schema;
|
|
33
|
+
const pageMatch = schema !== void 0 ? matchRscPage(schema, req.path, req.ctx.user) : void 0;
|
|
34
|
+
const rscData = rscPath !== void 0 && schema !== void 0 && schema.rsc?.enabled !== false && pageMatch !== void 0 && hasServerElements(schema, pageMatch.pageId) && config.adapters.getRscData ? await m("rsc", () => config.adapters.getRscData?.({
|
|
35
|
+
req,
|
|
36
|
+
spaceId: req.ctx.spaceDeployment?.spaceId ?? spaceId,
|
|
37
|
+
environment: req.ctx.spaceDeployment?.environment ?? environment,
|
|
38
|
+
revision: req.ctx.spaceDeployment?.revision ?? revision,
|
|
39
|
+
user: req.ctx.user,
|
|
40
|
+
loadOfflineData
|
|
41
|
+
})) : rscPath ? { serverData: {} } : void 0;
|
|
42
|
+
const pageSeo = resolvePageSeo(schema, pageMatch?.pageId);
|
|
43
|
+
const server = buildServerInfo(req, config, {
|
|
44
|
+
rscPath,
|
|
45
|
+
rscData,
|
|
46
|
+
actionPath: resolveActionEndpoint(config)
|
|
47
|
+
});
|
|
21
48
|
if (offlineDataOverride === void 0 && !cachedOfflineStr && offlineCacheKey && offlineData !== void 0) offlineDataCache?.set(offlineCacheKey, JSON.stringify(offlineData));
|
|
22
49
|
const version = config.assetVersion ?? sdkAssetVersion();
|
|
23
50
|
const v = version ? `?v=${version}` : "";
|
|
24
51
|
const sdkDevToolsStylePath = `/sdk-assets/plitzi-sdk-devtools.css${v}`;
|
|
25
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Two facts, and they have to leave this server separately.
|
|
54
|
+
*
|
|
55
|
+
* `debugAuthorized` is the deployment's decision and is what the client bootstrap is handed, because on the
|
|
56
|
+
* client that argument is what arms the shortcut and the "currently hidden" console hint. Collapsing the
|
|
57
|
+
* cookie into it — which is what this used to send — made hiding the panel on an SSR page permanent: the page
|
|
58
|
+
* came back authorizing nothing, so the shortcut was dead and nothing on screen or in the console said why.
|
|
59
|
+
*
|
|
60
|
+
* `debugRendered` is what this particular render draws, preference included. The client derives the same
|
|
61
|
+
* product from the same cookie on its first pass, so the markup it hydrates matches.
|
|
62
|
+
*/
|
|
63
|
+
const debugAuthorized = !Boolean(req.query["__pt"]) && Boolean(config.debugMode ?? config.devMode);
|
|
64
|
+
const debugRendered = resolveDebugMode(debugAuthorized, readCookie(req.headers.cookie, debugCookieName(req.headers.host)));
|
|
26
65
|
const { degrade, analytics } = req.ctx.meter ?? {};
|
|
27
66
|
const clientAnalytics = analytics ? {
|
|
28
67
|
...analytics,
|
|
29
68
|
firstViewCounted: true
|
|
30
69
|
} : void 0;
|
|
31
70
|
const branding = degrade ? true : void 0;
|
|
71
|
+
const overQuota = degrade ? true : void 0;
|
|
72
|
+
/**
|
|
73
|
+
* The visitor's theme, from the cookie the SDK writes it to.
|
|
74
|
+
*
|
|
75
|
+
* This is the whole reason it is a cookie and not web storage: read here, the class goes on `<html>` in the
|
|
76
|
+
* document the server sends, so the page arrives already painted in the right theme — no blocking script in the
|
|
77
|
+
* head, and no first paint in the other theme for the SDK to correct four hundred milliseconds later.
|
|
78
|
+
*
|
|
79
|
+
* It travels to the browser too. The class alone would settle what the page LOOKS like, but a space can bind to
|
|
80
|
+
* `{{ theme.resolved }}` or gate a rule on the scheme, and a client that started at `system` while the server
|
|
81
|
+
* rendered `dark` would hydrate different markup and throw away the tree.
|
|
82
|
+
*
|
|
83
|
+
* Absent — a first visit — is not a problem to solve: nothing is stamped, `system` is what the provider starts
|
|
84
|
+
* at, and the stylesheet's media queries answer, which is exactly right.
|
|
85
|
+
*/
|
|
86
|
+
const theme = themeFromCookies(req.headers.cookie);
|
|
32
87
|
const offlineDataStr = escapeJson(JSON.stringify({
|
|
33
88
|
offlineData,
|
|
34
89
|
offlineMode: true,
|
|
@@ -36,8 +91,10 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
36
91
|
renderMode: "raw",
|
|
37
92
|
server,
|
|
38
93
|
sdkDevToolsStylePath,
|
|
94
|
+
...theme ? { theme } : {},
|
|
39
95
|
...clientAnalytics ? { analytics: clientAnalytics } : {},
|
|
40
|
-
...branding ? { branding } : {}
|
|
96
|
+
...branding ? { branding } : {},
|
|
97
|
+
...overQuota ? { overQuota } : {}
|
|
41
98
|
}));
|
|
42
99
|
const pluginNames = req.ctx.spaceDeployment?.pluginNames ?? [];
|
|
43
100
|
const pluginSources = req.ctx.spaceDeployment?.pluginSources;
|
|
@@ -55,7 +112,10 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
55
112
|
];
|
|
56
113
|
const entries = allPluginNames.length > 0 ? await pluginManager.getEntries(allPluginNames) : [];
|
|
57
114
|
const pluginComponents = await m("plugins", () => loadPluginComponents(entries, pluginManager.getComponents()));
|
|
58
|
-
const templatePlugins = entries.length > 0 ? entries : req.ctx.spaceDeployment?.templateProps?.plugins
|
|
115
|
+
const templatePlugins = (entries.length > 0 ? entries : req.ctx.spaceDeployment?.templateProps?.plugins)?.map((entry) => ({
|
|
116
|
+
...entry,
|
|
117
|
+
ssr: entry.keyName in pluginComponents
|
|
118
|
+
}));
|
|
59
119
|
const vendorJs = (config.devMode ? "/sdk-assets/plitzi-sdk-dev-vendor.js" : "/sdk-assets/plitzi-sdk-vendor.js") + v;
|
|
60
120
|
return {
|
|
61
121
|
componentProps: {
|
|
@@ -63,13 +123,15 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
63
123
|
offlineData,
|
|
64
124
|
server,
|
|
65
125
|
environment: req.ctx.spaceDeployment?.environment ?? environment,
|
|
66
|
-
debugMode,
|
|
126
|
+
debugMode: debugRendered,
|
|
67
127
|
sdkDevToolsStylePath,
|
|
68
|
-
branding
|
|
128
|
+
branding,
|
|
129
|
+
overQuota,
|
|
130
|
+
theme
|
|
69
131
|
},
|
|
70
132
|
entries,
|
|
71
133
|
templateParams: {
|
|
72
|
-
title:
|
|
134
|
+
title: DEFAULT_TITLE,
|
|
73
135
|
jsPath: `/sdk-assets/plitzi-sdk.js${v}`,
|
|
74
136
|
cssPath: `/sdk-assets/plitzi-sdk.css${v}`,
|
|
75
137
|
react: vendorJs,
|
|
@@ -77,9 +139,26 @@ var prepareRender = async (req, config, spaceId, environment, revision, pluginMa
|
|
|
77
139
|
reactDom: vendorJs,
|
|
78
140
|
reactDomClient: vendorJs,
|
|
79
141
|
reactCompilerRuntime: vendorJs,
|
|
142
|
+
/**
|
|
143
|
+
* The class this document wears, straight onto `<html>` — see `theme` above.
|
|
144
|
+
*
|
|
145
|
+
* `system` and "never chose" both write nothing, and the silence is the mechanism: the stylesheet's
|
|
146
|
+
* `prefers-color-scheme` queries are guarded on the absence of these classes, so stamping one would freeze the
|
|
147
|
+
* page against the machine it is running on.
|
|
148
|
+
*/
|
|
149
|
+
themeClass: theme && theme !== "system" ? theme : void 0,
|
|
150
|
+
/**
|
|
151
|
+
* The space's own families, requested by the document itself.
|
|
152
|
+
*
|
|
153
|
+
* Ahead of the deployment's `templateProps` because a deployment cannot know them: they are a fact about the
|
|
154
|
+
* space's style document, and a page whose text is laid out in a face the browser has not been asked for is
|
|
155
|
+
* a page that renders in a fallback and then reflows.
|
|
156
|
+
*/
|
|
157
|
+
fonts: fontsToHead(offlineData?.style.fonts ?? [], fontUrlResolver(config.fonts?.baseUrl)),
|
|
80
158
|
...req.ctx.spaceDeployment?.templateProps,
|
|
159
|
+
...pageSeo,
|
|
81
160
|
plugins: templatePlugins,
|
|
82
|
-
debugMode,
|
|
161
|
+
debugMode: debugAuthorized,
|
|
83
162
|
ssrOnly: config.ssrOnly === true,
|
|
84
163
|
offlineData: offlineDataStr
|
|
85
164
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { DraftStore, OfflineDataRaw, SSRRequest, SSRServerConfig } from '@plitzi/sdk-shared';
|
|
2
|
-
/** The READ side of draft-preview: a render resolving a stashed draft from its
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
/** In-memory
|
|
6
|
-
*
|
|
2
|
+
/** The READ side of draft-preview: a render resolving a stashed draft from its token. The write side — applying
|
|
3
|
+
* unsaved operations and rendering the result — lives in `@plitzi/sdk-mcp`, which is what knows how to interpret an
|
|
4
|
+
* operation; a page-only deployment keeps this much and never loads that. */
|
|
5
|
+
/** In-memory draft store — the default when the consumer injects none. Fine for a single replica; a multi-replica
|
|
6
|
+
* deployment must inject a shared store so a preview URL resolves on any replica. */
|
|
7
7
|
export declare const createMemoryDraftStore: () => DraftStore;
|
|
8
|
-
/**
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
/** What a draft render has to say about itself, so nothing downstream has to ask the store a second time. */
|
|
9
|
+
export type DraftOverride = {
|
|
10
|
+
data: OfflineDataRaw;
|
|
11
|
+
token: string;
|
|
12
|
+
/** Whether the token survived the read — which is what decides if the visit is worth remembering in a cookie. */
|
|
13
|
+
reusable: boolean;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Resolve the draft this request is looking at, if any.
|
|
17
|
+
*
|
|
18
|
+
* Returns undefined on a normal request (renders persisted state) and on a token the store no longer has — expired,
|
|
19
|
+
* dropped, or a one-shot that has already been rendered once.
|
|
20
|
+
*/
|
|
21
|
+
export declare const takeDraftOverride: (req: SSRRequest, config: SSRServerConfig) => Promise<DraftOverride | undefined>;
|
|
22
|
+
/** The `Set-Cookie` that carries a draft session through the rest of the visit. */
|
|
23
|
+
export declare const draftSessionCookie: (req: SSRRequest, token: string, ttlMs: number) => string;
|
|
@@ -1,33 +1,57 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { PREVIEW_COOKIE, readDraftToken } from "../../core/previewToken.js";
|
|
2
2
|
//#region src/modules/ssr/preview.ts
|
|
3
|
-
/** The READ side of draft-preview: a render resolving a stashed draft from its
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
/** In-memory
|
|
7
|
-
*
|
|
3
|
+
/** The READ side of draft-preview: a render resolving a stashed draft from its token. The write side — applying
|
|
4
|
+
* unsaved operations and rendering the result — lives in `@plitzi/sdk-mcp`, which is what knows how to interpret an
|
|
5
|
+
* operation; a page-only deployment keeps this much and never loads that. */
|
|
6
|
+
/** In-memory draft store — the default when the consumer injects none. Fine for a single replica; a multi-replica
|
|
7
|
+
* deployment must inject a shared store so a preview URL resolves on any replica. */
|
|
8
8
|
var createMemoryDraftStore = () => {
|
|
9
9
|
const store = /* @__PURE__ */ new Map();
|
|
10
10
|
return {
|
|
11
|
-
put(token, data, ttlMs) {
|
|
11
|
+
put(token, data, { ttlMs, reusable = false }) {
|
|
12
12
|
store.set(token, {
|
|
13
13
|
data,
|
|
14
|
-
expiresAt: Date.now() + ttlMs
|
|
14
|
+
expiresAt: Date.now() + ttlMs,
|
|
15
|
+
reusable
|
|
15
16
|
});
|
|
16
17
|
},
|
|
17
18
|
take(token) {
|
|
18
19
|
const hit = store.get(token);
|
|
19
20
|
if (!hit) return;
|
|
21
|
+
if (!hit.reusable || hit.expiresAt <= Date.now()) store.delete(token);
|
|
22
|
+
return hit.expiresAt > Date.now() ? {
|
|
23
|
+
data: hit.data,
|
|
24
|
+
reusable: hit.reusable
|
|
25
|
+
} : void 0;
|
|
26
|
+
},
|
|
27
|
+
drop(token) {
|
|
20
28
|
store.delete(token);
|
|
21
|
-
return hit.expiresAt > Date.now() ? hit.data : void 0;
|
|
22
29
|
}
|
|
23
30
|
};
|
|
24
31
|
};
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the draft this request is looking at, if any.
|
|
34
|
+
*
|
|
35
|
+
* Returns undefined on a normal request (renders persisted state) and on a token the store no longer has — expired,
|
|
36
|
+
* dropped, or a one-shot that has already been rendered once.
|
|
37
|
+
*/
|
|
27
38
|
var takeDraftOverride = async (req, config) => {
|
|
28
|
-
const token = req
|
|
39
|
+
const token = readDraftToken(req);
|
|
29
40
|
if (!token || !config.draftStore) return;
|
|
30
|
-
|
|
41
|
+
const entry = await config.draftStore.take(token);
|
|
42
|
+
return entry ? {
|
|
43
|
+
...entry,
|
|
44
|
+
token
|
|
45
|
+
} : void 0;
|
|
31
46
|
};
|
|
47
|
+
/** The `Set-Cookie` that carries a draft session through the rest of the visit. */
|
|
48
|
+
var draftSessionCookie = (req, token, ttlMs) => [
|
|
49
|
+
`${PREVIEW_COOKIE}=${encodeURIComponent(token)}`,
|
|
50
|
+
"Path=/",
|
|
51
|
+
"HttpOnly",
|
|
52
|
+
"SameSite=Lax",
|
|
53
|
+
`Max-Age=${Math.max(0, Math.floor(ttlMs / 1e3))}`,
|
|
54
|
+
...req.protocol === "https" ? ["Secure"] : []
|
|
55
|
+
].join("; ");
|
|
32
56
|
//#endregion
|
|
33
|
-
export { createMemoryDraftStore, takeDraftOverride };
|
|
57
|
+
export { createMemoryDraftStore, draftSessionCookie, takeDraftOverride };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
|
-
import crypto from "node:crypto";
|
|
4
4
|
//#region src/modules/ssr/registerExternalPlugins.ts
|
|
5
5
|
var MANIFEST_TTL_MS = 6e5;
|
|
6
6
|
var manifestCache = /* @__PURE__ */ new Map();
|
|
@@ -86,7 +86,7 @@ var registerPlugin = async (pluginManager, plugin) => {
|
|
|
86
86
|
const source = {
|
|
87
87
|
js: jsUrl,
|
|
88
88
|
css: findAsset(manifest, "style", plugin.resource),
|
|
89
|
-
action: "
|
|
89
|
+
action: "cdn",
|
|
90
90
|
version: manifest.version
|
|
91
91
|
};
|
|
92
92
|
return pluginManager.ensure(plugin.type, source);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Schema } from '@plitzi/sdk-shared';
|
|
2
|
+
export type PageSeo = {
|
|
3
|
+
title?: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* The title and description the addressed page declares for itself.
|
|
8
|
+
*
|
|
9
|
+
* The page element has carried `seoEnabled` / `seoPageTitle` / `seoPageDescription` all along — the builder edits
|
|
10
|
+
* them and the client renders them into the head through Helmet — but the SSR document was built from a constant,
|
|
11
|
+
* so every server-rendered page arrived titled the same. That is the half a crawler and a link preview see, which
|
|
12
|
+
* is the half that matters: they read the document as delivered and never run the client.
|
|
13
|
+
*
|
|
14
|
+
* `seoEnabled: false` returns nothing rather than a default, so the deployment's own title stays in charge of a
|
|
15
|
+
* page that opted out. Blank strings are treated as absent for the same reason — the builder writes one for a
|
|
16
|
+
* field the author cleared, and an empty <title> is worse than a generic one.
|
|
17
|
+
*/
|
|
18
|
+
export declare const resolvePageSeo: (schema: Schema | undefined, pageId: string | undefined) => PageSeo;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region src/modules/ssr/resolvePageSeo.ts
|
|
2
|
+
/**
|
|
3
|
+
* The title and description the addressed page declares for itself.
|
|
4
|
+
*
|
|
5
|
+
* The page element has carried `seoEnabled` / `seoPageTitle` / `seoPageDescription` all along — the builder edits
|
|
6
|
+
* them and the client renders them into the head through Helmet — but the SSR document was built from a constant,
|
|
7
|
+
* so every server-rendered page arrived titled the same. That is the half a crawler and a link preview see, which
|
|
8
|
+
* is the half that matters: they read the document as delivered and never run the client.
|
|
9
|
+
*
|
|
10
|
+
* `seoEnabled: false` returns nothing rather than a default, so the deployment's own title stays in charge of a
|
|
11
|
+
* page that opted out. Blank strings are treated as absent for the same reason — the builder writes one for a
|
|
12
|
+
* field the author cleared, and an empty <title> is worse than a generic one.
|
|
13
|
+
*/
|
|
14
|
+
var resolvePageSeo = (schema, pageId) => {
|
|
15
|
+
if (!schema || !pageId) return {};
|
|
16
|
+
const page = schema.flat[pageId];
|
|
17
|
+
if (!page?.attributes.seoEnabled) return {};
|
|
18
|
+
const title = page.attributes.seoPageTitle?.trim();
|
|
19
|
+
const description = page.attributes.seoPageDescription?.trim();
|
|
20
|
+
return {
|
|
21
|
+
...title ? { title } : {},
|
|
22
|
+
...description ? { description } : {}
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
//#endregion
|
|
26
|
+
export { resolvePageSeo };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|