@frockbot/applet-sdk 0.5.1 → 0.6.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/README.md +25 -28
- package/package.json +4 -9
- package/src/{cli/build.ts → build/artifacts.ts} +109 -44
- package/src/{cli → build}/check.ts +2 -2
- package/src/{cli → build}/manifest.ts +2 -4
- package/src/{cli → build}/paths.ts +7 -6
- package/src/build/pipeline.ts +96 -0
- package/src/{cli → build}/runtime.ts +2 -2
- package/src/client/index.ts +1 -1
- package/src/lint/index.ts +1 -1
- package/src/server/applet.ts +2 -2
- package/template/README.md +5 -8
- package/dist/cli.mjs +0 -1008
- package/src/cli/bin.ts +0 -128
- package/src/cli/dev.ts +0 -84
- package/src/cli/main.ts +0 -17
- package/src/cli/new.ts +0 -74
package/README.md
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
The SDK a FrockBot Applet is written against: a schema-first Durable Object
|
|
4
4
|
server, a TanStack DB client over one real-time socket, a precompiled component
|
|
5
|
-
kit on the theme tokens, a linter, and the
|
|
5
|
+
kit on the theme tokens, a linter, and the build pipeline the cloud build
|
|
6
|
+
service runs.
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
An Applet is authored with the `applet_*` tools, built by `apps/applet-build`,
|
|
9
|
+
and mounted as a Durable Object facet from an immutable artifact. There is no
|
|
10
|
+
CLI: nothing outside the service builds an Applet, and no Computer is involved
|
|
11
|
+
at any point.
|
|
10
12
|
|
|
11
13
|
## Entry points
|
|
12
14
|
|
|
@@ -17,29 +19,24 @@ Applet's state is a Durable Object facet the kernel owns the lifecycle of, and
|
|
|
17
19
|
| `@frockbot/applet-sdk/kit` | the fourteen components (`src/kit/README.md`) |
|
|
18
20
|
| `@frockbot/applet-sdk/lint` | the flat ESLint config and the five custom rules |
|
|
19
21
|
| `@frockbot/applet-sdk/protocol` | wire protocol v1, for the kernel and for tests |
|
|
22
|
+
| `@frockbot/applet-sdk/build` | `runAppletBuildV1` — the five stages, for the service |
|
|
20
23
|
|
|
21
|
-
## The
|
|
24
|
+
## The build
|
|
22
25
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
```
|
|
26
|
+
`runAppletBuildV1(directory, { mode })` is five named stages over one
|
|
27
|
+
directory: `descriptor`, `typecheck`, `lint`, `bundle`, `describe`. `check`
|
|
28
|
+
stops after the linter; `build` goes on to the artifacts. A stage that fails
|
|
29
|
+
stops the run and names itself, and every failure is a list of
|
|
30
|
+
`{file, line, column, message, severity}`.
|
|
29
31
|
|
|
30
|
-
`
|
|
31
|
-
`
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
`manifest.json`'s tool declarations are derived by mounting the built
|
|
33
|
+
`server.js` in Miniflare and calling `health()` — the same question the kernel
|
|
34
|
+
asks the facet before it admits a generation, so the manifest cannot disagree
|
|
35
|
+
with the code.
|
|
34
36
|
|
|
35
|
-
|
|
36
|
-
`
|
|
37
|
-
|
|
38
|
-
once under the shared Computer runtime; an in-place runtime update repairs that
|
|
39
|
-
installation when it is missing. An Applet project deliberately has no
|
|
40
|
-
`node_modules` of its own. The checker and bundler resolve SDK, React, and
|
|
41
|
-
TanStack imports from the shared installation, while project dependency trees
|
|
42
|
-
remain reproducible scratch and never enter the durable-root sync.
|
|
37
|
+
`template/` is the scaffold a new Applet starts as.
|
|
38
|
+
`scripts/build-applets-assets.ts` turns it into `applets/template.generated.ts`,
|
|
39
|
+
which `applet_create` writes through the Workspace.
|
|
43
40
|
|
|
44
41
|
## What runs where
|
|
45
42
|
|
|
@@ -50,9 +47,9 @@ page served from the anonymous artifact origin into a sandboxed iframe, which
|
|
|
50
47
|
receives its theme tokens and a short-lived viewer token through the host's
|
|
51
48
|
`init` message and opens exactly one WebSocket back to the facet.
|
|
52
49
|
|
|
53
|
-
The Cloudflare programming model is not hidden
|
|
54
|
-
|
|
55
|
-
|
|
50
|
+
The Cloudflare programming model is not hidden: an Applet is a Durable Object
|
|
51
|
+
with SQLite and hibernating sockets. What the SDK does hide is every binding
|
|
52
|
+
name — an author sees `tables`, `tools`, and `this.db`.
|
|
56
53
|
|
|
57
54
|
## Wire protocol v1
|
|
58
55
|
|
|
@@ -77,5 +74,5 @@ bun test test spike
|
|
|
77
74
|
|
|
78
75
|
Pure modules and the client are tested in `bun test`: the store runs against
|
|
79
76
|
`bun:sqlite`, and `test/loopback.ts` joins the real protocol server to the real
|
|
80
|
-
client transport through a pair of fake sockets. `test/
|
|
81
|
-
`spike/` run the built Applet in Miniflare
|
|
77
|
+
client transport through a pair of fake sockets. `test/build.test.ts` and
|
|
78
|
+
`spike/` run the real pipeline and the built Applet in Miniflare.
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/applet-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
|
-
"description": "Authoring SDK for FrockBot Applets: schema-first Durable Object server, TanStack DB client, component kit, linter, and the
|
|
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
9
|
"./server": "./src/server/index.ts",
|
|
@@ -11,21 +11,16 @@
|
|
|
11
11
|
"./kit": "./src/kit/index.tsx",
|
|
12
12
|
"./lint": "./src/lint/index.ts",
|
|
13
13
|
"./protocol": "./src/protocol/index.ts",
|
|
14
|
+
"./build": "./src/build/pipeline.ts",
|
|
14
15
|
"./package.json": "./package.json"
|
|
15
16
|
},
|
|
16
|
-
"bin": {
|
|
17
|
-
"applet": "./dist/cli.mjs"
|
|
18
|
-
},
|
|
19
17
|
"files": [
|
|
20
|
-
"dist",
|
|
21
18
|
"src",
|
|
22
19
|
"types",
|
|
23
20
|
"template",
|
|
24
21
|
"README.md"
|
|
25
22
|
],
|
|
26
23
|
"scripts": {
|
|
27
|
-
"build": "bun scripts/build-cli.ts",
|
|
28
|
-
"prepublishOnly": "bun scripts/build-cli.ts",
|
|
29
24
|
"test": "bun test test spike",
|
|
30
25
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
31
26
|
},
|
|
@@ -52,7 +47,7 @@
|
|
|
52
47
|
"repository": {
|
|
53
48
|
"type": "git",
|
|
54
49
|
"url": "git+https://github.com/timoconnellaus/frockbot.git",
|
|
55
|
-
"directory": "
|
|
50
|
+
"directory": "applets/sdk"
|
|
56
51
|
},
|
|
57
52
|
"frockbot": {
|
|
58
53
|
"npm": true
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Three immutable artifacts from two source files.
|
|
3
3
|
*
|
|
4
|
-
* `
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* `
|
|
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
10
|
*
|
|
11
11
|
* The tool declarations come from mounting the built server in Miniflare and
|
|
12
12
|
* calling `health()`, not from reading the source. Static analysis would be a
|
|
@@ -14,33 +14,91 @@
|
|
|
14
14
|
* the kernel actually asks — and the kernel admits a generation by comparing
|
|
15
15
|
* the manifest to the facet's own `health()`, so any disagreement is a failed
|
|
16
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.
|
|
17
21
|
*/
|
|
18
22
|
|
|
19
23
|
import { createHash, randomUUID } from "node:crypto";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
24
|
+
import { realpathSync } from "node:fs";
|
|
25
|
+
import { relative, resolve } from "node:path";
|
|
22
26
|
|
|
23
|
-
import { build as esbuild } from "esbuild";
|
|
27
|
+
import { build as esbuild, type Metafile } from "esbuild";
|
|
24
28
|
|
|
25
29
|
import type { AppletDescriptionV1 } from "../server/applet.js";
|
|
26
30
|
import { readDescriptor, type AppletBuildManifestV1 } from "./manifest.js";
|
|
27
|
-
import { bundlerNodePaths, SDK_ENTRIES } from "./paths.js";
|
|
31
|
+
import { bundlerNodePaths, SDK_ENTRIES, SDK_ROOT } from "./paths.js";
|
|
28
32
|
import { startAppletRuntime } from "./runtime.js";
|
|
29
33
|
|
|
30
|
-
export interface
|
|
31
|
-
directory: string;
|
|
32
|
-
serverPath: string;
|
|
33
|
-
uiPath: string;
|
|
34
|
-
manifestPath: string;
|
|
34
|
+
export interface AppletArtifactsV1 {
|
|
35
35
|
manifest: AppletBuildManifestV1;
|
|
36
|
+
server: string;
|
|
37
|
+
ui: string;
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
function sha256(text: string): string {
|
|
39
41
|
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
40
42
|
}
|
|
41
43
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
/** esbuild reports real paths; a temp directory is often a symlink to one. */
|
|
45
|
+
function real(path: string): string {
|
|
46
|
+
try {
|
|
47
|
+
return realpathSync(resolve(path));
|
|
48
|
+
} catch {
|
|
49
|
+
return resolve(path);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Where a bundled module came from, said the same way everywhere.
|
|
55
|
+
*
|
|
56
|
+
* An Applet's directory, the SDK's directory and the working directory are all
|
|
57
|
+
* different on an author's machine and in the build container — and esbuild
|
|
58
|
+
* writes each module's path into the unminified output as a comment. Left
|
|
59
|
+
* alone, identical source bundled in two places produces two different files
|
|
60
|
+
* and therefore two different content hashes, which would give R2 a new object
|
|
61
|
+
* on every publish of unchanged code.
|
|
62
|
+
*/
|
|
63
|
+
function moduleLabel(absolute: string, appletRoot: string): string {
|
|
64
|
+
for (const [root, prefix] of [
|
|
65
|
+
[appletRoot, ""],
|
|
66
|
+
[real(SDK_ROOT), "applet-sdk/"],
|
|
67
|
+
] as const) {
|
|
68
|
+
const inside = relative(root, absolute);
|
|
69
|
+
if (!inside.startsWith("..") && inside !== "") return prefix + inside;
|
|
70
|
+
}
|
|
71
|
+
const dependency = absolute.lastIndexOf("node_modules/");
|
|
72
|
+
return dependency === -1 ? absolute : absolute.slice(dependency);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Rewrites esbuild's module comments to those stable labels.
|
|
77
|
+
*
|
|
78
|
+
* The paths come from the metafile rather than from a pattern over the output,
|
|
79
|
+
* so only lines esbuild actually wrote are touched and a comment in someone's
|
|
80
|
+
* own source is left alone.
|
|
81
|
+
*/
|
|
82
|
+
function stableModulePaths(
|
|
83
|
+
text: string,
|
|
84
|
+
metafile: Metafile,
|
|
85
|
+
appletRoot: string,
|
|
86
|
+
): string {
|
|
87
|
+
const root = real(appletRoot);
|
|
88
|
+
const labels = new Map(
|
|
89
|
+
Object.keys(metafile.inputs).map((input) => [
|
|
90
|
+
input,
|
|
91
|
+
moduleLabel(real(input), root),
|
|
92
|
+
]),
|
|
93
|
+
);
|
|
94
|
+
return text
|
|
95
|
+
.split("\n")
|
|
96
|
+
.map((line) => {
|
|
97
|
+
if (!line.startsWith("// ")) return line;
|
|
98
|
+
const label = labels.get(line.slice(3));
|
|
99
|
+
return label === undefined ? line : `// ${label}`;
|
|
100
|
+
})
|
|
101
|
+
.join("\n");
|
|
44
102
|
}
|
|
45
103
|
|
|
46
104
|
async function bundle(options: {
|
|
@@ -68,15 +126,16 @@ async function bundle(options: {
|
|
|
68
126
|
minify: options.minify,
|
|
69
127
|
legalComments: "none",
|
|
70
128
|
external: options.external,
|
|
71
|
-
alias:
|
|
129
|
+
alias: { ...SDK_ENTRIES },
|
|
72
130
|
nodePaths: bundlerNodePaths(),
|
|
73
131
|
conditions: ["import", "module", "browser", "default"],
|
|
74
132
|
define: { "process.env.NODE_ENV": '"production"' },
|
|
133
|
+
metafile: true,
|
|
75
134
|
logLevel: "silent",
|
|
76
135
|
});
|
|
77
136
|
const file = result.outputFiles?.[0];
|
|
78
137
|
if (!file) throw new Error("The bundler produced no output");
|
|
79
|
-
return file.text;
|
|
138
|
+
return stableModulePaths(file.text, result.metafile, options.resolveDir);
|
|
80
139
|
}
|
|
81
140
|
|
|
82
141
|
function page(title: string, script: string): string {
|
|
@@ -130,12 +189,12 @@ export async function readDescription(
|
|
|
130
189
|
}
|
|
131
190
|
}
|
|
132
191
|
|
|
133
|
-
|
|
192
|
+
/** The two bundles, in the order that lets a bundle failure precede a boot. */
|
|
193
|
+
export async function bundleAppletArtifacts(
|
|
134
194
|
directory: string,
|
|
135
|
-
): Promise<
|
|
195
|
+
): Promise<{ descriptorId: string; server: string; ui: string }> {
|
|
136
196
|
const descriptor = await readDescriptor(directory);
|
|
137
|
-
|
|
138
|
-
const serverCode = await bundle({
|
|
197
|
+
const server = await bundle({
|
|
139
198
|
// `Applet` is the export name the kernel's facet mount looks up; the author
|
|
140
199
|
// writes an ordinary default export and never learns that name.
|
|
141
200
|
stdin:
|
|
@@ -147,7 +206,6 @@ export async function buildApplet(
|
|
|
147
206
|
minify: false,
|
|
148
207
|
loaderName: "applet-server-entry.ts",
|
|
149
208
|
});
|
|
150
|
-
|
|
151
209
|
const uiScript = await bundle({
|
|
152
210
|
stdin: 'import "./ui";\n',
|
|
153
211
|
resolveDir: directory,
|
|
@@ -157,27 +215,34 @@ export async function buildApplet(
|
|
|
157
215
|
minify: true,
|
|
158
216
|
loaderName: "applet-ui-entry.tsx",
|
|
159
217
|
});
|
|
160
|
-
|
|
218
|
+
return {
|
|
219
|
+
descriptorId: descriptor.id,
|
|
220
|
+
server,
|
|
221
|
+
ui: page(descriptor.displayName, uiScript),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
161
224
|
|
|
162
|
-
|
|
163
|
-
|
|
225
|
+
/** The manifest for artifacts whose declarations have been read. */
|
|
226
|
+
export function appletManifest(
|
|
227
|
+
tools: AppletDescriptionV1["tools"],
|
|
228
|
+
artifacts: { server: string; ui: string },
|
|
229
|
+
): AppletBuildManifestV1 {
|
|
230
|
+
return {
|
|
164
231
|
contract: 1,
|
|
165
|
-
tools
|
|
166
|
-
hashes: { server: sha256(
|
|
232
|
+
tools,
|
|
233
|
+
hashes: { server: sha256(artifacts.server), ui: sha256(artifacts.ui) },
|
|
167
234
|
};
|
|
235
|
+
}
|
|
168
236
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
await
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
);
|
|
181
|
-
|
|
182
|
-
return { directory, serverPath, uiPath, manifestPath, manifest };
|
|
237
|
+
/** Bundle, boot, describe. The whole artifact derivation, with no output. */
|
|
238
|
+
export async function buildAppletArtifacts(
|
|
239
|
+
directory: string,
|
|
240
|
+
): Promise<AppletArtifactsV1> {
|
|
241
|
+
const { descriptorId, server, ui } = await bundleAppletArtifacts(directory);
|
|
242
|
+
const description = await readDescription(server, descriptorId);
|
|
243
|
+
return {
|
|
244
|
+
manifest: appletManifest(description.tools, { server, ui }),
|
|
245
|
+
server,
|
|
246
|
+
ui,
|
|
247
|
+
};
|
|
183
248
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* The check stage — the type checker and the linter, one diagnostic list.
|
|
3
3
|
*
|
|
4
4
|
* The Applet has no `node_modules`: it is source at a durable root. So the
|
|
5
5
|
* compiler options are built here from `paths.ts` rather than from a tsconfig
|
|
@@ -106,7 +106,7 @@ export async function typeCheckApplet(
|
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
108
|
|
|
109
|
-
/** Everything
|
|
109
|
+
/** Everything the check stage reports, in source order. */
|
|
110
110
|
export async function checkApplet(
|
|
111
111
|
directory: string,
|
|
112
112
|
): Promise<AppletDiagnostic[]> {
|
|
@@ -7,7 +7,7 @@ import { APPLET_CONTRACT_VERSION } from "../protocol/index.js";
|
|
|
7
7
|
import type { AppletToolDeclarationV1 } from "../server/applet.js";
|
|
8
8
|
|
|
9
9
|
export interface AppletDescriptorV1 {
|
|
10
|
-
/** `/^[a-z][a-z0-9-]{0,31}$/`; the directory name
|
|
10
|
+
/** `/^[a-z][a-z0-9-]{0,31}$/`; the scaffold's directory name. */
|
|
11
11
|
id: string;
|
|
12
12
|
displayName: string;
|
|
13
13
|
contract: 1;
|
|
@@ -50,9 +50,7 @@ export async function readDescriptor(
|
|
|
50
50
|
try {
|
|
51
51
|
text = await readFile(path, "utf8");
|
|
52
52
|
} catch {
|
|
53
|
-
throw new Error(
|
|
54
|
-
`No applet.json in ${directory}; run \`applet new <name>\` first`,
|
|
55
|
-
);
|
|
53
|
+
throw new Error(`No applet.json in ${directory}`);
|
|
56
54
|
}
|
|
57
55
|
return decodeDescriptor(JSON.parse(text));
|
|
58
56
|
}
|
|
@@ -17,11 +17,12 @@ const require = createRequire(import.meta.url);
|
|
|
17
17
|
* Found by walking up to this package's own `package.json`, not by counting
|
|
18
18
|
* directories.
|
|
19
19
|
*
|
|
20
|
-
* The same module runs from
|
|
21
|
-
* bundled `dist/cli.mjs` under Node
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
20
|
+
* The same module runs from three depths: `src/build/paths.ts` under Bun, the
|
|
21
|
+
* bundled `dist/cli.mjs` under Node, and the build service's own bundle, which
|
|
22
|
+
* is emitted into `dist/` for exactly this reason. A fixed `../../` is right
|
|
23
|
+
* for one and silently wrong for the others — it would resolve the SDK's
|
|
24
|
+
* entries to a directory that does not exist and every Applet import would
|
|
25
|
+
* fail to type-check with no explanation.
|
|
25
26
|
*/
|
|
26
27
|
function findSdkRoot(): string {
|
|
27
28
|
let directory = dirname(fileURLToPath(import.meta.url));
|
|
@@ -92,7 +93,7 @@ export function typeCheckerPaths(): Record<string, string[]> {
|
|
|
92
93
|
paths["react/jsx-runtime"] = [join(types, "jsx-runtime.d.ts")];
|
|
93
94
|
paths["react/*"] = [join(types, "*")];
|
|
94
95
|
} catch {
|
|
95
|
-
// No React declarations available;
|
|
96
|
+
// No React declarations available; the check stage reports the import.
|
|
96
97
|
}
|
|
97
98
|
return paths;
|
|
98
99
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
}
|
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Miniflare gives the built `dist/server.js` the one thing no fake can: a
|
|
5
5
|
* SQLite-backed Durable Object with hibernating WebSockets, which is exactly
|
|
6
|
-
* what the loader gives it in production.
|
|
7
|
-
*
|
|
6
|
+
* what the loader gives it in production. The build uses it to ask the
|
|
7
|
+
* mounted class what
|
|
8
8
|
* tools it declares rather than guessing from the source.
|
|
9
9
|
*/
|
|
10
10
|
|
package/src/client/index.ts
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* The page never opens the socket itself: the host sends an `init` postMessage
|
|
18
18
|
* carrying the theme tokens and a short-lived viewer token, and `createApplet`
|
|
19
19
|
* connects from that. `connect(init)` is the same path, called by hand, which
|
|
20
|
-
* is what
|
|
20
|
+
* is what the tests use.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import type { Collection } from "@tanstack/db";
|
package/src/lint/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@frockbot/applet-sdk/lint` — the flat config, the rules, and the one call
|
|
3
|
-
*
|
|
3
|
+
* the check stage makes.
|
|
4
4
|
*
|
|
5
5
|
* A diagnostic is the SDK's whole answer to "what did I do wrong": the CLI
|
|
6
6
|
* prints `path:line:col message` and nothing else, so what a Bot must remember
|
package/src/server/applet.ts
CHANGED
|
@@ -61,7 +61,7 @@ export interface AppletHealthV1 {
|
|
|
61
61
|
schemaRevision: number;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
/** The build-time description
|
|
64
|
+
/** The build-time description the build writes into the manifest. */
|
|
65
65
|
export interface AppletDescriptionV1 {
|
|
66
66
|
contract: 1;
|
|
67
67
|
tools: AppletToolDeclarationV1[];
|
|
@@ -250,7 +250,7 @@ export abstract class Applet<
|
|
|
250
250
|
};
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
/** The tool declarations, for
|
|
253
|
+
/** The tool declarations, for the build to write into the manifest. */
|
|
254
254
|
describe(): AppletDescriptionV1 {
|
|
255
255
|
return {
|
|
256
256
|
contract: APPLET_CONTRACT_VERSION,
|
package/template/README.md
CHANGED
|
@@ -9,14 +9,11 @@ A FrockBot Applet. Two files are yours:
|
|
|
9
9
|
|
|
10
10
|
## The loop
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
Open the printed URL in the Computer's browser to look at it. Publish with
|
|
19
|
-
`applet_publish` once `applet check` is clean.
|
|
12
|
+
`applet_files` and `applet_read_file` to see what is here, `applet_write_file`
|
|
13
|
+
to change it, then `applet_check` — it type-checks, lints, bundles and boots
|
|
14
|
+
your server, and answers either with every problem as `path:line:col message`
|
|
15
|
+
or with the tools it declares and a URL for the page. Publish with
|
|
16
|
+
`applet_publish` once the check is clean.
|
|
20
17
|
|
|
21
18
|
## Rules the linter enforces
|
|
22
19
|
|