@frockbot/applet-sdk 0.7.180 → 0.7.181

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 CHANGED
@@ -68,11 +68,11 @@ the type checker; `build` goes on to the module and its manifest.
68
68
  each model provider a `stream`. A Plugin declares at most 64 tools, each
69
69
  name once.
70
70
 
71
- Each describe spawns its own workerd, and `boot.ts` bounds the boot. A
72
- runtime that is not ready within `BOOT_DEADLINE_MS` (30 seconds), or whose
73
- spawn fails outright, is a `RuntimeDidNotStart`. The build lets that runtime
74
- go rather than waiting on it and boots once more; if the second boot does not
75
- come up either, the stage fails with that as its diagnostic.
71
+ Each describe spawns its own workerd, and both ends of its life are bounded
72
+ so a build answers rather than hangs: a runtime not ready within
73
+ `BOOT_DEADLINE_MS` (30 seconds) fails the stage with that as its diagnostic,
74
+ and the teardown, awaited on every path, gets `DISPOSE_DEADLINE_MS`
75
+ (10 seconds).
76
76
 
77
77
  A build answers the module text and its manifest:
78
78
  `{ contract: 1, tools, hooks, services, triggers, views, cards, modelProviders, hashes: { module } }`,
package/package.json CHANGED
@@ -1,39 +1,28 @@
1
1
  {
2
2
  "name": "@frockbot/applet-sdk",
3
- "version": "0.7.180",
3
+ "version": "0.7.181",
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 build pipeline.",
6
+ "description": "Authoring SDK for FrockBot Plugins: the declarations a Plugin is written against, and the build pipeline the cloud build service runs.",
7
7
  "license": "UNLICENSED",
8
8
  "exports": {
9
- "./lint": "./src/lint/index.ts",
10
9
  "./build/plugin": "./src/build/plugin.ts",
11
10
  "./plugin": "./plugin/index.d.ts",
12
11
  "./package.json": "./package.json"
13
12
  },
14
13
  "files": [
15
14
  "src",
16
- "types",
17
- "template",
18
15
  "plugin",
19
16
  "README.md"
20
17
  ],
21
18
  "scripts": {
22
- "test": "bun test test spike",
19
+ "test": "bun test test",
23
20
  "typecheck": "tsc --noEmit -p tsconfig.json"
24
21
  },
25
22
  "dependencies": {
26
- "@tanstack/db": "0.8.7",
27
- "@tanstack/react-db": "0.3.7",
28
- "@types/react": "19.2.18",
29
- "@types/react-dom": "19.2.7",
30
23
  "esbuild": "0.28.2",
31
- "eslint": "10.10.0",
32
24
  "miniflare": "5.20260828.0-alpha",
33
- "react": "19.2.8",
34
- "react-dom": "19.2.8",
35
- "typescript": "npm:typescript-native-bridge@6.0.3-bridge.16.tsgo.7.0.2",
36
- "typescript-eslint": "8.69.0"
25
+ "typescript": "npm:typescript-native-bridge@6.0.3-bridge.16.tsgo.7.0.2"
37
26
  },
38
27
  "devDependencies": {
39
28
  "@types/bun": "1.4.1",
package/plugin/index.d.ts CHANGED
@@ -3,10 +3,11 @@
3
3
  * against (ADR 0026).
4
4
  *
5
5
  * A Plugin is one ESM module with no imports of its own. It exports `tools`
6
- * and `execute`, and may export `hooks`, `services` and `triggers`. The
7
- * kernel's generated index imports the built module, checks these exports
8
- * against the Plugin's `plugin.json` once at mount, and hands every call a
9
- * narrow `ctx` naming only what that Plugin declared it may do.
6
+ * and `execute`, and may export `hooks`, `services`, `triggers`, `views`,
7
+ * `cards` and `modelProviders` (`PluginModule`). The kernel's generated index
8
+ * imports the built module, checks these exports against the Plugin's
9
+ * `plugin.json` once at mount, and hands every call a narrow `ctx` naming
10
+ * only what that Plugin declared it may do.
10
11
  *
11
12
  * These declarations are types only. They mirror `core/contracts/isolate.ts`
12
13
  * member for member — `BOT_ISOLATE_CONTEXT_KEYS_V1` is the list this file's
@@ -1,28 +1,21 @@
1
1
  /**
2
- * Where the SDK is on disk, and how an Applet's imports resolve to it.
3
- *
4
- * An Applet lives at a durable root with no `node_modules` of its own — it is
5
- * synchronised source, not an npm project — so every specifier it may write is
6
- * mapped here, once, and the same map feeds the type checker and the bundler.
2
+ * Where the SDK is on disk, so the Plugin build can resolve
3
+ * `@frockbot/applet-sdk/plugin` to its declarations.
7
4
  */
8
5
 
9
6
  import { readFileSync } from "node:fs";
10
- import { createRequire } from "node:module";
11
7
  import { dirname, join } from "node:path";
12
8
  import { fileURLToPath } from "node:url";
13
9
 
14
- const require = createRequire(import.meta.url);
15
-
16
10
  /**
17
11
  * Found by walking up to this package's own `package.json`, not by counting
18
12
  * directories.
19
13
  *
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.
14
+ * The same module runs from two depths: `src/build/paths.ts` under Bun, and
15
+ * the build service's own bundle, which is emitted into `dist/` for exactly
16
+ * this reason. A fixed `../../` is right for one and silently wrong for the
17
+ * other — it would resolve the Plugin declarations to a file that does not
18
+ * exist and every Plugin import would fail to type-check with no explanation.
26
19
  */
27
20
  function findSdkRoot(): string {
28
21
  let directory = dirname(fileURLToPath(import.meta.url));
@@ -40,63 +33,12 @@ function findSdkRoot(): string {
40
33
  directory = parent;
41
34
  }
42
35
  throw new Error(
43
- "the Applets SDK cannot find its own package root; reinstall @frockbot/applet-sdk",
36
+ "the Plugin SDK cannot find its own package root; reinstall @frockbot/applet-sdk",
44
37
  );
45
38
  }
46
39
 
47
40
  /** The installed `@frockbot/applet-sdk` directory. */
48
41
  export const SDK_ROOT = findSdkRoot();
49
42
 
50
- export const SDK_ENTRIES = {
51
- "@frockbot/applet-sdk/server": join(SDK_ROOT, "src/server/index.ts"),
52
- "@frockbot/applet-sdk/client": join(SDK_ROOT, "src/client/index.ts"),
53
- "@frockbot/applet-sdk/kit": join(SDK_ROOT, "src/kit/index.tsx"),
54
- "@frockbot/applet-sdk/protocol": join(SDK_ROOT, "src/protocol/index.ts"),
55
- } as const;
56
-
57
- /** The ambient declaration of the one Cloudflare module the SDK names. */
58
- export const SDK_WORKERS_TYPES = join(
59
- SDK_ROOT,
60
- "types/cloudflare-workers.d.ts",
61
- );
62
-
63
43
  /** The Plugin declarations (`@frockbot/applet-sdk/plugin`), types only. */
64
44
  export const SDK_PLUGIN_TYPES = join(SDK_ROOT, "plugin/index.d.ts");
65
-
66
- function packageDirectory(specifier: string): string {
67
- return dirname(require.resolve(`${specifier}/package.json`));
68
- }
69
-
70
- /** Node module directories the bundler searches for React and TanStack DB. */
71
- export function bundlerNodePaths(): string[] {
72
- const paths = new Set<string>([join(SDK_ROOT, "node_modules")]);
73
- for (const specifier of [
74
- "react",
75
- "react-dom",
76
- "@tanstack/db",
77
- "@tanstack/react-db",
78
- ]) {
79
- try {
80
- paths.add(join(packageDirectory(specifier), "..", ".."));
81
- } catch {
82
- // Resolved through the SDK's own node_modules instead.
83
- }
84
- }
85
- return [...paths];
86
- }
87
-
88
- /** `paths` for the type checker: the SDK's entries plus React's declarations. */
89
- export function typeCheckerPaths(): Record<string, string[]> {
90
- const paths: Record<string, string[]> = {};
91
- for (const [specifier, file] of Object.entries(SDK_ENTRIES))
92
- paths[specifier] = [file];
93
- try {
94
- const types = packageDirectory("@types/react");
95
- paths.react = [join(types, "index.d.ts")];
96
- paths["react/jsx-runtime"] = [join(types, "jsx-runtime.d.ts")];
97
- paths["react/*"] = [join(types, "*")];
98
- } catch {
99
- // No React declarations available; the check stage reports the import.
100
- }
101
- return paths;
102
- }
@@ -27,7 +27,6 @@ import { build as esbuild } from "esbuild";
27
27
  import { convertV4MiniflareOptions, Miniflare } from "miniflare";
28
28
  import ts from "typescript";
29
29
 
30
- import type { AppletDiagnostic } from "../lint/index.js";
31
30
  import { stableModulePaths } from "./module-paths.js";
32
31
  import { SDK_PLUGIN_TYPES } from "./paths.js";
33
32
 
@@ -67,6 +66,15 @@ async function within<T>(
67
66
  export type PluginBuildStage =
68
67
  "descriptor" | "typecheck" | "bundle" | "describe";
69
68
 
69
+ export interface PluginDiagnostic {
70
+ /** Path relative to the Plugin's directory. */
71
+ file: string;
72
+ line: number;
73
+ column: number;
74
+ message: string;
75
+ severity: "error" | "warning";
76
+ }
77
+
70
78
  /** The Plugin's `plugin.json`, as far as the build reads it. */
71
79
  export interface PluginBuildDescriptorV1 {
72
80
  id: string;
@@ -100,11 +108,10 @@ export type PluginBuildOutcome =
100
108
  | {
101
109
  status: "failed";
102
110
  stage: PluginBuildStage;
103
- diagnostics: AppletDiagnostic[];
111
+ diagnostics: PluginDiagnostic[];
104
112
  };
105
113
 
106
114
  const PLUGIN_ID = /^[a-z][a-z0-9-]{0,63}$/;
107
- const TOOL_NAME = /^[a-z][a-z0-9_]{0,63}$/;
108
115
  const MAX_TOOLS = 64;
109
116
 
110
117
  const COMPILER_OPTIONS: ts.CompilerOptions = {
@@ -122,7 +129,7 @@ const COMPILER_OPTIONS: ts.CompilerOptions = {
122
129
  types: [],
123
130
  };
124
131
 
125
- function thrown(error: unknown, file = "plugin.json"): AppletDiagnostic[] {
132
+ function thrown(error: unknown, file = "plugin.json"): PluginDiagnostic[] {
126
133
  return [
127
134
  {
128
135
  file,
@@ -184,7 +191,7 @@ async function pluginSources(directory: string): Promise<string[]> {
184
191
  /** Type-check the Plugin against the SDK's Plugin declarations. */
185
192
  export async function typeCheckPlugin(
186
193
  directory: string,
187
- ): Promise<AppletDiagnostic[]> {
194
+ ): Promise<PluginDiagnostic[]> {
188
195
  const root = resolve(directory);
189
196
  const files = await pluginSources(root);
190
197
  if (!files.some((file) => relative(root, file) === "plugin.ts")) {
package/src/lint/index.ts DELETED
@@ -1,148 +0,0 @@
1
- /**
2
- * `@frockbot/applet-sdk/lint` — the flat config, the rules, and the one call
3
- * the check stage makes.
4
- *
5
- * A diagnostic is the SDK's whole answer to "what did I do wrong": the CLI
6
- * prints `path:line:col message` and nothing else, so what a Bot must remember
7
- * is the message, not a manual.
8
- */
9
-
10
- import { readdir, readFile } from "node:fs/promises";
11
- import { join, relative, resolve } from "node:path";
12
-
13
- import { ESLint, type Linter, type Rule } from "eslint";
14
- import tseslint from "typescript-eslint";
15
-
16
- import { anchoredToToken, appletRules } from "./rules.js";
17
-
18
- export * from "./rules.js";
19
-
20
- export interface AppletDiagnostic {
21
- /** Path relative to the Applet's directory. */
22
- file: string;
23
- line: number;
24
- column: number;
25
- message: string;
26
- severity: "error" | "warning";
27
- }
28
-
29
- /** The one line the CLI prints per diagnostic. */
30
- export function formatDiagnostic(diagnostic: AppletDiagnostic): string {
31
- return `${diagnostic.file}:${diagnostic.line}:${diagnostic.column} ${diagnostic.message}`;
32
- }
33
-
34
- export const appletPlugin = {
35
- rules: appletRules as unknown as Record<string, Rule.RuleModule>,
36
- };
37
-
38
- /** The flat config an Applet is linted with. */
39
- export function appletLintConfig(): Linter.Config[] {
40
- return [
41
- {
42
- files: ["**/*.ts", "**/*.tsx"],
43
- languageOptions: {
44
- parser: tseslint.parser as unknown as Linter.Parser,
45
- parserOptions: {
46
- ecmaVersion: 2023,
47
- sourceType: "module",
48
- ecmaFeatures: { jsx: true },
49
- },
50
- },
51
- plugins: { applet: appletPlugin },
52
- rules: {
53
- "applet/no-raw-colors": "error",
54
- "applet/no-network": "error",
55
- "applet/allowed-imports": "error",
56
- "applet/tables-via-table": "error",
57
- "applet/tools-via-this-tool": "error",
58
- },
59
- },
60
- ];
61
- }
62
-
63
- const CSS_COLOR =
64
- /#[0-9a-fA-F]{3,8}\b|\brgba?\s*\([^)]*\)|\bhsla?\s*\([^)]*\)|\bcolor-mix\s*\(/g;
65
-
66
- /**
67
- * ESLint does not see `.css`, and a stylesheet is the easiest place to smuggle
68
- * a colour past the theme, so the same rule is applied here by hand.
69
- */
70
- export function lintCssText(text: string, file: string): AppletDiagnostic[] {
71
- const diagnostics: AppletDiagnostic[] = [];
72
- const lines = text.split("\n");
73
- lines.forEach((line, index) => {
74
- if (line.trimStart().startsWith("/*")) return;
75
- for (const match of line.matchAll(CSS_COLOR)) {
76
- // Judge the declaration the literal sits in, the same unit the TSX rule
77
- // judges: a literal is fine where its own value names a theme token.
78
- const start = line.lastIndexOf(";", match.index) + 1;
79
- const end = line.indexOf(";", match.index);
80
- const declaration = line.slice(start, end === -1 ? undefined : end);
81
- if (anchoredToToken(declaration)) continue;
82
- diagnostics.push({
83
- file,
84
- line: index + 1,
85
- column: match.index + 1,
86
- message:
87
- `"${match[0]}" is a raw colour; use a --frockbot-* theme token ` +
88
- "(or a var(--frockbot-…, fallback)).",
89
- severity: "error",
90
- });
91
- }
92
- });
93
- return diagnostics;
94
- }
95
-
96
- async function sourceFiles(
97
- directory: string,
98
- extensions: string[],
99
- ): Promise<string[]> {
100
- const found: string[] = [];
101
- const walk = async (current: string): Promise<void> => {
102
- for (const entry of await readdir(current, { withFileTypes: true })) {
103
- if (entry.name === "node_modules" || entry.name === "dist") continue;
104
- if (entry.name.startsWith(".")) continue;
105
- const path = join(current, entry.name);
106
- if (entry.isDirectory()) await walk(path);
107
- else if (extensions.some((extension) => entry.name.endsWith(extension))) {
108
- found.push(path);
109
- }
110
- }
111
- };
112
- await walk(directory);
113
- return found;
114
- }
115
-
116
- /** Lint one Applet directory. Returns diagnostics; never throws on a finding. */
117
- export async function lintApplet(
118
- directory: string,
119
- ): Promise<AppletDiagnostic[]> {
120
- const root = resolve(directory);
121
- const eslint = new ESLint({
122
- cwd: root,
123
- overrideConfigFile: true,
124
- overrideConfig: appletLintConfig(),
125
- errorOnUnmatchedPattern: false,
126
- });
127
- const results = await eslint.lintFiles(
128
- await sourceFiles(root, [".ts", ".tsx"]),
129
- );
130
- const diagnostics: AppletDiagnostic[] = [];
131
- for (const result of results) {
132
- for (const message of result.messages) {
133
- diagnostics.push({
134
- file: relative(root, result.filePath),
135
- line: message.line ?? 1,
136
- column: message.column ?? 1,
137
- message: `${message.message}${message.ruleId ? ` (${message.ruleId})` : ""}`,
138
- severity: message.severity === 2 ? "error" : "warning",
139
- });
140
- }
141
- }
142
- for (const path of await sourceFiles(root, [".css"])) {
143
- diagnostics.push(
144
- ...lintCssText(await readFile(path, "utf8"), relative(root, path)),
145
- );
146
- }
147
- return diagnostics;
148
- }
package/src/lint/rules.ts DELETED
@@ -1,394 +0,0 @@
1
- /**
2
- * The five rules that keep an Applet inside the SDK.
3
- *
4
- * Each one exists because of a way an Applet can look right and be wrong: a
5
- * hard-coded colour that ignores the user's theme, a network call the loader
6
- * would block anyway, an import that will not exist at build time, and state
7
- * or tools declared in a shape the server cannot see. This set grows from
8
- * observed failures — add a rule and a test here, never a paragraph in a
9
- * prompt.
10
- */
11
-
12
- /** A syntax node, walked structurally so no parser type leaks into the rules. */
13
- export interface AstNode {
14
- type: string;
15
- [field: string]: unknown;
16
- }
17
-
18
- export interface RuleContext {
19
- report(descriptor: { node: AstNode; message: string }): void;
20
- }
21
-
22
- export interface AppletRule {
23
- meta: {
24
- type: "problem" | "suggestion";
25
- docs: { description: string };
26
- schema: [];
27
- messages?: Record<string, string>;
28
- };
29
- create(context: RuleContext): Record<string, (node: AstNode) => void>;
30
- }
31
-
32
- function child(node: AstNode | undefined, field: string): AstNode | undefined {
33
- const value = node?.[field];
34
- return value && typeof value === "object" ? (value as AstNode) : undefined;
35
- }
36
-
37
- function name(node: AstNode | undefined): string | undefined {
38
- if (!node) return undefined;
39
- if (node.type === "Identifier" && typeof node.name === "string")
40
- return node.name;
41
- if (node.type === "Literal" && typeof node.value === "string")
42
- return node.value;
43
- return undefined;
44
- }
45
-
46
- function list(node: AstNode | undefined, field: string): AstNode[] {
47
- const value = node?.[field];
48
- return Array.isArray(value) ? (value as AstNode[]) : [];
49
- }
50
-
51
- // ---------------------------------------------------------------------------
52
- // no-raw-colors
53
- // ---------------------------------------------------------------------------
54
-
55
- const FUNCTIONAL_COLOR =
56
- /#[0-9a-fA-F]{3,8}\b|\brgba?\s*\(|\bhsla?\s*\(|\bcolor-mix\s*\(/;
57
- const NAMED_COLORS = new Set([
58
- "aqua",
59
- "black",
60
- "blue",
61
- "brown",
62
- "cyan",
63
- "fuchsia",
64
- "gold",
65
- "gray",
66
- "green",
67
- "grey",
68
- "indigo",
69
- "lime",
70
- "magenta",
71
- "maroon",
72
- "navy",
73
- "olive",
74
- "orange",
75
- "pink",
76
- "purple",
77
- "red",
78
- "silver",
79
- "teal",
80
- "violet",
81
- "white",
82
- "yellow",
83
- ]);
84
- const COLOR_PROPERTIES = new Set([
85
- "color",
86
- "background",
87
- "backgroundColor",
88
- "borderColor",
89
- "borderTopColor",
90
- "borderRightColor",
91
- "borderBottomColor",
92
- "borderLeftColor",
93
- "outlineColor",
94
- "fill",
95
- "stroke",
96
- "caretColor",
97
- "accentColor",
98
- "textDecorationColor",
99
- "boxShadow",
100
- "textShadow",
101
- ]);
102
-
103
- /**
104
- * A colour literal is fine only where the same value is anchored to a theme
105
- * token: as a `var(--frockbot-x, #fallback)` fallback, or as an ingredient of a
106
- * `color-mix()` over one. Anything else is a colour the User's theme cannot
107
- * move.
108
- */
109
- export function anchoredToToken(text: string): boolean {
110
- return /var\(\s*--frockbot-[a-z-]+/.test(text);
111
- }
112
-
113
- export const noRawColors: AppletRule = {
114
- meta: {
115
- type: "problem",
116
- docs: {
117
- description:
118
- "Colours come from the nine --frockbot-* theme tokens, never from a literal",
119
- },
120
- schema: [],
121
- },
122
- create(context) {
123
- const flag = (node: AstNode, text: string) => {
124
- if (anchoredToToken(text)) return;
125
- if (!FUNCTIONAL_COLOR.test(text)) return;
126
- context.report({
127
- node,
128
- message:
129
- "Use a --frockbot-* theme token instead of a colour literal " +
130
- "(the kit's components already do).",
131
- });
132
- };
133
- return {
134
- Literal(node) {
135
- if (typeof node.value === "string") flag(node, node.value);
136
- },
137
- TemplateElement(node) {
138
- const value = child(node, "value");
139
- const raw = value?.raw;
140
- if (typeof raw === "string") flag(node, raw);
141
- },
142
- Property(node) {
143
- const key = name(child(node, "key"));
144
- if (!key || !COLOR_PROPERTIES.has(key)) return;
145
- const value = child(node, "value");
146
- if (value?.type !== "Literal" || typeof value.value !== "string")
147
- return;
148
- const text = value.value.trim().toLowerCase();
149
- if (anchoredToToken(text) || !NAMED_COLORS.has(text)) return;
150
- context.report({
151
- node: value,
152
- message: `"${text}" is a raw colour; use a --frockbot-* theme token.`,
153
- });
154
- },
155
- };
156
- },
157
- };
158
-
159
- // ---------------------------------------------------------------------------
160
- // no-network
161
- // ---------------------------------------------------------------------------
162
-
163
- const FORBIDDEN_CONSTRUCTORS = new Set([
164
- "XMLHttpRequest",
165
- "WebSocket",
166
- "EventSource",
167
- ]);
168
-
169
- export const noNetwork: AppletRule = {
170
- meta: {
171
- type: "problem",
172
- docs: {
173
- description:
174
- "An Applet reaches the outside world through its tools, never directly",
175
- },
176
- schema: [],
177
- },
178
- create(context) {
179
- const complain = (node: AstNode, what: string) =>
180
- context.report({
181
- node,
182
- message:
183
- `${what} is not available to an Applet: the loader runs it with no ` +
184
- "outbound network. Add a tool on the server, or use the Applet socket.",
185
- });
186
- return {
187
- CallExpression(node) {
188
- const callee = child(node, "callee");
189
- if (name(callee) === "fetch") {
190
- complain(node, "fetch()");
191
- return;
192
- }
193
- if (callee?.type !== "MemberExpression") return;
194
- const property = name(child(callee, "property"));
195
- const object = name(child(callee, "object"));
196
- if (
197
- property === "fetch" &&
198
- (object === "window" || object === "globalThis")
199
- ) {
200
- complain(node, "fetch()");
201
- }
202
- if (property === "sendBeacon" && object === "navigator") {
203
- complain(node, "navigator.sendBeacon()");
204
- }
205
- },
206
- NewExpression(node) {
207
- const callee = name(child(node, "callee"));
208
- if (callee && FORBIDDEN_CONSTRUCTORS.has(callee))
209
- complain(node, `new ${callee}`);
210
- },
211
- };
212
- },
213
- };
214
-
215
- // ---------------------------------------------------------------------------
216
- // allowed-imports
217
- // ---------------------------------------------------------------------------
218
-
219
- function importAllowed(specifier: string): boolean {
220
- if (specifier.startsWith(".")) return true;
221
- if (specifier === "react" || specifier.startsWith("react/")) return true;
222
- return (
223
- specifier === "@frockbot/applet-sdk" ||
224
- specifier.startsWith("@frockbot/applet-sdk/")
225
- );
226
- }
227
-
228
- export const allowedImports: AppletRule = {
229
- meta: {
230
- type: "problem",
231
- docs: {
232
- description:
233
- "An Applet bundles from @frockbot/applet-sdk, react, and its own files only",
234
- },
235
- schema: [],
236
- },
237
- create(context) {
238
- const check = (node: AstNode, source: AstNode | undefined) => {
239
- const specifier = source?.type === "Literal" ? source.value : undefined;
240
- if (typeof specifier !== "string" || importAllowed(specifier)) return;
241
- context.report({
242
- node,
243
- message:
244
- `"${specifier}" cannot be imported: an Applet may import ` +
245
- "@frockbot/applet-sdk/*, react, and its own relative files.",
246
- });
247
- };
248
- return {
249
- ImportDeclaration: (node) => check(node, child(node, "source")),
250
- ExportNamedDeclaration: (node) => check(node, child(node, "source")),
251
- ExportAllDeclaration: (node) => check(node, child(node, "source")),
252
- ImportExpression: (node) => check(node, child(node, "source")),
253
- CallExpression(node) {
254
- if (name(child(node, "callee")) !== "require") return;
255
- check(node, list(node, "arguments")[0]);
256
- },
257
- };
258
- },
259
- };
260
-
261
- // ---------------------------------------------------------------------------
262
- // Class-shape rules
263
- // ---------------------------------------------------------------------------
264
-
265
- function isAppletClassBody(node: AstNode): boolean {
266
- const parent = child(node, "parent");
267
- return name(child(parent, "superClass")) === "Applet";
268
- }
269
-
270
- function declaredProperty(node: AstNode, field: string): AstNode | undefined {
271
- for (const member of list(node, "body")) {
272
- if (member.type !== "PropertyDefinition") continue;
273
- if (name(child(member, "key")) === field) return member;
274
- }
275
- return undefined;
276
- }
277
-
278
- export const tablesViaTable: AppletRule = {
279
- meta: {
280
- type: "problem",
281
- docs: {
282
- description: "Tables are declared with table() so the SDK can derive DDL",
283
- },
284
- schema: [],
285
- },
286
- create(context) {
287
- // `tables = tables` naming a `const tables = { … }` is the shape the
288
- // template uses, so the rule follows one level of indirection. The lookup
289
- // happens on `Program:exit` so the declaration may come after the class.
290
- const objectsByName = new Map<string, AstNode>();
291
- const classBodies: AstNode[] = [];
292
- return {
293
- VariableDeclarator(node) {
294
- const identifier = name(child(node, "id"));
295
- const init = child(node, "init");
296
- if (identifier && init?.type === "ObjectExpression") {
297
- objectsByName.set(identifier, init);
298
- }
299
- },
300
- ClassBody(node) {
301
- if (isAppletClassBody(node)) classBodies.push(node);
302
- },
303
- "Program:exit"() {
304
- for (const body of classBodies) {
305
- const property = declaredProperty(body, "tables");
306
- if (!property) continue;
307
- const declared = child(property, "value");
308
- const value =
309
- declared?.type === "Identifier"
310
- ? objectsByName.get(name(declared) ?? "")
311
- : declared;
312
- if (value?.type !== "ObjectExpression") {
313
- context.report({
314
- node: property,
315
- message:
316
- "`tables` must be an object literal of table({ … }) declarations.",
317
- });
318
- continue;
319
- }
320
- for (const entry of list(value, "properties")) {
321
- const declaration = child(entry, "value");
322
- if (
323
- declaration?.type === "CallExpression" &&
324
- name(child(declaration, "callee")) === "table"
325
- ) {
326
- continue;
327
- }
328
- context.report({
329
- node: entry,
330
- message:
331
- "Each table must be declared with table({ … }) from the SDK.",
332
- });
333
- }
334
- }
335
- },
336
- };
337
- },
338
- };
339
-
340
- export const toolsViaThisTool: AppletRule = {
341
- meta: {
342
- type: "problem",
343
- docs: {
344
- description:
345
- "Tools are declared with this.tool() so health() can report them",
346
- },
347
- schema: [],
348
- },
349
- create(context) {
350
- return {
351
- ClassBody(node) {
352
- if (!isAppletClassBody(node)) return;
353
- const property = declaredProperty(node, "tools");
354
- if (!property) return;
355
- const value = child(property, "value");
356
- if (value?.type !== "ObjectExpression") {
357
- context.report({
358
- node: property,
359
- message:
360
- "`tools` must be an object literal of this.tool(…) declarations.",
361
- });
362
- return;
363
- }
364
- for (const entry of list(value, "properties")) {
365
- const declaration = child(entry, "value");
366
- const callee = child(declaration, "callee");
367
- if (
368
- declaration?.type === "CallExpression" &&
369
- callee?.type === "MemberExpression" &&
370
- child(callee, "object")?.type === "ThisExpression" &&
371
- name(child(callee, "property")) === "tool"
372
- ) {
373
- continue;
374
- }
375
- context.report({
376
- node: entry,
377
- message:
378
- "Each tool must be declared with this.tool({ … }, handler).",
379
- });
380
- }
381
- },
382
- };
383
- },
384
- };
385
-
386
- export const appletRules = {
387
- "no-raw-colors": noRawColors,
388
- "no-network": noNetwork,
389
- "allowed-imports": allowedImports,
390
- "tables-via-table": tablesViaTable,
391
- "tools-via-this-tool": toolsViaThisTool,
392
- } satisfies Record<string, AppletRule>;
393
-
394
- export type AppletRuleName = keyof typeof appletRules;
@@ -1,55 +0,0 @@
1
- /**
2
- * The only part of the Cloudflare programming model the SDK names.
3
- *
4
- * The ceiling is deliberate: an Applet is a Durable Object with alarms and
5
- * hibernating sockets, and no adapter hides that. What this file does is keep
6
- * the surface to the handful of members `server/` actually uses, so an Applet
7
- * author never types a binding name and the SDK type-checks without
8
- * `@cloudflare/workers-types` in scope. It lives outside `src/` so a
9
- * consumer that already has the real Workers types cannot see two declarations
10
- * of the same module.
11
- */
12
-
13
- declare module "cloudflare:workers" {
14
- export interface AppletSqlCursor {
15
- toArray(): Array<Record<string, unknown>>;
16
- }
17
-
18
- export interface AppletSqlStorageHandle {
19
- exec(query: string, ...bindings: unknown[]): AppletSqlCursor;
20
- }
21
-
22
- export interface AppletDurableObjectStorage {
23
- readonly sql: AppletSqlStorageHandle;
24
- transactionSync<T>(closure: () => T): T;
25
- deleteAll(): Promise<void>;
26
- }
27
-
28
- export interface AppletHibernatableWebSocket {
29
- send(message: string): void;
30
- close(code?: number, reason?: string): void;
31
- serializeAttachment(value: unknown): void;
32
- deserializeAttachment(): unknown;
33
- }
34
-
35
- export interface AppletDurableObjectState {
36
- readonly id: { toString(): string; readonly name?: string };
37
- readonly storage: AppletDurableObjectStorage;
38
- acceptWebSocket(socket: AppletHibernatableWebSocket, tags?: string[]): void;
39
- getWebSockets(tag?: string): AppletHibernatableWebSocket[];
40
- blockConcurrencyWhile<T>(closure: () => Promise<T>): Promise<T>;
41
- }
42
-
43
- export class DurableObject<Env = unknown> {
44
- constructor(ctx: AppletDurableObjectState, env: Env);
45
- protected readonly ctx: AppletDurableObjectState;
46
- protected readonly env: Env;
47
- }
48
- }
49
-
50
- declare const WebSocketPair: {
51
- new (): {
52
- 0: import("cloudflare:workers").AppletHibernatableWebSocket;
53
- 1: import("cloudflare:workers").AppletHibernatableWebSocket;
54
- };
55
- };