@k2b/cloud 0.37.0 → 0.39.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/package.json +8 -3
- package/scripts/build-canvas-workers.ts +109 -29
- package/scripts/build.ts +3 -5
- package/src/_internal/app-path.ts +25 -0
- package/src/_internal/app-presentation.ts +4 -0
- package/src/_internal/canvas-worker.ts +5 -3
- package/src/_internal/define-app.ts +28 -2
- package/src/_internal/help-catalog.ts +123 -4
- package/src/_internal/help.ts +27 -6
- package/src/_internal/panel-cookie.ts +39 -0
- package/src/_internal/registry-validation.ts +21 -26
- package/src/_internal/runtime-context.ts +1 -0
- package/src/access/PermissionEditor.browser-harness.tsx +57 -38
- package/src/access/PermissionEditor.tsx +30 -5
- package/src/access/ResourceApiKeys.tsx +5 -1
- package/src/account/Avatar.tsx +20 -2
- package/src/account/ui.ts +1 -1
- package/src/ai/approval-routes.ts +5 -2
- package/src/ai/approvals.ts +139 -28
- package/src/ai/browser.ts +11 -0
- package/src/ai/chart-block.ts +44 -13
- package/src/ai/chat/action-sentences.ts +1 -44
- package/src/ai/chat/chart-block.tsx +2 -11
- package/src/ai/chat/messages.ts +10 -0
- package/src/ai/chat/turn-view.tsx +35 -3
- package/src/ai/chat/view-block.tsx +7 -2
- package/src/ai/chat/web-tools.tsx +23 -6
- package/src/ai/chat-task-contracts.ts +28 -3
- package/src/ai/chat-task-reasons-migrate.ts +68 -0
- package/src/ai/chat-task-reasons.ts +92 -0
- package/src/ai/chat-tasks.ts +460 -66
- package/src/ai/default-tool-contracts.ts +30 -13
- package/src/ai/fetch-file-tool.ts +37 -3
- package/src/ai/index.ts +12 -0
- package/src/ai/migrate.ts +46 -3
- package/src/ai/readable-html.ts +1057 -0
- package/src/ai/request-context.ts +7 -1
- package/src/ai/routes.ts +27 -1
- package/src/ai/runtime.ts +12 -27
- package/src/ai/store.ts +5 -3
- package/src/ai/timeline.ts +9 -1
- package/src/ai/tool-wording.ts +45 -0
- package/src/ai/turn-abort.ts +33 -0
- package/src/ai/types.ts +12 -1
- package/src/api/admin-core-settings.ts +190 -139
- package/src/api/admin-outgoing-mail.ts +4 -2
- package/src/api/app-approval.ts +9 -2
- package/src/api/approvals.ts +1 -1
- package/src/api/help.ts +23 -4
- package/src/api/index.ts +2 -1
- package/src/api/mcp.ts +8 -2
- package/src/browser/CloudResourceSearch.tsx +10 -6
- package/src/browser/FileChooser.tsx +11 -3
- package/src/browser/app-approval.ts +7 -2
- package/src/browser/choose-files.browser-harness.tsx +4 -1
- package/src/browser/composer-catalog.ts +103 -0
- package/src/browser/composer-insert-messages.ts +31 -0
- package/src/browser/composer-insert.ts +150 -0
- package/src/browser/file-chooser-messages.ts +4 -0
- package/src/browser/file-providers.ts +14 -1
- package/src/browser/panel-messages.ts +24 -0
- package/src/browser/panel-slot.ts +354 -0
- package/src/browser/panel.ts +28 -0
- package/src/browser/provider-folder.tsx +58 -5
- package/src/browser/resource-picker.tsx +17 -2
- package/src/cli/admin/workflows.ts +1 -0
- package/src/config/env.ts +14 -0
- package/src/contracts/app-approval.ts +49 -9
- package/src/contracts/app.ts +14 -0
- package/src/contracts/file-provider.ts +48 -1
- package/src/contracts/outgoing-mail.ts +8 -1
- package/src/contracts/registry.ts +3 -1
- package/src/contracts/shared.ts +2 -0
- package/src/server/help.ts +136 -10
- package/src/server/services/access.ts +5 -1
- package/src/server/services/geo.ts +21 -4
- package/src/services/accounts/posix.ts +2 -1
- package/src/services/allowed-network-fetch.ts +189 -0
- package/src/services/app-approval.ts +158 -38
- package/src/services/approvals/device.ts +2 -0
- package/src/services/help/index.ts +46 -5
- package/src/services/help/store.ts +26 -5
- package/src/services/help/types.ts +10 -2
- package/src/services/index.ts +22 -3
- package/src/services/notifications/browser.ts +16 -5
- package/src/services/outgoing-mail/dispatcher.ts +7 -2
- package/src/services/outgoing-mail/resolve-host.ts +39 -0
- package/src/services/outgoing-mail/test-send.ts +18 -3
- package/src/services/outgoing-mail/transport.ts +24 -13
- package/src/services/pdf/image-protocol.ts +135 -0
- package/src/services/pdf/image-variants.ts +124 -0
- package/src/services/pdf/image-worker.ts +31 -0
- package/src/services/pdf/image.ts +109 -0
- package/src/services/pdf/index.ts +9 -0
- package/src/services/settings/api.ts +11 -6
- package/src/services/settings/app.ts +5 -4
- package/src/services/settings/core-settings.ts +4 -3
- package/src/services/settings/index.ts +67 -43
- package/src/services/settings/namespace.ts +2 -1
- package/src/services/settings/store.ts +83 -11
- package/src/shared/ai-platform-prompt.ts +1 -1
- package/src/shared/app-presentation.ts +1 -0
- package/src/shared/help.ts +41 -0
- package/src/shared/index.ts +9 -1
- package/src/shared/markdown/extensions/guided-help.ts +208 -64
- package/src/shared/markdown/formula.ts +8 -8
- package/src/shared/markdown/index.ts +72 -11
- package/src/shared/redirect.ts +12 -0
- package/src/ssr/CloudShell.island.tsx +86 -9
- package/src/ssr/Layout.tsx +62 -11
- package/src/ssr/LayoutHeader.tsx +15 -9
- package/src/ssr/LayoutHelp.browser-harness.tsx +130 -27
- package/src/ssr/LayoutHelp.tsx +569 -410
- package/src/ssr/LayoutRail.tsx +2 -2
- package/src/ssr/LayoutTabBar.tsx +29 -0
- package/src/ssr/PanelLayout.tsx +27 -0
- package/src/ssr/PanelRuntime.island.tsx +87 -0
- package/src/ssr/PanelSlot.tsx +108 -0
- package/src/ssr/PanelTrigger.tsx +49 -0
- package/src/ssr/ShellRuntime.island.tsx +72 -25
- package/src/ssr/app-appearance.ts +3 -2
- package/src/ssr/app-color.ts +100 -0
- package/src/ssr/app-launchpad.tsx +18 -1
- package/src/ssr/app-navigation.ts +15 -0
- package/src/ssr/compact-shell.baseline.json +5270 -0
- package/src/ssr/compact-shell.browser-harness.ts +431 -0
- package/src/ssr/help-messages.ts +60 -38
- package/src/ssr/help.ts +3 -0
- package/src/ssr/index.ts +3 -0
- package/src/ssr/layout-help-article.ts +41 -0
- package/src/ssr/layout-help-navigation.ts +0 -12
- package/src/ssr/layout-help-registry.tsx +4 -11
- package/src/ssr/layout-help-search.ts +9 -12
- package/src/ssr/layout-help-start.ts +68 -0
- package/src/ssr/panel-slot.browser-harness.tsx +110 -0
- package/src/ssr/platform-messages.ts +34 -0
- package/src/ssr/rail-navigation.ts +44 -0
- package/src/ssr/shell-data.tsx +11 -0
- package/src/ssr/shell-shortcuts.ts +19 -0
- package/src/styles/effects.css +689 -185
- package/src/styles/global.css +269 -55
- package/src/styles/panel.css +373 -0
- package/src/styles/resource-search.css +25 -0
- package/src/styles/tokens.css +23 -6
- package/src/styles/utilities-navigation.css +5 -21
- package/src/workflows/store/events.ts +24 -0
- package/src/workflows/store/index.ts +8 -0
- package/src/workflows/store/observability.ts +6 -1
- package/src/workflows/store/retention.ts +157 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@k2b/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"description": "Application platform library for independently deployed Hono and SolidJS services behind a dynamic gateway.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"repository": {
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"!src/**/*.test.tsx",
|
|
20
20
|
"!src/**/*.integration.test.ts",
|
|
21
21
|
"!src/**/*.test-fixture.ts",
|
|
22
|
+
"!src/**/*.test-fixture.tsx",
|
|
22
23
|
"!scripts/**/*.test.ts",
|
|
23
24
|
"!scripts/runtime-recovery-*.ts"
|
|
24
25
|
],
|
|
@@ -37,10 +38,12 @@
|
|
|
37
38
|
"./browser/notifications": "./src/browser/notifications.ts",
|
|
38
39
|
"./browser/resource-clipboard": "./src/browser/resource-clipboard.ts",
|
|
39
40
|
"./browser/commands": "./src/browser/commands.ts",
|
|
41
|
+
"./browser/panel": "./src/browser/panel.ts",
|
|
40
42
|
"./browser/search": "./src/browser/search.ts",
|
|
41
43
|
"./browser/widgets": "./src/browser/widgets.ts",
|
|
42
44
|
"./browser/testing": "./src/browser/testing.ts",
|
|
43
45
|
"./browser/resource-picker": "./src/browser/resource-picker.tsx",
|
|
46
|
+
"./browser/composer-insert": "./src/browser/composer-insert.ts",
|
|
44
47
|
"./browser/files": "./src/browser/files.tsx",
|
|
45
48
|
"./capabilities": "./src/capabilities/client.ts",
|
|
46
49
|
"./capabilities/server": "./src/capabilities/server.ts",
|
|
@@ -102,12 +105,13 @@
|
|
|
102
105
|
"@firecrawl/anydoc": "0.2.4",
|
|
103
106
|
"@napi-rs/canvas": "1.0.9",
|
|
104
107
|
"pdfjs-dist": "6.3.289",
|
|
108
|
+
"sharp": "0.35.4",
|
|
105
109
|
"@simplewebauthn/server": "14.0.3",
|
|
106
110
|
"@tabler/icons": "3.48.0",
|
|
107
111
|
"@tailwindcss/typography": "0.5.20",
|
|
108
112
|
"@k2b/nessi": "0.18.0",
|
|
109
113
|
"@k2b/ssr": "0.17.0",
|
|
110
|
-
"@k2b/ui": "0.
|
|
114
|
+
"@k2b/ui": "0.29.0",
|
|
111
115
|
"@k2b/stdlib": "0.29.0",
|
|
112
116
|
"@k2b/sync": "7.0.0",
|
|
113
117
|
"@nats-io/jetstream": "3.4.0",
|
|
@@ -115,6 +119,7 @@
|
|
|
115
119
|
"@modelcontextprotocol/sdk": "1.32.1",
|
|
116
120
|
"bun-plugin-tailwind": "0.1.2",
|
|
117
121
|
"hono-openapi": "1.3.3",
|
|
122
|
+
"htmlparser2": "12.0.0",
|
|
118
123
|
"jose": "6.2.12",
|
|
119
124
|
"katex": "0.18.9",
|
|
120
125
|
"liquidjs": "10.29.0",
|
|
@@ -139,7 +144,7 @@
|
|
|
139
144
|
"@types/bun": "1.4.2",
|
|
140
145
|
"babel-preset-solid": "1.9.15",
|
|
141
146
|
"hono": "4.13.9",
|
|
142
|
-
"playwright": "1.
|
|
147
|
+
"playwright": "1.64.1",
|
|
143
148
|
"solid-js": "1.9.15",
|
|
144
149
|
"typescript": "7.0.2",
|
|
145
150
|
"zod": "4.6.5"
|
|
@@ -1,58 +1,138 @@
|
|
|
1
|
-
import { cp, mkdir } from "node:fs/promises";
|
|
2
|
-
import { dirname, resolve } from "node:path";
|
|
1
|
+
import { cp, mkdir, realpath } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, resolve } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { readImageRecords } from "../src/services/pdf/image-protocol";
|
|
5
6
|
|
|
6
|
-
/** Isolated native canvas workers, each packaged only when the server bundle reaches the module that starts it. */
|
|
7
7
|
const CANVAS_WORKERS = [
|
|
8
|
-
{ reachedBy: "/ai/pdf-render.ts", entry: "src/ai/pdf-render-worker.ts", output: "pdf-worker.js", packages: ["pdfjs-dist"] },
|
|
9
8
|
{
|
|
10
|
-
|
|
9
|
+
define: "__CLOUD_PDF_RENDER_WORKER__",
|
|
10
|
+
entry: "src/ai/pdf-render-worker.ts",
|
|
11
|
+
output: "pdf-worker.js",
|
|
12
|
+
packages: ["@napi-rs/canvas", "pdfjs-dist"],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
define: "__CLOUD_ICON_RENDER_WORKER__",
|
|
11
16
|
entry: "src/services/branding/icon-render-worker.ts",
|
|
12
17
|
output: "icon-worker.js",
|
|
13
|
-
packages: [],
|
|
18
|
+
packages: ["@napi-rs/canvas"],
|
|
14
19
|
},
|
|
20
|
+
{ define: "__CLOUD_PDF_IMAGE_WORKER__", entry: "src/services/pdf/image-worker.ts", output: "image-worker.js", packages: ["sharp"] },
|
|
15
21
|
] as const;
|
|
16
22
|
|
|
23
|
+
/** Server bundle defines: each caller starts its worker from `canvas/` next to `server.js`. */
|
|
24
|
+
export const CANVAS_WORKER_DEFINES = Object.fromEntries(
|
|
25
|
+
CANVAS_WORKERS.map((worker) => [worker.define, JSON.stringify(`./canvas/${worker.output}`)]),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const Manifest = z.object({
|
|
29
|
+
version: z.string(),
|
|
30
|
+
dependencies: z.record(z.string(), z.string()).default({}),
|
|
31
|
+
optionalDependencies: z.record(z.string(), z.string()).default({}),
|
|
32
|
+
os: z.array(z.string()).optional(),
|
|
33
|
+
cpu: z.array(z.string()).optional(),
|
|
34
|
+
libc: z.array(z.string()).optional(),
|
|
35
|
+
});
|
|
36
|
+
const matches = (values: string[] | undefined, target: string) =>
|
|
37
|
+
!values || (!values.includes(`!${target}`) && (values.every((value) => value.startsWith("!")) || values.includes(target)));
|
|
38
|
+
|
|
17
39
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
40
|
+
* Bundle each reached native worker with only its runtime packages external. Copy their real dependency closures flat,
|
|
41
|
+
* dereferenced and filtered by OS, CPU and libc; reject version collisions and missing native binaries. Smoke-test sharp
|
|
42
|
+
* in the packaged subprocess so the build proves that its native closure loads on this architecture.
|
|
20
43
|
*/
|
|
21
|
-
export async function buildCanvasWorkers(dist: string,
|
|
22
|
-
|
|
44
|
+
export async function buildCanvasWorkers(dist: string, serverCode: string) {
|
|
45
|
+
// A worker ships only when the server code still contains its path, which only the code that starts it carries.
|
|
46
|
+
// Metafile inputs also list tree-shaken modules, and a retained module binding contributes bytes without its caller.
|
|
47
|
+
const workers = CANVAS_WORKERS.filter((worker) => serverCode.includes(`./canvas/${worker.output}`));
|
|
23
48
|
if (workers.length === 0) return;
|
|
24
49
|
const framework = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
50
|
const out = resolve(dist, "canvas");
|
|
26
51
|
await mkdir(out, { recursive: true });
|
|
27
|
-
const external = ["@napi-rs/canvas", ...workers.flatMap((worker) => worker.packages)];
|
|
28
52
|
for (const worker of workers) {
|
|
29
53
|
const build = await Bun.build({
|
|
30
54
|
entrypoints: [resolve(framework, worker.entry)],
|
|
31
55
|
target: "bun",
|
|
32
56
|
outdir: out,
|
|
33
57
|
naming: worker.output,
|
|
34
|
-
external,
|
|
58
|
+
external: [...worker.packages],
|
|
35
59
|
});
|
|
36
|
-
if (!build.success) throw new Error(`
|
|
60
|
+
if (!build.success) throw new Error(`Native worker ${worker.output} build failed: ${build.logs.join("\n")}`);
|
|
37
61
|
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
62
|
+
const libc = z.object({ header: z.object({ glibcVersionRuntime: z.string().optional() }) }).parse(process.report.getReport()).header
|
|
63
|
+
.glibcVersionRuntime
|
|
64
|
+
? "glibc"
|
|
65
|
+
: "musl";
|
|
66
|
+
const copied = new Map<string, string>();
|
|
67
|
+
const copyPackage = async (name: string, dependent: string, optional = false): Promise<boolean> => {
|
|
68
|
+
let source: string;
|
|
69
|
+
try {
|
|
70
|
+
source = await realpath(dirname(Bun.resolveSync(`${name}/package.json`, dependent)));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (optional) return false;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
const manifest = Manifest.parse(await Bun.file(resolve(source, "package.json")).json());
|
|
76
|
+
if (!matches(manifest.os, process.platform) || !matches(manifest.cpu, process.arch) || !matches(manifest.libc, libc)) {
|
|
77
|
+
if (optional) return false;
|
|
78
|
+
throw new Error(`Native worker dependency ${name} does not support the build platform.`);
|
|
79
|
+
}
|
|
80
|
+
const existing = copied.get(name);
|
|
81
|
+
if (existing) {
|
|
82
|
+
if (existing !== manifest.version)
|
|
83
|
+
throw new Error(`Native worker dependency collision: ${name} ${existing} and ${manifest.version}.`);
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
copied.set(name, manifest.version);
|
|
87
|
+
await cp(source, resolve(out, "node_modules", name), {
|
|
88
|
+
recursive: true,
|
|
89
|
+
dereference: true,
|
|
90
|
+
filter: (path) => basename(path) !== "node_modules",
|
|
91
|
+
});
|
|
92
|
+
for (const dependency of Object.keys(manifest.dependencies)) {
|
|
93
|
+
if (dependency in manifest.optionalDependencies) continue;
|
|
94
|
+
await copyPackage(dependency, source);
|
|
95
|
+
}
|
|
45
96
|
let nativePackages = 0;
|
|
46
97
|
for (const dependency of Object.keys(manifest.optionalDependencies)) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
98
|
+
if (await copyPackage(dependency, source, true)) {
|
|
99
|
+
if (
|
|
100
|
+
(name === "sharp" && dependency.startsWith("@img/sharp-") && !dependency.startsWith("@img/sharp-libvips-")) ||
|
|
101
|
+
(name === "@napi-rs/canvas" && dependency.startsWith("@napi-rs/canvas-"))
|
|
102
|
+
)
|
|
103
|
+
nativePackages++;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if ((name === "sharp" || name === "@napi-rs/canvas") && !nativePackages) {
|
|
107
|
+
throw new Error(`Native workers need an installed ${name} binary for the build platform.`);
|
|
108
|
+
}
|
|
109
|
+
return true;
|
|
110
|
+
};
|
|
111
|
+
for (const name of new Set(workers.flatMap((worker) => [...worker.packages]))) await copyPackage(name, framework);
|
|
112
|
+
if (workers.some((worker) => worker.output === "image-worker.js")) await smokeImageWorker(resolve(out, "image-worker.js"));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function smokeImageWorker(entry: string) {
|
|
116
|
+
// A fixed 2x2 RGBA PNG; the build process never loads sharp.
|
|
117
|
+
const png = Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAAEUlEQVR4nGP4z8DQAMIMMAYAOOgF/REzMMkAAAAASUVORK5CYII=", "base64");
|
|
118
|
+
const child = Bun.spawn([process.execPath, "--no-env-file", entry], {
|
|
119
|
+
stdin: new Blob([
|
|
120
|
+
`${JSON.stringify({ bytes: png.byteLength, boxes: [{ width: 2, height: 2 }], maxBytes: 4096, timeoutMs: 10_000 })}\n`,
|
|
121
|
+
png,
|
|
122
|
+
]),
|
|
123
|
+
stdout: "pipe",
|
|
124
|
+
stderr: "ignore",
|
|
125
|
+
env: { PATH: process.env.PATH },
|
|
126
|
+
});
|
|
127
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 15_000);
|
|
128
|
+
try {
|
|
129
|
+
const result = await readImageRecords(child.stdout, { boxes: [{ width: 2, height: 2 }], maxBytes: 4096 });
|
|
130
|
+
if ((await child.exited) !== 0 || !result.ok || result.variants.length !== 1 || !result.variants[0]) {
|
|
131
|
+
throw new Error("Packaged image worker could not convert its PNG: sharp's native dependency closure is incomplete or unusable.");
|
|
55
132
|
}
|
|
56
|
-
|
|
133
|
+
} finally {
|
|
134
|
+
clearTimeout(timer);
|
|
135
|
+
child.kill("SIGKILL");
|
|
136
|
+
await child.exited;
|
|
57
137
|
}
|
|
58
138
|
}
|
package/scripts/build.ts
CHANGED
|
@@ -35,7 +35,7 @@ import tailwind from "bun-plugin-tailwind";
|
|
|
35
35
|
import type { AppCliModules } from "../src/contracts/app";
|
|
36
36
|
import { writeAppFavicon } from "./app-favicon";
|
|
37
37
|
import { buildBrowserPerformance } from "./browser-performance";
|
|
38
|
-
import { buildCanvasWorkers } from "./build-canvas-workers";
|
|
38
|
+
import { buildCanvasWorkers, CANVAS_WORKER_DEFINES } from "./build-canvas-workers";
|
|
39
39
|
|
|
40
40
|
const appId = process.env.APP_ID;
|
|
41
41
|
if (!appId) throw new Error("APP_ID env var required");
|
|
@@ -127,13 +127,11 @@ try {
|
|
|
127
127
|
naming: "server.js",
|
|
128
128
|
target: "bun",
|
|
129
129
|
minify: true,
|
|
130
|
-
metafile: true,
|
|
131
130
|
define: {
|
|
132
131
|
__CLOUD_VERSION__: JSON.stringify(version),
|
|
133
132
|
__CLOUD_RELEASE__: JSON.stringify(release),
|
|
134
133
|
__CLOUD_SYNC_VERSION__: JSON.stringify(syncVersion),
|
|
135
|
-
|
|
136
|
-
__CLOUD_ICON_RENDER_WORKER__: JSON.stringify("./canvas/icon-worker.js"),
|
|
134
|
+
...CANVAS_WORKER_DEFINES,
|
|
137
135
|
__CLOUD_APP_ASSETS__: JSON.stringify("./assets/"),
|
|
138
136
|
__CLOUD_CLI_PLUGINS__: JSON.stringify("./cli/"),
|
|
139
137
|
},
|
|
@@ -146,7 +144,7 @@ if (!server.success) {
|
|
|
146
144
|
for (const m of server.logs) console.error(m);
|
|
147
145
|
throw new Error("Server bundle failed");
|
|
148
146
|
}
|
|
149
|
-
await buildCanvasWorkers(dist,
|
|
147
|
+
await buildCanvasWorkers(dist, await Bun.file(resolve(dist, "server.js")).text());
|
|
150
148
|
|
|
151
149
|
// 2. Per-app Tailwind stylesheet.
|
|
152
150
|
const appCss = resolve(appDir, "src/styles/app.css");
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** An absolute path on the current origin: one leading slash, no backslash or whitespace. */
|
|
2
|
+
export const SAME_ORIGIN_PATH = /^\/(?![\/\\])[^\\\s]*$/;
|
|
3
|
+
|
|
4
|
+
/** A route as the gateway reads it: trimmed and without empty segments, so `//pwa/x/` is `/pwa/x`. */
|
|
5
|
+
export const canonicalRoute = (route: string): string => {
|
|
6
|
+
const trimmed = route.trim();
|
|
7
|
+
return trimmed.startsWith("/") ? `/${trimmed.split("/").filter(Boolean).join("/")}` : trimmed;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Whether an application path stays below one of the app's own `routes`. It is
|
|
12
|
+
* checked on the path the browser actually requests, after dot segments, and
|
|
13
|
+
* never starts with `//`, which a URL resolver reads as another host.
|
|
14
|
+
*/
|
|
15
|
+
export const isOwnAppPath = (href: string, routes: readonly string[]): boolean => {
|
|
16
|
+
if (!SAME_ORIGIN_PATH.test(href)) return false;
|
|
17
|
+
const path = new URL(href, "https://cloud.invalid").pathname;
|
|
18
|
+
return (
|
|
19
|
+
!path.startsWith("//") &&
|
|
20
|
+
routes.some((route) => {
|
|
21
|
+
const prefix = canonicalRoute(route);
|
|
22
|
+
return prefix === "/" || path === prefix || path.startsWith(`${prefix}/`);
|
|
23
|
+
})
|
|
24
|
+
);
|
|
25
|
+
};
|
|
@@ -92,7 +92,11 @@ export const compileAppPresentation = (app: AppMeta, catalog: AppPresentationCat
|
|
|
92
92
|
if (!canonical) throw new Error(`App presentation locale ${JSON.stringify(locale)} is not a valid BCP 47 locale`);
|
|
93
93
|
if (canonical === baseLocale) throw new Error(`App presentation translations must not repeat base locale ${baseLocale}`);
|
|
94
94
|
if (translations[canonical]) throw new Error(`App presentation locale ${canonical} is duplicated after canonicalization`);
|
|
95
|
+
if (translation.panelLabel !== undefined && !app.panel) {
|
|
96
|
+
throw new Error(`${canonical}.panelLabel contains unknown key "panel"`);
|
|
97
|
+
}
|
|
95
98
|
translations[canonical] = {
|
|
99
|
+
...(translation.panelLabel !== undefined ? { panelLabel: text(translation.panelLabel, `${canonical}.panelLabel`) } : {}),
|
|
96
100
|
...(translation.name !== undefined ? { name: text(translation.name, `${canonical}.name`) } : {}),
|
|
97
101
|
...(translation.description !== undefined
|
|
98
102
|
? { description: text(translation.description, `${canonical}.description`, MAX_DESCRIPTION) }
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Starts a native
|
|
4
|
+
* Starts a native worker (PDF pages, app icons, print images) in a process of its own. RLIMIT_DATA bounds the decoder heap
|
|
5
5
|
* and native writable mappings on Linux. Positional arguments keep paths out of shell code; no elevated privileges.
|
|
6
6
|
* The caller owns the deadline and kills the process on abort.
|
|
7
7
|
*/
|
|
8
|
-
export const spawnCanvasWorker = (worker: URL, name: string, input: string) =>
|
|
8
|
+
export const spawnCanvasWorker = (worker: URL, name: string, input: string | Blob) =>
|
|
9
9
|
Bun.spawn(["/bin/sh", "-c", 'ulimit -d 524288 || exit 70; exec "$@"', name, process.execPath, "--no-env-file", fileURLToPath(worker)], {
|
|
10
10
|
stdin: new Blob([input]),
|
|
11
11
|
stdout: "pipe",
|
|
12
12
|
stderr: "ignore",
|
|
13
13
|
// No application credentials or configuration reach the decoder, only fixed
|
|
14
|
-
//
|
|
14
|
+
// runtime settings: the Bun work pool, canvas tokio runtime, and JSC GC
|
|
15
15
|
// markers otherwise grow with the host's cores, and RLIMIT_DATA also counts
|
|
16
16
|
// each thread's stack and allocator memory.
|
|
17
|
+
// Bun's allocator otherwise commits whole arenas up front, which RLIMIT_DATA counts as used.
|
|
17
18
|
env: {
|
|
18
19
|
PATH: process.env.PATH,
|
|
19
20
|
LANG: "C.UTF-8",
|
|
20
21
|
UV_THREADPOOL_SIZE: "2",
|
|
21
22
|
TOKIO_WORKER_THREADS: "2",
|
|
22
23
|
BUN_JSC_numberOfGCMarkers: "2",
|
|
24
|
+
MIMALLOC_ARENA_EAGER_COMMIT: "0",
|
|
23
25
|
},
|
|
24
26
|
});
|
|
@@ -89,7 +89,7 @@ import { configurePostgresApplicationName } from "./postgres-application-name";
|
|
|
89
89
|
import { getProcessSync, startProcessSync } from "./process-sync";
|
|
90
90
|
import { APP_READINESS_PATH, appReadinessResponse } from "./readiness";
|
|
91
91
|
import { appRegistry, type CapabilityRegistryRecord, capabilityRegistry } from "./registry";
|
|
92
|
-
import { canonicalRoute,
|
|
92
|
+
import { canonicalRoute, isOwnAppPath } from "./registry-validation";
|
|
93
93
|
import { ensureRuntimeWatcher, getCurrentRuntime, stopRuntimeWatcher } from "./runtime-watcher";
|
|
94
94
|
import { servePublicAsset } from "./static-assets";
|
|
95
95
|
import { createStatusPreservingSsrHandler } from "./status-preserving-ssr";
|
|
@@ -144,6 +144,7 @@ export type AppOptions<S extends AppSettingsMap = {}, N extends NotificationDefi
|
|
|
144
144
|
label: string;
|
|
145
145
|
links: ReadonlyArray<{ label: string; href: string; icon: string }>;
|
|
146
146
|
}>;
|
|
147
|
+
panel?: AppMeta["panel"];
|
|
147
148
|
nav?: {
|
|
148
149
|
href: string;
|
|
149
150
|
match?: string;
|
|
@@ -371,11 +372,31 @@ const validateAppPwaPart = (opts: Pick<AppOptions, "id" | "routes" | "pwa">): Ap
|
|
|
371
372
|
/** The badge route is read by the browser through the gateway, so it must be one of this app's own paths. */
|
|
372
373
|
const validateAppNavBadge = (opts: Pick<AppOptions, "id" | "routes" | "nav">): void => {
|
|
373
374
|
const badge = opts.nav?.badge;
|
|
374
|
-
if (badge !== undefined && !
|
|
375
|
+
if (badge !== undefined && !isOwnAppPath(badge, opts.routes)) {
|
|
375
376
|
throw new Error(`App "${opts.id}" declares nav.badge "${badge}"; it must be a same-origin path below one of its routes`);
|
|
376
377
|
}
|
|
377
378
|
};
|
|
378
379
|
|
|
380
|
+
const validateAppPanel = (opts: Pick<AppOptions, "id" | "routes" | "panel">): AppMeta["panel"] => {
|
|
381
|
+
const panel = opts.panel;
|
|
382
|
+
if (panel === undefined) return undefined;
|
|
383
|
+
if (!panel || typeof panel !== "object" || Array.isArray(panel)) throw new Error(`App "${opts.id}" declares panel; it must be an object`);
|
|
384
|
+
if (typeof panel.href !== "string" || !isOwnAppPath(panel.href, opts.routes)) {
|
|
385
|
+
throw new Error(`App "${opts.id}" declares panel.href "${panel.href}"; it must be a same-origin path below one of its routes`);
|
|
386
|
+
}
|
|
387
|
+
for (const field of ["label", "icon"] as const) {
|
|
388
|
+
if (typeof panel[field] !== "string" || !panel[field].trim())
|
|
389
|
+
throw new Error(`App "${opts.id}" declares panel.${field}; it must be a non-empty string`);
|
|
390
|
+
}
|
|
391
|
+
if (
|
|
392
|
+
panel.requiresRoles !== undefined &&
|
|
393
|
+
(!Array.isArray(panel.requiresRoles) || !panel.requiresRoles.every((role) => typeof role === "string"))
|
|
394
|
+
) {
|
|
395
|
+
throw new Error(`App "${opts.id}" declares panel.requiresRoles; it must be an array of strings`);
|
|
396
|
+
}
|
|
397
|
+
return { ...panel, requiresRoles: panel.requiresRoles ? [...panel.requiresRoles] : undefined };
|
|
398
|
+
};
|
|
399
|
+
|
|
379
400
|
export const defineApp = <
|
|
380
401
|
const S extends AppSettingsMap = {},
|
|
381
402
|
const N extends NotificationDefinitionMap = {},
|
|
@@ -391,6 +412,7 @@ export const defineApp = <
|
|
|
391
412
|
const cliModules = validateAppCliModules(opts.id, opts.cli);
|
|
392
413
|
const pwaPart = validateAppPwaPart(opts);
|
|
393
414
|
validateAppNavBadge(opts);
|
|
415
|
+
const panel = validateAppPanel(opts);
|
|
394
416
|
|
|
395
417
|
// ── 0. Register declared settings into the runtime registry ──────────
|
|
396
418
|
// SETTINGS_MAP is the single source of truth for validation in store.ts
|
|
@@ -528,6 +550,7 @@ export const defineApp = <
|
|
|
528
550
|
legalLinks: opts.legalLinks ? [...opts.legalLinks] : undefined,
|
|
529
551
|
searchLinks: opts.searchLinks?.map((link) => ({ ...link, keywords: link.keywords ? [...link.keywords] : undefined })),
|
|
530
552
|
pwa: pwaPart,
|
|
553
|
+
panel,
|
|
531
554
|
widgets: compileWidgetDeclarations(opts.id, opts.widgets),
|
|
532
555
|
settingKeys: opts.settings ? Object.keys(opts.settings) : undefined,
|
|
533
556
|
openapi: opts.openapi,
|
|
@@ -627,6 +650,9 @@ export const defineApp = <
|
|
|
627
650
|
pwa: meta.pwa
|
|
628
651
|
? { href: meta.pwa.href, requiresRoles: meta.pwa.requiresRoles ? [...meta.pwa.requiresRoles] : undefined }
|
|
629
652
|
: undefined,
|
|
653
|
+
panel: meta.panel
|
|
654
|
+
? { ...meta.panel, requiresRoles: meta.panel.requiresRoles ? [...meta.panel.requiresRoles] : undefined }
|
|
655
|
+
: undefined,
|
|
630
656
|
widgets: meta.widgets?.map((widget) => ({
|
|
631
657
|
...widget,
|
|
632
658
|
...(widget.settings ? { settings: structuredClone(widget.settings) } : {}),
|
|
@@ -1,5 +1,124 @@
|
|
|
1
|
+
import { i18n } from "@k2b/stdlib";
|
|
2
|
+
import { formatShortcut } from "@k2b/ui";
|
|
3
|
+
import type { Token } from "marked";
|
|
1
4
|
import type { HelpArticle } from "../services/help/types";
|
|
2
|
-
import {
|
|
5
|
+
import { parseHelpReference } from "../shared/help";
|
|
6
|
+
import { lexHelpMarkdown, markdownToPlainText } from "../shared/markdown";
|
|
7
|
+
|
|
8
|
+
const helpAgentMessages = i18n.define({
|
|
9
|
+
baseLocale: "en",
|
|
10
|
+
messages: {
|
|
11
|
+
en: { apple: ({ shortcut }: { shortcut: string }) => `${shortcut} on Mac` },
|
|
12
|
+
de: { apple: ({ shortcut }) => `${shortcut} auf dem Mac` },
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const helpLinkLabel = (source: string, fallback: string): string => {
|
|
17
|
+
// Index backtick runs once so unmatched runs cannot repeatedly scan the remaining source.
|
|
18
|
+
const backticks = new Map<number, { positions: number[]; next: number }>();
|
|
19
|
+
for (const run of source.matchAll(/`+/g)) {
|
|
20
|
+
const length = run[0].length;
|
|
21
|
+
if (!backticks.has(length)) backticks.set(length, { positions: [], next: 0 });
|
|
22
|
+
backticks.get(length)!.positions.push(run.index);
|
|
23
|
+
}
|
|
24
|
+
let depth = 0;
|
|
25
|
+
for (let index = 1; index < source.length; index++) {
|
|
26
|
+
const char = source[index];
|
|
27
|
+
if (char === "\\") index++;
|
|
28
|
+
else if (char === "`") {
|
|
29
|
+
let end = index + 1;
|
|
30
|
+
while (source[end] === "`") end++;
|
|
31
|
+
const length = end - index;
|
|
32
|
+
const runs = backticks.get(length);
|
|
33
|
+
if (runs) {
|
|
34
|
+
while (runs.next < runs.positions.length && runs.positions[runs.next]! <= index) runs.next++;
|
|
35
|
+
}
|
|
36
|
+
const closing = runs?.positions[runs.next];
|
|
37
|
+
index = closing === undefined ? end - 1 : closing + length - 1;
|
|
38
|
+
} else if (char === "[") depth++;
|
|
39
|
+
else if (char === "]") {
|
|
40
|
+
if (depth === 0) return source.slice(1, index);
|
|
41
|
+
depth--;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return fallback;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Portable source for agents and Copy as Markdown; code is kept verbatim. */
|
|
48
|
+
export const helpAgentMarkdown = (
|
|
49
|
+
article: Pick<HelpArticle, "markdown" | "locale" | "appId" | "links">,
|
|
50
|
+
tokens: readonly Token[] = lexHelpMarkdown(article.markdown, article.locale),
|
|
51
|
+
): string => {
|
|
52
|
+
const { t } = helpAgentMessages.resolve([article.locale]);
|
|
53
|
+
const rewriteChildren = (raw: string, children: readonly Token[]): string => {
|
|
54
|
+
let offset = 0;
|
|
55
|
+
let result = "";
|
|
56
|
+
for (const child of children) {
|
|
57
|
+
let start = raw.indexOf(child.raw, offset);
|
|
58
|
+
let source = child.raw;
|
|
59
|
+
if (start < 0 && child.raw.includes("|")) {
|
|
60
|
+
source = child.raw.replaceAll("|", "\\|");
|
|
61
|
+
start = raw.indexOf(source, offset);
|
|
62
|
+
}
|
|
63
|
+
// Marked removes quote markers and list indentation from child block sources.
|
|
64
|
+
if (start < 0 && child.raw.includes("\n")) {
|
|
65
|
+
const pattern = child.raw
|
|
66
|
+
.split("\n")
|
|
67
|
+
.map((line, index) => (index > 0 ? line.replace(/^[ \t]+/, "") : line).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
68
|
+
.join("\\r?\\n[ \\t>]*");
|
|
69
|
+
const match = new RegExp(pattern).exec(raw.slice(offset));
|
|
70
|
+
if (match) {
|
|
71
|
+
start = offset + match.index;
|
|
72
|
+
source = match[0];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (start < 0) break;
|
|
76
|
+
result += raw.slice(offset, start) + rewrite(child, source);
|
|
77
|
+
offset = start + source.length;
|
|
78
|
+
}
|
|
79
|
+
return result + raw.slice(offset);
|
|
80
|
+
};
|
|
81
|
+
const rewrite = (token: Token, source: string): string => {
|
|
82
|
+
if (token.type === "code" || token.type === "codespan") return source;
|
|
83
|
+
if (token.type === "helpKbd") {
|
|
84
|
+
const keys = String(token.keys);
|
|
85
|
+
const other = formatShortcut(keys, { platform: "other", locale: article.locale });
|
|
86
|
+
const apple = formatShortcut(keys, { platform: "apple", locale: article.locale }).label;
|
|
87
|
+
// Key labels such as | or ` stay text in a table cell or a sentence.
|
|
88
|
+
const escape = (text: string) => text.replace(/[\\`*_[\]|<>~]/g, "\\$&");
|
|
89
|
+
// A key without a platform modifier reads by its name: "Enter", not "↵".
|
|
90
|
+
return other.label === apple ? escape(other.name) : `${escape(other.label)} (${t.apple({ shortcut: escape(apple) })})`;
|
|
91
|
+
}
|
|
92
|
+
if (token.type === "helpDetails") {
|
|
93
|
+
const newline = source.includes("\r\n") ? "\r\n" : "\n";
|
|
94
|
+
const bodyStart = source.indexOf("\n") + 1;
|
|
95
|
+
const closing = [...source.matchAll(/^[ \t>]*:::[ \t]*(?:\r?\n|$)/gm)].at(-1)!;
|
|
96
|
+
const body = source.slice(bodyStart, closing.index);
|
|
97
|
+
const prefix = body.match(/^[ \t>]+/)?.[0] ?? "";
|
|
98
|
+
const trailing = source.slice(closing.index + closing[0].length);
|
|
99
|
+
return `**${rewriteChildren(String(token.title), token.titleTokens ?? [])}**${newline}${prefix}${newline}${rewriteChildren(body, token.tokens ?? [])}${trailing}`;
|
|
100
|
+
}
|
|
101
|
+
if (token.type === "helpLabel") return rewriteChildren(source.replace(/\{icon="[a-z0-9-]+"\}$/, ""), token.tokens ?? []);
|
|
102
|
+
if (token.type === "link" && token.href.startsWith("help:")) {
|
|
103
|
+
const ref = parseHelpReference(token.href.slice(5));
|
|
104
|
+
const target = ref && article.links[`${ref.appId ?? article.appId}/${ref.documentId}`];
|
|
105
|
+
const label = rewriteChildren(helpLinkLabel(source, token.text), token.tokens ?? []);
|
|
106
|
+
return target ? `[${label}](${target.url}${ref.fragment})` : label;
|
|
107
|
+
}
|
|
108
|
+
const children =
|
|
109
|
+
token.type === "list"
|
|
110
|
+
? token.items
|
|
111
|
+
: token.type === "table"
|
|
112
|
+
? [...token.header, ...token.rows.flat()].flatMap((cell) => cell.tokens)
|
|
113
|
+
: "tokens" in token
|
|
114
|
+
? (token.tokens ?? [])
|
|
115
|
+
: [];
|
|
116
|
+
const raw = rewriteChildren(source, children);
|
|
117
|
+
return token.type === "heading" ? raw.replace(/[ \t]+\{icon="[a-z0-9-]+"\}(?=[ \t]*\r?\n?$)/i, "") : raw;
|
|
118
|
+
};
|
|
119
|
+
return rewriteChildren(article.markdown, tokens);
|
|
120
|
+
};
|
|
121
|
+
|
|
3
122
|
export const HELP_SEARCH_MAX_LIMIT = 25;
|
|
4
123
|
export const HELP_READ_MAX_CHARS = 7_000;
|
|
5
124
|
const normalizeSearchText = (value: string): string =>
|
|
@@ -33,7 +152,7 @@ const boundedExcerpt = (markdown: string, terms: readonly string[]): string => {
|
|
|
33
152
|
return `${start > 0 ? `${marker}\n\n` : ""}${body}${start + budget < markdown.length ? `\n\n${marker}` : ""}`;
|
|
34
153
|
};
|
|
35
154
|
|
|
36
|
-
export const selectHelpMarkdown = (markdown: string, query?: string): { markdown: string; truncated: boolean } => {
|
|
155
|
+
export const selectHelpMarkdown = (markdown: string, query?: string, locale?: string): { markdown: string; truncated: boolean } => {
|
|
37
156
|
// An exact heading identifies one complete reference section, even in short articles.
|
|
38
157
|
const headingQuery = query?.trim().toLocaleLowerCase();
|
|
39
158
|
const exact =
|
|
@@ -53,7 +172,7 @@ export const selectHelpMarkdown = (markdown: string, query?: string): { markdown
|
|
|
53
172
|
const phrase = normalizeSearchText(query ?? "");
|
|
54
173
|
const ranked = splitSections(markdown)
|
|
55
174
|
.map((section, index) => {
|
|
56
|
-
const text = normalizeSearchText(markdownToPlainText(section));
|
|
175
|
+
const text = normalizeSearchText(markdownToPlainText(section, locale));
|
|
57
176
|
const score = (phrase && text.includes(phrase) ? 10 : 0) + terms.filter((term) => text.includes(term)).length;
|
|
58
177
|
return { section, index, score };
|
|
59
178
|
})
|
|
@@ -79,7 +198,7 @@ export const readHelpArticle = (document: HelpArticle, query?: string) => ({
|
|
|
79
198
|
documentId: document.documentId,
|
|
80
199
|
title: document.title,
|
|
81
200
|
description: document.description,
|
|
82
|
-
...selectHelpMarkdown(document
|
|
201
|
+
...selectHelpMarkdown(helpAgentMarkdown(document), query, document.locale),
|
|
83
202
|
});
|
|
84
203
|
|
|
85
204
|
export const helpResourceUri = (appId: string, documentId: string): string =>
|
package/src/_internal/help.ts
CHANGED
|
@@ -2,28 +2,49 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import type { AppRegistryHelpSummary } from "../contracts/registry";
|
|
3
3
|
import type { HelpDefinition } from "../server/help";
|
|
4
4
|
import type { HelpCorpus, HelpDocument } from "../services/help/types";
|
|
5
|
+
import { parseHelpReference } from "../shared/help";
|
|
6
|
+
import { lexHelpMarkdown, walkHelpTokens } from "../shared/markdown";
|
|
5
7
|
|
|
6
8
|
export const HELP_DOCUMENT_MAX_BYTES = 128 * 1024;
|
|
9
|
+
// defineHelp documents are frozen; retain only their validated targets, not the token trees.
|
|
10
|
+
export const helpLinkTargets = new WeakMap<object, readonly string[]>();
|
|
7
11
|
export type CompiledHelp = { summary: AppRegistryHelpSummary; corpus: HelpCorpus };
|
|
8
12
|
|
|
9
13
|
export const compileHelp = (input: { appId: string; basePath?: string; definition: HelpDefinition }): CompiledHelp => {
|
|
10
|
-
const
|
|
11
|
-
|
|
14
|
+
const baseIds = new Set(input.definition.documents.map((document) => document.id));
|
|
15
|
+
const compileDocuments = (source: HelpDefinition["documents"], locale: string): HelpDocument[] =>
|
|
16
|
+
source.map((document) => {
|
|
17
|
+
const { id, title, icon, description, order, kind, needs, next, related, markdown, searchText } = document;
|
|
12
18
|
if (Buffer.byteLength(markdown) > HELP_DOCUMENT_MAX_BYTES) {
|
|
13
19
|
throw new Error(`Help document "${id}" exceeds the ${HELP_DOCUMENT_MAX_BYTES}-byte limit`);
|
|
14
20
|
}
|
|
15
|
-
|
|
21
|
+
const validate = (reference: string) => {
|
|
22
|
+
const ref = parseHelpReference(reference);
|
|
23
|
+
if (ref?.appId === input.appId && !baseIds.has(ref.documentId))
|
|
24
|
+
throw new Error(`Help locale "${locale}" document "${id}" has unknown reference "${reference}"`);
|
|
25
|
+
};
|
|
26
|
+
for (const reference of related) validate(reference);
|
|
27
|
+
const targets = helpLinkTargets.get(document);
|
|
28
|
+
if (targets) {
|
|
29
|
+
for (const target of targets) validate(target);
|
|
30
|
+
} else {
|
|
31
|
+
walkHelpTokens(lexHelpMarkdown(markdown, locale), (token) => {
|
|
32
|
+
if (token.type === "link" && token.href.startsWith("help:")) validate(token.href.slice(5));
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
return { id, title, icon, description, order, kind, needs, next, related, markdown, searchText };
|
|
16
36
|
});
|
|
17
37
|
const baseLocale = input.definition.baseLocale ?? "en";
|
|
18
|
-
const documents = compileDocuments(input.definition.documents);
|
|
38
|
+
const documents = compileDocuments(input.definition.documents, baseLocale);
|
|
19
39
|
const documentsByLocale = Object.fromEntries(
|
|
20
40
|
Object.entries(input.definition.documentsByLocale ?? {})
|
|
21
41
|
.filter(([locale]) => locale !== baseLocale)
|
|
22
42
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
23
|
-
.map(([locale, docs]) => [locale, compileDocuments(docs)]),
|
|
43
|
+
.map(([locale, docs]) => [locale, compileDocuments(docs, locale)]),
|
|
24
44
|
);
|
|
45
|
+
// A revision bump republishes every corpus, here to fill the new metadata columns.
|
|
25
46
|
const manifestHash = createHash("sha256")
|
|
26
|
-
.update(JSON.stringify({ revision:
|
|
47
|
+
.update(JSON.stringify({ revision: 2, appId: input.appId, baseLocale, documents, documentsByLocale }))
|
|
27
48
|
.digest("hex");
|
|
28
49
|
return {
|
|
29
50
|
summary: { manifestHash, pageBase: `${input.basePath?.replace(/\/$/, "") ?? ""}/help`, baseLocale },
|