@johannc-dev/zyrk 0.0.0-stage → 0.2.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 +79 -2
- package/dist/build.d.ts +48 -0
- package/dist/build.js +338 -0
- package/dist/cli.d.ts +4 -0
- package/dist/cli.js +295 -0
- package/dist/client.d.ts +208 -0
- package/dist/client.js +416 -0
- package/dist/contracts.d.ts +312 -0
- package/dist/contracts.js +154 -0
- package/dist/errors.d.ts +91 -0
- package/dist/errors.js +58 -0
- package/dist/export.d.ts +87 -0
- package/dist/export.js +338 -0
- package/dist/globals.d.ts +59 -0
- package/dist/hourfile.d.ts +57 -0
- package/dist/hourfile.js +338 -0
- package/dist/limits.d.ts +91 -0
- package/dist/limits.js +54 -0
- package/dist/local-data.d.ts +106 -0
- package/dist/local-data.js +345 -0
- package/dist/local-replay.d.ts +40 -0
- package/dist/local-replay.js +342 -0
- package/dist/local-storage.d.ts +76 -0
- package/dist/local-storage.js +499 -0
- package/dist/local.d.ts +235 -0
- package/dist/local.js +1133 -0
- package/dist/sandbox-bootstrap.js +519 -0
- package/dist/sdk.d.ts +1129 -0
- package/dist/sdk.js +208 -0
- package/dist/stream.d.ts +36 -0
- package/dist/stream.js +69 -0
- package/dist/webhook.d.ts +43 -0
- package/dist/webhook.js +54 -0
- package/package.json +56 -4
package/README.md
CHANGED
|
@@ -1,3 +1,80 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @johannc-dev/zyrk 0.2
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
SDK et CLI des fonctions hébergées Zyrk (runtime funrun v2, V8). Node.js 24
|
|
4
|
+
minimum. Ce package contient les types du programme, quelques aides de
|
|
5
|
+
lecture, le compilateur `zyrk build`, le client HTTP et le flux d'événements.
|
|
6
|
+
Le moteur, le stockage et le sandbox restent sur le serveur.
|
|
7
|
+
|
|
8
|
+
On fournit de l'outillage (lecture rapide de l'historique en colonnes, travaux
|
|
9
|
+
parallèles, abonnements), jamais une stratégie : aucune aide de ce package ne
|
|
10
|
+
filtre, ne note ni ne décide à votre place.
|
|
11
|
+
|
|
12
|
+
```sh
|
|
13
|
+
npm i @johannc-dev/zyrk
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
```ts
|
|
17
|
+
import { defineProgram, tradeId, Side } from "@johannc-dev/zyrk";
|
|
18
|
+
|
|
19
|
+
export default defineProgram({
|
|
20
|
+
functions: {
|
|
21
|
+
async countBuys(ctx, args) {
|
|
22
|
+
const { wallet, sinceMs, untilMs } = args as {
|
|
23
|
+
wallet: string;
|
|
24
|
+
sinceMs: number;
|
|
25
|
+
untilMs: number;
|
|
26
|
+
};
|
|
27
|
+
let buys = 0,
|
|
28
|
+
last: string | null = null;
|
|
29
|
+
for await (const batch of ctx.data.walletTrades(wallet, {
|
|
30
|
+
sinceMs,
|
|
31
|
+
untilMs,
|
|
32
|
+
})) {
|
|
33
|
+
for (let i = 0; i < batch.length; i++)
|
|
34
|
+
if (batch.side[i] === Side.buy) {
|
|
35
|
+
buys++;
|
|
36
|
+
last = tradeId(batch, i);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { buys, last };
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npx zyrk build program.ts # vérifie et écrit .functions/program.js
|
|
47
|
+
export TH_API_URL=https://your-gateway.example.com TH_API_KEY=thk_...
|
|
48
|
+
npx zyrk deploy wallet program.ts
|
|
49
|
+
npx zyrk run wallet countBuys '{"wallet":"…","sinceMs":0,"untilMs":3600000}'
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`zyrk build` refuse les modules Node (`fs`, `net`, `node:*`…), `fetch` et les
|
|
53
|
+
autres API réseau dans votre code, et l'ancienne API 0.1 (`ctx.data.query`,
|
|
54
|
+
`ctx.data.pages`) avec un guide de migration.
|
|
55
|
+
|
|
56
|
+
Modules :
|
|
57
|
+
|
|
58
|
+
| Import | Contenu |
|
|
59
|
+
| ----------------------------- | ----------------------------------------------------- |
|
|
60
|
+
| `@johannc-dev/zyrk` | types du programme, `defineProgram`, aides de lecture |
|
|
61
|
+
| `@johannc-dev/zyrk/limits` | limites du runtime (importable dans un programme) |
|
|
62
|
+
| `@johannc-dev/zyrk/globals` | types des seuls globaux du sandbox (pour l'éditeur) |
|
|
63
|
+
| `@johannc-dev/zyrk/client` | `FunctionsClient` (Promise) et `ZyrkClient` (Effect) |
|
|
64
|
+
| `@johannc-dev/zyrk/stream` | flux WebSocket des exécutions |
|
|
65
|
+
| `@johannc-dev/zyrk/build` | `buildProgram`, `writeBundle` |
|
|
66
|
+
| `@johannc-dev/zyrk/contracts` | schémas des réponses HTTP |
|
|
67
|
+
| `@johannc-dev/zyrk/errors` | erreurs du client, du CLI et du build |
|
|
68
|
+
|
|
69
|
+
Chaque fonction et type est documenté dans l'éditeur (autocomplétion).
|
|
70
|
+
|
|
71
|
+
## Replays
|
|
72
|
+
|
|
73
|
+
`client.replay(program, request)` sends past events to a function in the
|
|
74
|
+
live delivery format. Results are not stored: read them while the replay
|
|
75
|
+
runs with `client.replayEmits(id, afterSeq)` (its `ctx.emit` values, then
|
|
76
|
+
each delivery's record; reading frees what was read; the replay pauses when
|
|
77
|
+
16 MiB wait and fails after 2 minutes unread). A replay runs 30 minutes at
|
|
78
|
+
most. Its `ctx.storage` is its own space, deleted at the end unless
|
|
79
|
+
`keepStorage: { to: "long" | "durable" }` copies it. Locally,
|
|
80
|
+
`runner.replay({ ..., onFeed })` does the same.
|
package/dist/build.d.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { BuildError } from "./errors.js";
|
|
3
|
+
export { BuildError } from "./errors.js";
|
|
4
|
+
export declare const BUNDLE_PATH = ".functions/program.js";
|
|
5
|
+
export interface BuildOptions {
|
|
6
|
+
/** Type-check against the SDK types first (default `true`). */
|
|
7
|
+
readonly typecheck?: boolean;
|
|
8
|
+
readonly minify?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface BuildResult {
|
|
11
|
+
readonly code: string;
|
|
12
|
+
readonly bytes: number;
|
|
13
|
+
/** Non-fatal findings, e.g. a dependency referring to `fetch`. */
|
|
14
|
+
readonly warnings: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/** Globals a program cannot use: there is no network in the sandbox. */
|
|
17
|
+
export declare const FORBIDDEN_GLOBALS: readonly ["fetch", "XMLHttpRequest", "WebSocket", "EventSource"];
|
|
18
|
+
/** `fs`, `fs/promises`, `node:anything`. */
|
|
19
|
+
export declare const isNodeModule: (specifier: string) => boolean;
|
|
20
|
+
export interface Finding {
|
|
21
|
+
readonly file: string;
|
|
22
|
+
readonly line: number;
|
|
23
|
+
readonly column: number;
|
|
24
|
+
readonly text: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* References to the global `fetch` (and other network APIs): `fetch(…)`,
|
|
28
|
+
* `globalThis.fetch`, `self["fetch"]`, `{ fetch }`. Ignores comments,
|
|
29
|
+
* strings, property names (`cache.fetch`), local bindings and `typeof fetch`.
|
|
30
|
+
*/
|
|
31
|
+
export declare function findNetworkGlobals(file: string, text: string): Finding[];
|
|
32
|
+
/** Uses of the 0.1 API: `ctx.data.query/pages`, `DataQuery`, `{ kind: "walletBuys", params }`. */
|
|
33
|
+
export declare function findV1Usage(file: string, text: string): Finding[];
|
|
34
|
+
export declare const V1_MIGRATION = "The 0.1 data API was removed in @johannc-dev/zyrk 0.2. History now comes hour by hour, in columns:\n\n ctx.data.query/pages({ kind: \"walletOperations\", wallet, params: { since_ms, until_ms } })\n -> for await (const batch of ctx.data.walletTrades(wallet, { sinceMs, untilMs })) { ... }\n kind \"walletBuys\" -> ctx.data.walletTrades(wallet, { sinceMs, untilMs, side: \"buy\" })\n kind \"tokenOperations\" -> ctx.data.tokenTrades(mint, { sinceMs, untilMs })\n kind \"tokenTotals\" -> ctx.data.tokenTotals(mint, { quote })\n kind \"volumeSearch\" -> for await (const page of ctx.data.volumeSearch({ since, until, ... }))\n\nNo cursor, no page of 1000: each batch is one hour (batch.length trades, one typed array per field).\n\"allow_partial\" becomes \"allowPartial\"; trade ids are \"hour:line:op\" (tradeId(batch, i)).";
|
|
35
|
+
/** Type-checks the entry against the SDK types and the sandbox globals. */
|
|
36
|
+
export declare const typecheck: (entry: string) => Effect.Effect<void, BuildError>;
|
|
37
|
+
/** Bundles, scans and (by default) type-checks the entry. */
|
|
38
|
+
export declare const buildProgramEffect: (entry: string, options?: BuildOptions | undefined) => Effect.Effect<BuildResult, BuildError, never>;
|
|
39
|
+
/** Builds and writes the bundle (default `.functions/program.js`). */
|
|
40
|
+
export declare const writeBundleEffect: (entry: string, options?: BuildOptions & {
|
|
41
|
+
readonly out?: string;
|
|
42
|
+
}) => Effect.Effect<BuildResult, BuildError, never>;
|
|
43
|
+
/** Promise API: rejects with a `BuildError`. */
|
|
44
|
+
export declare function buildProgram(entry: string, options?: BuildOptions): Promise<BuildResult>;
|
|
45
|
+
/** Promise API: rejects with a `BuildError`. */
|
|
46
|
+
export declare function writeBundle(entry: string, options?: BuildOptions & {
|
|
47
|
+
readonly out?: string;
|
|
48
|
+
}): Promise<BuildResult>;
|
package/dist/build.js
ADDED
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
import { build } from "esbuild";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { builtinModules } from "node:module";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { dirname, relative, resolve, sep } from "node:path";
|
|
6
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
7
|
+
import { Effect } from "effect";
|
|
8
|
+
import { MAX_BUNDLE_BYTES } from "./limits.js";
|
|
9
|
+
import { BuildError, messageOf } from "./errors.js";
|
|
10
|
+
export { BuildError } from "./errors.js";
|
|
11
|
+
export const BUNDLE_PATH = ".functions/program.js";
|
|
12
|
+
const here = (file) => fileURLToPath(new URL(file, import.meta.url));
|
|
13
|
+
const SDK_JS = here("./sdk.js");
|
|
14
|
+
const SDK_DTS = here("./sdk.d.ts");
|
|
15
|
+
const LIMITS_JS = here("./limits.js");
|
|
16
|
+
const GLOBALS_DTS = here("./globals.d.ts");
|
|
17
|
+
export const FORBIDDEN_GLOBALS = [
|
|
18
|
+
"fetch",
|
|
19
|
+
"XMLHttpRequest",
|
|
20
|
+
"WebSocket",
|
|
21
|
+
"EventSource",
|
|
22
|
+
];
|
|
23
|
+
const GLOBAL_OBJECTS = new Set(["globalThis", "window", "self", "global"]);
|
|
24
|
+
const CLIENT_ONLY = /^@johannc-dev\/zyrk\/(client|stream|build|contracts|errors|local|export|hourfile)$/;
|
|
25
|
+
const builtins = new Set(builtinModules);
|
|
26
|
+
export const isNodeModule = (specifier) => specifier.startsWith("node:") ||
|
|
27
|
+
builtins.has(specifier) ||
|
|
28
|
+
builtins.has(specifier.split("/")[0] ?? "");
|
|
29
|
+
const isDependency = (file) => file.split(/[\\/]/).includes("node_modules");
|
|
30
|
+
const scriptKind = (file) => /\.[cm]?tsx?$/.test(file)
|
|
31
|
+
? file.endsWith("x")
|
|
32
|
+
? ts.ScriptKind.TSX
|
|
33
|
+
: ts.ScriptKind.TS
|
|
34
|
+
: file.endsWith("x")
|
|
35
|
+
? ts.ScriptKind.JSX
|
|
36
|
+
: ts.ScriptKind.JS;
|
|
37
|
+
const parse = (file, text) => ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, scriptKind(file));
|
|
38
|
+
const at = (source, node, text) => {
|
|
39
|
+
const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source));
|
|
40
|
+
return { file: source.fileName, line: line + 1, column: character + 1, text };
|
|
41
|
+
};
|
|
42
|
+
const walk = (node, visit) => {
|
|
43
|
+
visit(node);
|
|
44
|
+
ts.forEachChild(node, (child) => walk(child, visit));
|
|
45
|
+
};
|
|
46
|
+
const declaredNames = (source) => {
|
|
47
|
+
const names = new Set();
|
|
48
|
+
const add = (name) => {
|
|
49
|
+
if (name && ts.isIdentifier(name))
|
|
50
|
+
names.add(name.text);
|
|
51
|
+
};
|
|
52
|
+
walk(source, (node) => {
|
|
53
|
+
if (ts.isVariableDeclaration(node) ||
|
|
54
|
+
ts.isFunctionDeclaration(node) ||
|
|
55
|
+
ts.isFunctionExpression(node) ||
|
|
56
|
+
ts.isClassDeclaration(node) ||
|
|
57
|
+
ts.isParameter(node) ||
|
|
58
|
+
ts.isBindingElement(node) ||
|
|
59
|
+
ts.isImportSpecifier(node) ||
|
|
60
|
+
ts.isImportClause(node) ||
|
|
61
|
+
ts.isNamespaceImport(node) ||
|
|
62
|
+
ts.isImportEqualsDeclaration(node))
|
|
63
|
+
add(node.name);
|
|
64
|
+
});
|
|
65
|
+
return names;
|
|
66
|
+
};
|
|
67
|
+
export function findNetworkGlobals(file, text) {
|
|
68
|
+
const source = parse(file, text);
|
|
69
|
+
const declared = declaredNames(source);
|
|
70
|
+
const forbidden = new Set(FORBIDDEN_GLOBALS);
|
|
71
|
+
const findings = [];
|
|
72
|
+
const isGlobalObject = (node) => ts.isIdentifier(node) &&
|
|
73
|
+
GLOBAL_OBJECTS.has(node.text) &&
|
|
74
|
+
!declared.has(node.text);
|
|
75
|
+
walk(source, (node) => {
|
|
76
|
+
if (ts.isElementAccessExpression(node) &&
|
|
77
|
+
ts.isStringLiteralLike(node.argumentExpression) &&
|
|
78
|
+
forbidden.has(node.argumentExpression.text) &&
|
|
79
|
+
isGlobalObject(node.expression)) {
|
|
80
|
+
findings.push(at(source, node, node.argumentExpression.text));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (!ts.isIdentifier(node) || !forbidden.has(node.text))
|
|
84
|
+
return;
|
|
85
|
+
const parent = node.parent;
|
|
86
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
87
|
+
if (isGlobalObject(parent.expression))
|
|
88
|
+
findings.push(at(source, parent, node.text));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (declared.has(node.text))
|
|
92
|
+
return;
|
|
93
|
+
if (ts.isTypeOfExpression(parent))
|
|
94
|
+
return;
|
|
95
|
+
if ((ts.isPropertyAssignment(parent) ||
|
|
96
|
+
ts.isPropertyDeclaration(parent) ||
|
|
97
|
+
ts.isPropertySignature(parent) ||
|
|
98
|
+
ts.isMethodDeclaration(parent) ||
|
|
99
|
+
ts.isMethodSignature(parent) ||
|
|
100
|
+
ts.isGetAccessorDeclaration(parent) ||
|
|
101
|
+
ts.isSetAccessorDeclaration(parent) ||
|
|
102
|
+
ts.isEnumMember(parent)) &&
|
|
103
|
+
parent.name === node)
|
|
104
|
+
return;
|
|
105
|
+
if (ts.isLabeledStatement(parent) && parent.label === node)
|
|
106
|
+
return;
|
|
107
|
+
if (ts.isQualifiedName(parent) ||
|
|
108
|
+
ts.isTypeReferenceNode(parent) ||
|
|
109
|
+
ts.isTypeQueryNode(parent) ||
|
|
110
|
+
ts.isExportSpecifier(parent) ||
|
|
111
|
+
ts.isImportSpecifier(parent))
|
|
112
|
+
return;
|
|
113
|
+
findings.push(at(source, node, node.text));
|
|
114
|
+
});
|
|
115
|
+
return findings;
|
|
116
|
+
}
|
|
117
|
+
const V1_KINDS = new Set([
|
|
118
|
+
"tokenOperations",
|
|
119
|
+
"walletOperations",
|
|
120
|
+
"walletBuys",
|
|
121
|
+
"tokenTotals",
|
|
122
|
+
"volumeSearch",
|
|
123
|
+
]);
|
|
124
|
+
const V1_TYPES = new Set(["DataQuery", "DataPage"]);
|
|
125
|
+
const SDK_MODULES = new Set([
|
|
126
|
+
"@johannc-dev/zyrk",
|
|
127
|
+
"@johannc-dev/zyrk/contracts",
|
|
128
|
+
]);
|
|
129
|
+
export function findV1Usage(file, text) {
|
|
130
|
+
const source = parse(file, text);
|
|
131
|
+
const findings = [];
|
|
132
|
+
walk(source, (node) => {
|
|
133
|
+
if (ts.isPropertyAccessExpression(node) &&
|
|
134
|
+
(node.name.text === "query" || node.name.text === "pages") &&
|
|
135
|
+
ts.isPropertyAccessExpression(node.expression) &&
|
|
136
|
+
node.expression.name.text === "data")
|
|
137
|
+
findings.push(at(source, node, `${node.getText(source)}(…)`));
|
|
138
|
+
else if (ts.isImportDeclaration(node) &&
|
|
139
|
+
ts.isStringLiteral(node.moduleSpecifier) &&
|
|
140
|
+
SDK_MODULES.has(node.moduleSpecifier.text) &&
|
|
141
|
+
node.importClause?.namedBindings &&
|
|
142
|
+
ts.isNamedImports(node.importClause.namedBindings)) {
|
|
143
|
+
for (const element of node.importClause.namedBindings.elements) {
|
|
144
|
+
const name = (element.propertyName ?? element.name).text;
|
|
145
|
+
if (V1_TYPES.has(name))
|
|
146
|
+
findings.push(at(source, element, `import ${name}`));
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else if (ts.isObjectLiteralExpression(node)) {
|
|
150
|
+
const props = new Map();
|
|
151
|
+
for (const p of node.properties)
|
|
152
|
+
if (ts.isPropertyAssignment(p) && ts.isIdentifier(p.name))
|
|
153
|
+
props.set(p.name.text, p.initializer);
|
|
154
|
+
const kind = props.get("kind");
|
|
155
|
+
if (kind &&
|
|
156
|
+
ts.isStringLiteralLike(kind) &&
|
|
157
|
+
V1_KINDS.has(kind.text) &&
|
|
158
|
+
props.has("params"))
|
|
159
|
+
findings.push(at(source, node, `{ kind: "${kind.text}", params }`));
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
return findings;
|
|
163
|
+
}
|
|
164
|
+
const where = (cwd, f) => ` ${relative(cwd, f.file) || f.file}:${f.line}:${f.column} ${f.text}`;
|
|
165
|
+
export const V1_MIGRATION = `The 0.1 data API was removed in @johannc-dev/zyrk 0.2. History now comes hour by hour, in columns:
|
|
166
|
+
|
|
167
|
+
ctx.data.query/pages({ kind: "walletOperations", wallet, params: { since_ms, until_ms } })
|
|
168
|
+
-> for await (const batch of ctx.data.walletTrades(wallet, { sinceMs, untilMs })) { ... }
|
|
169
|
+
kind "walletBuys" -> ctx.data.walletTrades(wallet, { sinceMs, untilMs, side: "buy" })
|
|
170
|
+
kind "tokenOperations" -> ctx.data.tokenTrades(mint, { sinceMs, untilMs })
|
|
171
|
+
kind "tokenTotals" -> ctx.data.tokenTotals(mint, { quote })
|
|
172
|
+
kind "volumeSearch" -> for await (const page of ctx.data.volumeSearch({ since, until, ... }))
|
|
173
|
+
|
|
174
|
+
No cursor, no page of 1000: each batch is one hour (batch.length trades, one typed array per field).
|
|
175
|
+
"allow_partial" becomes "allowPartial"; trade ids are "hour:line:op" (tradeId(batch, i)).`;
|
|
176
|
+
export const typecheck = (entry) => Effect.suspend(() => {
|
|
177
|
+
const program = ts.createProgram([resolve(entry), GLOBALS_DTS], {
|
|
178
|
+
strict: true,
|
|
179
|
+
noEmit: true,
|
|
180
|
+
target: ts.ScriptTarget.ES2023,
|
|
181
|
+
module: ts.ModuleKind.ESNext,
|
|
182
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
183
|
+
allowImportingTsExtensions: true,
|
|
184
|
+
allowJs: true,
|
|
185
|
+
checkJs: false,
|
|
186
|
+
lib: ["lib.es2023.d.ts"],
|
|
187
|
+
types: [],
|
|
188
|
+
skipLibCheck: true,
|
|
189
|
+
paths: { "@johannc-dev/zyrk": [SDK_DTS] },
|
|
190
|
+
});
|
|
191
|
+
const diagnostics = ts.getPreEmitDiagnostics(program);
|
|
192
|
+
if (diagnostics.length === 0)
|
|
193
|
+
return Effect.void;
|
|
194
|
+
return Effect.fail(new BuildError({
|
|
195
|
+
stage: "typecheck",
|
|
196
|
+
message: ts.formatDiagnostics(diagnostics, {
|
|
197
|
+
getCanonicalFileName: (f) => f,
|
|
198
|
+
getCurrentDirectory: () => process.cwd(),
|
|
199
|
+
getNewLine: () => "\n",
|
|
200
|
+
}),
|
|
201
|
+
}));
|
|
202
|
+
});
|
|
203
|
+
const guestPlugin = {
|
|
204
|
+
name: "zyrk-guest",
|
|
205
|
+
setup(builder) {
|
|
206
|
+
builder.onResolve({ filter: /^@johannc-dev\/zyrk$/ }, () => ({
|
|
207
|
+
path: SDK_JS,
|
|
208
|
+
}));
|
|
209
|
+
builder.onResolve({ filter: /^@johannc-dev\/zyrk\/limits$/ }, () => ({
|
|
210
|
+
path: LIMITS_JS,
|
|
211
|
+
}));
|
|
212
|
+
builder.onResolve({ filter: CLIENT_ONLY }, (args) => ({
|
|
213
|
+
errors: [
|
|
214
|
+
{
|
|
215
|
+
text: `"${args.path}" runs on your machine (client, CLI, build), not inside a hosted program. Import "@johannc-dev/zyrk" instead.`,
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
}));
|
|
219
|
+
builder.onResolve({ filter: /.*/ }, async (args) => {
|
|
220
|
+
if (args.pluginData === "zyrk-resolve" || !isNodeModule(args.path))
|
|
221
|
+
return undefined;
|
|
222
|
+
if (!args.path.startsWith("node:")) {
|
|
223
|
+
const found = await builder.resolve(args.path, {
|
|
224
|
+
kind: args.kind,
|
|
225
|
+
importer: args.importer,
|
|
226
|
+
resolveDir: args.resolveDir,
|
|
227
|
+
pluginData: "zyrk-resolve",
|
|
228
|
+
});
|
|
229
|
+
if (found.errors.length === 0 && isDependency(found.path))
|
|
230
|
+
return found;
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
errors: [
|
|
234
|
+
{
|
|
235
|
+
text: `"${args.path}" is a Node module: hosted programs have no file system, network, process or Node API. Remove the import, or the dependency that needs it.`,
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
};
|
|
239
|
+
});
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
const formatMessage = (m) => m.location
|
|
243
|
+
? `${m.location.file}:${m.location.line}:${m.location.column + 1}: ${m.text}\n ${m.location.lineText}`
|
|
244
|
+
: m.text;
|
|
245
|
+
const bundle = (entry, options) => Effect.tryPromise({
|
|
246
|
+
try: () => build({
|
|
247
|
+
entryPoints: [resolve(entry)],
|
|
248
|
+
bundle: true,
|
|
249
|
+
format: "iife",
|
|
250
|
+
globalName: "__app",
|
|
251
|
+
platform: "neutral",
|
|
252
|
+
mainFields: ["module", "main"],
|
|
253
|
+
target: "es2023",
|
|
254
|
+
write: false,
|
|
255
|
+
sourcemap: false,
|
|
256
|
+
metafile: true,
|
|
257
|
+
minify: options.minify ?? false,
|
|
258
|
+
logLevel: "silent",
|
|
259
|
+
plugins: [guestPlugin],
|
|
260
|
+
}),
|
|
261
|
+
catch: (cause) => {
|
|
262
|
+
const errors = cause.errors ?? [];
|
|
263
|
+
const ours = errors.filter((e) => e.pluginName === "zyrk-guest");
|
|
264
|
+
return new BuildError({
|
|
265
|
+
stage: ours.length ? "forbidden_import" : "bundle",
|
|
266
|
+
message: errors.length
|
|
267
|
+
? (ours.length ? ours : errors).map(formatMessage).join("\n")
|
|
268
|
+
: messageOf(cause),
|
|
269
|
+
});
|
|
270
|
+
},
|
|
271
|
+
});
|
|
272
|
+
const scan = Effect.fnUntraced(function* (inputs) {
|
|
273
|
+
const cwd = process.cwd();
|
|
274
|
+
const sources = yield* Effect.tryPromise({
|
|
275
|
+
try: () => Promise.all(inputs
|
|
276
|
+
.map((input) => resolve(cwd, input))
|
|
277
|
+
.filter((file) => file !== SDK_JS && file !== LIMITS_JS)
|
|
278
|
+
.map(async (file) => ({ file, text: await readFile(file, "utf8") }))),
|
|
279
|
+
catch: (cause) => new BuildError({ stage: "bundle", message: messageOf(cause) }),
|
|
280
|
+
});
|
|
281
|
+
const own = sources.filter((s) => !isDependency(s.file));
|
|
282
|
+
const v1 = own.flatMap((s) => findV1Usage(s.file, s.text));
|
|
283
|
+
if (v1.length)
|
|
284
|
+
return yield* new BuildError({
|
|
285
|
+
stage: "v1_api",
|
|
286
|
+
message: `${v1.map((f) => where(cwd, f)).join("\n")}\n\n${V1_MIGRATION}`,
|
|
287
|
+
});
|
|
288
|
+
const network = own.flatMap((s) => findNetworkGlobals(s.file, s.text));
|
|
289
|
+
if (network.length)
|
|
290
|
+
return yield* new BuildError({
|
|
291
|
+
stage: "forbidden_global",
|
|
292
|
+
message: `${network.map((f) => where(cwd, f)).join("\n")}\n\nHosted programs have no network access: fetch, XMLHttpRequest, WebSocket and EventSource do not exist in the sandbox. Read history with ctx.data, receive live events with subscriptions, and send results with return or ctx.emit.`,
|
|
293
|
+
});
|
|
294
|
+
const warnings = [];
|
|
295
|
+
const byPackage = new Map();
|
|
296
|
+
for (const s of sources.filter((s) => isDependency(s.file)))
|
|
297
|
+
for (const f of findNetworkGlobals(s.file, s.text)) {
|
|
298
|
+
const parts = s.file.split(sep);
|
|
299
|
+
const at = parts.lastIndexOf("node_modules");
|
|
300
|
+
const name = parts[at + 1]?.startsWith("@")
|
|
301
|
+
? `${parts[at + 1]}/${parts[at + 2]}`
|
|
302
|
+
: (parts[at + 1] ?? s.file);
|
|
303
|
+
byPackage.set(name, [...(byPackage.get(name) ?? []), f]);
|
|
304
|
+
}
|
|
305
|
+
for (const [name, found] of byPackage)
|
|
306
|
+
warnings.push(`dependency "${name}" refers to ${[...new Set(found.map((f) => f.text))].join(", ")} (${where(cwd, found[0]).trim()}): it is undefined in the sandbox, that code path will throw.`);
|
|
307
|
+
return warnings;
|
|
308
|
+
});
|
|
309
|
+
export const buildProgramEffect = Effect.fnUntraced(function* (entry, options = {}) {
|
|
310
|
+
const built = yield* bundle(entry, options);
|
|
311
|
+
const warnings = yield* scan(Object.keys(built.metafile?.inputs ?? {}));
|
|
312
|
+
if (options.typecheck ?? true)
|
|
313
|
+
yield* typecheck(entry);
|
|
314
|
+
const code = built.outputFiles?.[0]?.text;
|
|
315
|
+
if (!code)
|
|
316
|
+
return yield* new BuildError({ stage: "bundle", message: "Empty bundle" });
|
|
317
|
+
const bytes = Buffer.byteLength(code);
|
|
318
|
+
if (bytes > MAX_BUNDLE_BYTES)
|
|
319
|
+
return yield* new BuildError({
|
|
320
|
+
stage: "size",
|
|
321
|
+
message: `bundle_too_large: ${bytes} bytes (max ${MAX_BUNDLE_BYTES}). Large dependencies are the usual cause; try --minify.`,
|
|
322
|
+
});
|
|
323
|
+
return { code, bytes, warnings };
|
|
324
|
+
});
|
|
325
|
+
export const writeBundleEffect = (entry, options = {}) => buildProgramEffect(entry, options).pipe(Effect.tap((result) => Effect.tryPromise({
|
|
326
|
+
try: async () => {
|
|
327
|
+
const out = options.out ?? BUNDLE_PATH;
|
|
328
|
+
await mkdir(dirname(out), { recursive: true });
|
|
329
|
+
await writeFile(out, result.code);
|
|
330
|
+
},
|
|
331
|
+
catch: (cause) => new BuildError({ stage: "write", message: messageOf(cause) }),
|
|
332
|
+
})));
|
|
333
|
+
export function buildProgram(entry, options) {
|
|
334
|
+
return Effect.runPromise(buildProgramEffect(entry, options));
|
|
335
|
+
}
|
|
336
|
+
export function writeBundle(entry, options) {
|
|
337
|
+
return Effect.runPromise(writeBundleEffect(entry, options));
|
|
338
|
+
}
|
package/dist/cli.d.ts
ADDED