@frockbot/applet-sdk 0.7.150 → 0.7.151

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 CHANGED
@@ -1,17 +1,12 @@
1
1
  {
2
2
  "name": "@frockbot/applet-sdk",
3
- "version": "0.7.150",
3
+ "version": "0.7.151",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Authoring SDK for FrockBot Applets: schema-first Durable Object server, TanStack DB client, component kit, linter, and the build pipeline.",
7
7
  "license": "UNLICENSED",
8
8
  "exports": {
9
- "./server": "./src/server/index.ts",
10
- "./client": "./src/client/index.ts",
11
- "./kit": "./src/kit/index.tsx",
12
9
  "./lint": "./src/lint/index.ts",
13
- "./protocol": "./src/protocol/index.ts",
14
- "./build": "./src/build/pipeline.ts",
15
10
  "./build/plugin": "./src/build/plugin.ts",
16
11
  "./plugin": "./plugin/index.d.ts",
17
12
  "./package.json": "./package.json"
@@ -12,7 +12,7 @@ import { convertV4MiniflareOptions, Miniflare } from "miniflare";
12
12
 
13
13
  import { bootedWithin } from "./boot.js";
14
14
 
15
- /** Pinned with the SDK: the runtime an Applet is checked against. */
15
+ /** Pinned with the SDK: the runtime a Plugin build is checked against. */
16
16
  export const APPLET_COMPATIBILITY_DATE = "2026-08-27";
17
17
 
18
18
  /**
@@ -1,200 +0,0 @@
1
- /**
2
- * Three immutable artifacts from two source files.
3
- *
4
- * `server.js` one ESM file whose only import is `cloudflare:workers`,
5
- * exporting `Applet`, which is the name the kernel mounts.
6
- * `ui.html` one self-contained page: React, TanStack DB, the kit, and
7
- * the app inlined. No external URL, because the artifact
8
- * origin serves it into a sandbox with no network of its own.
9
- * `manifest.json` `{ contract, tools, hashes }`.
10
- *
11
- * The tool declarations come from mounting the built server in Miniflare and
12
- * calling `health()`, not from reading the source. Static analysis would be a
13
- * second implementation of `this.tool(...)` that could disagree with the one
14
- * the kernel actually asks — and the kernel admits a generation by comparing
15
- * the manifest to the facet's own `health()`, so any disagreement is a failed
16
- * publish. Running the code is the only derivation that cannot drift.
17
- *
18
- * Nothing here touches a filesystem beyond the source it is given: the build
19
- * service returns these three strings over its contract, and the app stores
20
- * them under their content hashes.
21
- */
22
-
23
- import { createHash, randomUUID } from "node:crypto";
24
-
25
- import { build as esbuild } from "esbuild";
26
-
27
- import type { AppletDescriptionV1 } from "../server/applet.js";
28
- import { readDescriptor, type AppletBuildManifestV1 } from "./manifest.js";
29
- import { stableModulePaths } from "./module-paths.js";
30
- import { bundlerNodePaths, SDK_ENTRIES, SDK_ROOT } from "./paths.js";
31
- import { withOneMoreBoot } from "./boot.js";
32
- import { startAppletRuntime } from "./runtime.js";
33
-
34
- export interface AppletArtifactsV1 {
35
- manifest: AppletBuildManifestV1;
36
- server: string;
37
- ui: string;
38
- }
39
-
40
- function sha256(text: string): string {
41
- return createHash("sha256").update(text, "utf8").digest("hex");
42
- }
43
-
44
- async function bundle(options: {
45
- stdin: string;
46
- resolveDir: string;
47
- platform: "neutral" | "browser";
48
- format: "esm" | "iife";
49
- external: string[];
50
- minify: boolean;
51
- loaderName: string;
52
- }): Promise<string> {
53
- const result = await esbuild({
54
- stdin: {
55
- contents: options.stdin,
56
- resolveDir: options.resolveDir,
57
- sourcefile: options.loaderName,
58
- loader: "tsx",
59
- },
60
- bundle: true,
61
- write: false,
62
- format: options.format,
63
- platform: options.platform,
64
- target: "es2022",
65
- jsx: "automatic",
66
- minify: options.minify,
67
- legalComments: "none",
68
- external: options.external,
69
- alias: { ...SDK_ENTRIES },
70
- nodePaths: bundlerNodePaths(),
71
- conditions: ["import", "module", "browser", "default"],
72
- define: { "process.env.NODE_ENV": '"production"' },
73
- metafile: true,
74
- logLevel: "silent",
75
- });
76
- const file = result.outputFiles?.[0];
77
- if (!file) throw new Error("The bundler produced no output");
78
- return stableModulePaths(file.text, result.metafile, options.resolveDir, [
79
- { directory: SDK_ROOT, prefix: "applet-sdk/" },
80
- ]);
81
- }
82
-
83
- function page(title: string, script: string): string {
84
- // Nothing is fetched: the CSP on the artifact origin blocks every external
85
- // request, so React, the kit, and the app are all in this one <script>.
86
- return [
87
- "<!doctype html>",
88
- '<html lang="en">',
89
- "<head>",
90
- '<meta charset="utf-8">',
91
- '<meta name="viewport" content="width=device-width, initial-scale=1">',
92
- `<title>${title.replaceAll("<", "&lt;")}</title>`,
93
- // The root gets a definite height so the kit's own root (min-height: 100%)
94
- // fills the page; without it an Applet ends where its content ends.
95
- "<style>html,body,#applet-root{margin:0;height:100%;background:var(--frockbot-surface,#ffffff)}</style>",
96
- "</head>",
97
- "<body>",
98
- '<div id="applet-root"></div>',
99
- `<script>${script}</script>`,
100
- "</body>",
101
- "</html>",
102
- ].join("\n");
103
- }
104
-
105
- /**
106
- * Ask the built module what it declares, by running it. A boot that never
107
- * reports ready is tried once more (`boot.ts`) before the build gives up.
108
- */
109
- export function readDescription(
110
- serverCode: string,
111
- appletId: string,
112
- ): Promise<AppletDescriptionV1> {
113
- return withOneMoreBoot(() => describeInRuntime(serverCode, appletId));
114
- }
115
-
116
- async function describeInRuntime(
117
- serverCode: string,
118
- appletId: string,
119
- ): Promise<AppletDescriptionV1> {
120
- const runtime = await startAppletRuntime({
121
- serverCode,
122
- appletId,
123
- token: randomUUID(),
124
- });
125
- try {
126
- // `health()` first: it is what the kernel calls, so a server that cannot
127
- // mount fails here the way it would fail a publish.
128
- const health = await runtime.fetch("/health");
129
- if (!health.ok) {
130
- throw new Error(`The Applet failed to mount: ${await health.text()}`);
131
- }
132
- const response = await runtime.fetch("/describe");
133
- if (!response.ok) {
134
- throw new Error(
135
- `The Applet could not describe its tools: ${await response.text()}`,
136
- );
137
- }
138
- return (await response.json()) as AppletDescriptionV1;
139
- } finally {
140
- await runtime.dispose();
141
- }
142
- }
143
-
144
- /** The two bundles, in the order that lets a bundle failure precede a boot. */
145
- export async function bundleAppletArtifacts(
146
- directory: string,
147
- ): Promise<{ descriptorId: string; server: string; ui: string }> {
148
- const descriptor = await readDescriptor(directory);
149
- const server = await bundle({
150
- // `Applet` is the export name the kernel's facet mount looks up; the author
151
- // writes an ordinary default export and never learns that name.
152
- stdin:
153
- 'import AppletClass from "./server";\nexport { AppletClass as Applet };\n',
154
- resolveDir: directory,
155
- platform: "neutral",
156
- format: "esm",
157
- external: ["cloudflare:workers"],
158
- minify: false,
159
- loaderName: "applet-server-entry.ts",
160
- });
161
- const uiScript = await bundle({
162
- stdin: 'import "./ui";\n',
163
- resolveDir: directory,
164
- platform: "browser",
165
- format: "iife",
166
- external: [],
167
- minify: true,
168
- loaderName: "applet-ui-entry.tsx",
169
- });
170
- return {
171
- descriptorId: descriptor.id,
172
- server,
173
- ui: page(descriptor.displayName, uiScript),
174
- };
175
- }
176
-
177
- /** The manifest for artifacts whose declarations have been read. */
178
- export function appletManifest(
179
- tools: AppletDescriptionV1["tools"],
180
- artifacts: { server: string; ui: string },
181
- ): AppletBuildManifestV1 {
182
- return {
183
- contract: 1,
184
- tools,
185
- hashes: { server: sha256(artifacts.server), ui: sha256(artifacts.ui) },
186
- };
187
- }
188
-
189
- /** Bundle, boot, describe. The whole artifact derivation, with no output. */
190
- export async function buildAppletArtifacts(
191
- directory: string,
192
- ): Promise<AppletArtifactsV1> {
193
- const { descriptorId, server, ui } = await bundleAppletArtifacts(directory);
194
- const description = await readDescription(server, descriptorId);
195
- return {
196
- manifest: appletManifest(description.tools, { server, ui }),
197
- server,
198
- ui,
199
- };
200
- }
@@ -1,135 +0,0 @@
1
- /**
2
- * The check stage — the type checker and the linter, one diagnostic list.
3
- *
4
- * The Applet has no `node_modules`: it is source at a durable root. So the
5
- * compiler options are built here from `paths.ts` rather than from a tsconfig
6
- * the Bot would have to maintain, and every diagnostic comes back in the one
7
- * shape the CLI prints.
8
- */
9
-
10
- import { readdir } from "node:fs/promises";
11
- import { join, relative, resolve } from "node:path";
12
-
13
- import ts from "typescript";
14
-
15
- import { lintApplet, type AppletDiagnostic } from "../lint/index.js";
16
- import { readDescriptor } from "./manifest.js";
17
- import { SDK_WORKERS_TYPES, typeCheckerPaths } from "./paths.js";
18
-
19
- const COMPILER_OPTIONS: ts.CompilerOptions = {
20
- target: ts.ScriptTarget.ES2022,
21
- module: ts.ModuleKind.ESNext,
22
- moduleResolution: ts.ModuleResolutionKind.Bundler,
23
- jsx: ts.JsxEmit.ReactJSX,
24
- strict: true,
25
- noEmit: true,
26
- skipLibCheck: true,
27
- esModuleInterop: true,
28
- allowSyntheticDefaultImports: true,
29
- forceConsistentCasingInFileNames: true,
30
- lib: ["lib.es2022.d.ts", "lib.dom.d.ts"],
31
- types: [],
32
- };
33
-
34
- async function appletSources(directory: string): Promise<string[]> {
35
- const found: string[] = [];
36
- const walk = async (current: string): Promise<void> => {
37
- for (const entry of await readdir(current, { withFileTypes: true })) {
38
- if (entry.name === "node_modules" || entry.name === "dist") continue;
39
- if (entry.name.startsWith(".")) continue;
40
- const path = join(current, entry.name);
41
- if (entry.isDirectory()) await walk(path);
42
- else if (/\.(ts|tsx)$/.test(entry.name)) found.push(path);
43
- }
44
- };
45
- await walk(directory);
46
- return found;
47
- }
48
-
49
- /** Type-check the Applet against the SDK's declarations. */
50
- export async function typeCheckApplet(
51
- directory: string,
52
- ): Promise<AppletDiagnostic[]> {
53
- const root = resolve(directory);
54
- const files = await appletSources(root);
55
- if (files.length === 0) {
56
- return [
57
- {
58
- file: "applet.json",
59
- line: 1,
60
- column: 1,
61
- message:
62
- "No TypeScript sources found; an Applet needs server.ts and ui.tsx.",
63
- severity: "error",
64
- },
65
- ];
66
- }
67
- // No `baseUrl`: TypeScript 7 rejects it, and it was never needed — every
68
- // mapping `typeCheckerPaths()` returns is already an absolute path.
69
- const program = ts.createProgram([...files, SDK_WORKERS_TYPES], {
70
- ...COMPILER_OPTIONS,
71
- paths: typeCheckerPaths(),
72
- });
73
- return ts
74
- .getPreEmitDiagnostics(program)
75
- .filter(
76
- (diagnostic) =>
77
- !diagnostic.file || diagnostic.file.fileName.startsWith(root),
78
- )
79
- .map((diagnostic) => {
80
- const message = ts.flattenDiagnosticMessageText(
81
- diagnostic.messageText,
82
- " ",
83
- );
84
- if (!diagnostic.file || diagnostic.start === undefined) {
85
- return {
86
- file: "applet.json",
87
- line: 1,
88
- column: 1,
89
- message,
90
- severity: "error" as const,
91
- };
92
- }
93
- const position = diagnostic.file.getLineAndCharacterOfPosition(
94
- diagnostic.start,
95
- );
96
- return {
97
- file: relative(root, diagnostic.file.fileName),
98
- line: position.line + 1,
99
- column: position.character + 1,
100
- message: `${message} (TS${diagnostic.code})`,
101
- severity:
102
- diagnostic.category === ts.DiagnosticCategory.Error
103
- ? ("error" as const)
104
- : ("warning" as const),
105
- };
106
- });
107
- }
108
-
109
- /** Everything the check stage reports, in source order. */
110
- export async function checkApplet(
111
- directory: string,
112
- ): Promise<AppletDiagnostic[]> {
113
- const root = resolve(directory);
114
- const diagnostics: AppletDiagnostic[] = [];
115
- try {
116
- await readDescriptor(root);
117
- } catch (error) {
118
- diagnostics.push({
119
- file: "applet.json",
120
- line: 1,
121
- column: 1,
122
- message: error instanceof Error ? error.message : String(error),
123
- severity: "error",
124
- });
125
- return diagnostics;
126
- }
127
- diagnostics.push(...(await typeCheckApplet(root)));
128
- diagnostics.push(...(await lintApplet(root)));
129
- return diagnostics.sort(
130
- (left, right) =>
131
- left.file.localeCompare(right.file) ||
132
- left.line - right.line ||
133
- left.column - right.column,
134
- );
135
- }
@@ -1,56 +0,0 @@
1
- /** `applet.json`: the three facts the SDK needs before it reads any code. */
2
-
3
- import { readFile } from "node:fs/promises";
4
- import { join } from "node:path";
5
-
6
- import { APPLET_CONTRACT_VERSION } from "../protocol/index.js";
7
- import type { AppletToolDeclarationV1 } from "../server/applet.js";
8
-
9
- export interface AppletDescriptorV1 {
10
- /** `/^[a-z][a-z0-9-]{0,31}$/`; the scaffold's directory name. */
11
- id: string;
12
- displayName: string;
13
- contract: 1;
14
- }
15
-
16
- export interface AppletBuildManifestV1 {
17
- contract: 1;
18
- tools: AppletToolDeclarationV1[];
19
- hashes: { server: string; ui: string };
20
- }
21
-
22
- export function decodeDescriptor(input: unknown): AppletDescriptorV1 {
23
- if (!input || typeof input !== "object")
24
- throw new Error("applet.json must be an object");
25
- const value = input as Record<string, unknown>;
26
- const id = value.id;
27
- if (typeof id !== "string" || !/^[a-z][a-z0-9-]{0,31}$/.test(id)) {
28
- throw new Error('applet.json "id" must match /^[a-z][a-z0-9-]{0,31}$/');
29
- }
30
- if (
31
- typeof value.displayName !== "string" ||
32
- value.displayName.length === 0 ||
33
- value.displayName.length > 64
34
- ) {
35
- throw new Error('applet.json "displayName" must be 1-64 characters');
36
- }
37
- if (value.contract !== APPLET_CONTRACT_VERSION) {
38
- throw new Error(
39
- `applet.json "contract" must be ${APPLET_CONTRACT_VERSION}`,
40
- );
41
- }
42
- return { id, displayName: value.displayName, contract: 1 };
43
- }
44
-
45
- export async function readDescriptor(
46
- directory: string,
47
- ): Promise<AppletDescriptorV1> {
48
- const path = join(directory, "applet.json");
49
- let text: string;
50
- try {
51
- text = await readFile(path, "utf8");
52
- } catch {
53
- throw new Error(`No applet.json in ${directory}`);
54
- }
55
- return decodeDescriptor(JSON.parse(text));
56
- }
@@ -1,96 +0,0 @@
1
- /**
2
- * The whole Applet build, as five named stages over one directory.
3
- *
4
- * `applet_check` and `applet_publish` are the two ways in, and both reach
5
- * these stages through the same build service. There is one implementation of
6
- * each stage, so a check and the publish that follows it cannot disagree about
7
- * whether the code is admissible or about what it hashes to.
8
- *
9
- * A stage that fails stops the run and names itself, which is what turns a
10
- * wall of diagnostics into "your `server.ts` does not type-check". The two
11
- * stages that produce no diagnostic list of their own — `bundle` and
12
- * `describe` — report the thrown message as one diagnostic against
13
- * `applet.json`, so a caller has one shape to render.
14
- */
15
-
16
- import { lintApplet, type AppletDiagnostic } from "../lint/index.js";
17
- import { buildAppletArtifacts, type AppletArtifactsV1 } from "./artifacts.js";
18
- import { typeCheckApplet } from "./check.js";
19
- import { readDescriptor } from "./manifest.js";
20
-
21
- export type AppletBuildStage =
22
- "descriptor" | "typecheck" | "lint" | "bundle" | "describe";
23
-
24
- export type AppletBuildOutcome =
25
- | { status: "checked" }
26
- | ({ status: "built" } & AppletArtifactsV1)
27
- | {
28
- status: "failed";
29
- stage: AppletBuildStage;
30
- diagnostics: AppletDiagnostic[];
31
- };
32
-
33
- /** A thrown stage failure, reported at `applet.json` for want of a position. */
34
- function thrown(error: unknown): AppletDiagnostic[] {
35
- return [
36
- {
37
- file: "applet.json",
38
- line: 1,
39
- column: 1,
40
- message: error instanceof Error ? error.message : String(error),
41
- severity: "error",
42
- },
43
- ];
44
- }
45
-
46
- function hasError(diagnostics: AppletDiagnostic[]): boolean {
47
- return diagnostics.some((diagnostic) => diagnostic.severity === "error");
48
- }
49
-
50
- export interface AppletBuildPipelineOptions {
51
- /** `check` stops after the linter; `build` goes on to the artifacts. */
52
- mode: "check" | "build";
53
- }
54
-
55
- export async function runAppletBuildV1(
56
- directory: string,
57
- options: AppletBuildPipelineOptions,
58
- ): Promise<AppletBuildOutcome> {
59
- try {
60
- await readDescriptor(directory);
61
- } catch (error) {
62
- return {
63
- status: "failed",
64
- stage: "descriptor",
65
- diagnostics: thrown(error),
66
- };
67
- }
68
-
69
- const types = await typeCheckApplet(directory);
70
- if (hasError(types)) {
71
- return { status: "failed", stage: "typecheck", diagnostics: types };
72
- }
73
- const lint = await lintApplet(directory);
74
- if (hasError(lint)) {
75
- return { status: "failed", stage: "lint", diagnostics: lint };
76
- }
77
- if (options.mode === "check") return { status: "checked" };
78
-
79
- let artifacts: AppletArtifactsV1;
80
- try {
81
- artifacts = await buildAppletArtifacts(directory);
82
- } catch (error) {
83
- // `describe` is the only stage that boots the built module, and both of
84
- // its failures say so in their own words; anything else thrown here came
85
- // out of the bundler.
86
- const message = error instanceof Error ? error.message : String(error);
87
- return {
88
- status: "failed",
89
- stage: /^The Applet (failed to mount|could not describe)/.test(message)
90
- ? "describe"
91
- : "bundle",
92
- diagnostics: thrown(error),
93
- };
94
- }
95
- return { status: "built", ...artifacts };
96
- }
@@ -1,72 +0,0 @@
1
- /**
2
- * The TanStack DB adapter: one collection per declared table, synced from the
3
- * Applet socket and mutated back over it.
4
- *
5
- * `sync` applies `snapshot` and `changes`; `onInsert`/`onUpdate`/`onDelete`
6
- * send one `mutate` frame and return its promise, so an `ack` confirms the
7
- * optimistic write and a `reject` rolls it back.
8
- */
9
-
10
- import { createCollection, type Collection } from "@tanstack/db";
11
-
12
- import type { AppletMutationV1 } from "../protocol/index.js";
13
- import type { AppletTransport } from "./transport.js";
14
-
15
- export type AppletRow = Record<string, unknown> & { id: string };
16
-
17
- export function createAppletCollection(
18
- name: string,
19
- transport: AppletTransport,
20
- ): Collection<AppletRow, string> {
21
- return createCollection<AppletRow, string>({
22
- id: `applet:${name}`,
23
- getKey: (row) => row.id,
24
- startSync: true,
25
- sync: {
26
- // The server always sends the whole row on update.
27
- rowUpdateMode: "full",
28
- sync: ({ begin, write, commit, markReady, truncate }) =>
29
- transport.registerTable(name, {
30
- begin: () => begin(),
31
- write: (message) => {
32
- if (message.type === "delete") {
33
- write({ type: "delete", key: message.key! });
34
- return;
35
- }
36
- write({ type: message.type, value: message.value as AppletRow });
37
- },
38
- commit: () => {
39
- commit();
40
- },
41
- markReady,
42
- truncate,
43
- }),
44
- },
45
- onInsert: ({ transaction }) =>
46
- transport.mutate(
47
- transaction.mutations.map((mutation): AppletMutationV1 => ({
48
- table: name,
49
- op: "insert",
50
- key: String(mutation.key),
51
- value: mutation.modified as Record<string, unknown>,
52
- })),
53
- ),
54
- onUpdate: ({ transaction }) =>
55
- transport.mutate(
56
- transaction.mutations.map((mutation): AppletMutationV1 => ({
57
- table: name,
58
- op: "update",
59
- key: String(mutation.key),
60
- value: mutation.changes as Record<string, unknown>,
61
- })),
62
- ),
63
- onDelete: ({ transaction }) =>
64
- transport.mutate(
65
- transaction.mutations.map((mutation): AppletMutationV1 => ({
66
- table: name,
67
- op: "delete",
68
- key: String(mutation.key),
69
- })),
70
- ),
71
- });
72
- }