@flue/sdk 0.3.2 → 0.3.3
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/dist/index.mjs +101 -20
- package/package.json +1 -1
package/dist/index.mjs
CHANGED
|
@@ -127,6 +127,83 @@ function validateUserWranglerConfig(config) {
|
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
129
|
/**
|
|
130
|
+
* Compute Flue's migration contributions for a build.
|
|
131
|
+
*
|
|
132
|
+
* Algorithm:
|
|
133
|
+
*
|
|
134
|
+
* 1. Walk every existing migration entry in the user's config and union the
|
|
135
|
+
* SQLite-backed classes it declares — across `new_sqlite_classes` and
|
|
136
|
+
* the `to` side of `renamed_classes` / `transferred_classes`. The
|
|
137
|
+
* resulting set is "already-declared": every SQLite-backed class
|
|
138
|
+
* Cloudflare's runtime currently knows about for this Worker.
|
|
139
|
+
* `deleted_classes` and the `from` side of renames are subtracted, since
|
|
140
|
+
* they've been explicitly removed.
|
|
141
|
+
*
|
|
142
|
+
* KV-backed classes (`new_classes`) are deliberately NOT added to the
|
|
143
|
+
* "declared" set. Flue agents always need a SQLite-backed class for
|
|
144
|
+
* session storage; if a user happens to have a KV-backed DO with the
|
|
145
|
+
* same name as a Flue agent, we still need to emit our SQLite migration.
|
|
146
|
+
* The deploy itself will then surface a clear "class already defined"
|
|
147
|
+
* error from Cloudflare rather than silently shipping a broken worker
|
|
148
|
+
* where the agent has no working session store.
|
|
149
|
+
* 2. For each class in `currentClasses` that isn't already-declared, emit a
|
|
150
|
+
* deterministic per-class migration: one tag, one class. Per-class tags
|
|
151
|
+
* are essential because Cloudflare migration tags are immutable once
|
|
152
|
+
* deployed — packing all classes under a single shared tag (the original
|
|
153
|
+
* bug in issue #15) means new classes added on a redeploy are silently
|
|
154
|
+
* ignored. With per-class tags, every redeploy is a no-op except for
|
|
155
|
+
* the truly net-new classes.
|
|
156
|
+
*
|
|
157
|
+
* Renames and deletes are not auto-detected. If an agent file disappears,
|
|
158
|
+
* Flue silently emits no migration for it — Cloudflare's runtime keeps the
|
|
159
|
+
* orphaned class data alive but unbound, and the user can clean up (or
|
|
160
|
+
* rename to recover) by adding a manual `renamed_classes` / `deleted_classes`
|
|
161
|
+
* migration to their own wrangler.jsonc. Auto-emitting `deleted_classes`
|
|
162
|
+
* would destroy stored DO data on every accidental file removal, which is
|
|
163
|
+
* never the right default.
|
|
164
|
+
*
|
|
165
|
+
* Returned in alphabetical order so a regenerated `dist/wrangler.jsonc` is
|
|
166
|
+
* byte-identical across machines and CI runs.
|
|
167
|
+
*
|
|
168
|
+
* Pure function: takes the current class list + the user's existing
|
|
169
|
+
* migrations array (typically `userConfig.migrations` straight from
|
|
170
|
+
* wrangler's reader) and returns the migrations to append. Doesn't read or
|
|
171
|
+
* write any files.
|
|
172
|
+
*/
|
|
173
|
+
function computeFlueMigrations(currentClasses, userMigrations) {
|
|
174
|
+
const migrationsArray = Array.isArray(userMigrations) ? userMigrations : [];
|
|
175
|
+
const declared = /* @__PURE__ */ new Set();
|
|
176
|
+
for (const raw of migrationsArray) {
|
|
177
|
+
if (typeof raw !== "object" || raw === null) continue;
|
|
178
|
+
const m = raw;
|
|
179
|
+
const collectClassList = (key) => {
|
|
180
|
+
const v = m[key];
|
|
181
|
+
return Array.isArray(v) ? v.filter((c) => typeof c === "string") : [];
|
|
182
|
+
};
|
|
183
|
+
for (const c of collectClassList("new_sqlite_classes")) declared.add(c);
|
|
184
|
+
for (const c of collectClassList("deleted_classes")) declared.delete(c);
|
|
185
|
+
const renamed = Array.isArray(m.renamed_classes) ? m.renamed_classes : [];
|
|
186
|
+
for (const r of renamed) {
|
|
187
|
+
if (typeof r !== "object" || r === null) continue;
|
|
188
|
+
const obj = r;
|
|
189
|
+
if (typeof obj.from === "string") declared.delete(obj.from);
|
|
190
|
+
if (typeof obj.to === "string") declared.add(obj.to);
|
|
191
|
+
}
|
|
192
|
+
const transferred = Array.isArray(m.transferred_classes) ? m.transferred_classes : [];
|
|
193
|
+
for (const t of transferred) {
|
|
194
|
+
if (typeof t !== "object" || t === null) continue;
|
|
195
|
+
const obj = t;
|
|
196
|
+
if (typeof obj.to === "string") declared.add(obj.to);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const additions = [];
|
|
200
|
+
for (const c of [...currentClasses].sort()) if (!declared.has(c)) additions.push({
|
|
201
|
+
tag: `flue-class-${c}`,
|
|
202
|
+
new_sqlite_classes: [c]
|
|
203
|
+
});
|
|
204
|
+
return additions;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
130
207
|
* Produce the merged wrangler config: start from the user's, layer Flue's
|
|
131
208
|
* contributions on top. Pure function — caller handles reading and writing.
|
|
132
209
|
*/
|
|
@@ -149,7 +226,10 @@ function mergeFlueAdditions(userConfig, additions) {
|
|
|
149
226
|
const existingMigrations = Array.isArray(merged.migrations) ? merged.migrations : [];
|
|
150
227
|
const existingMigrationTags = new Set(existingMigrations.filter((m) => typeof m === "object" && m !== null).map((m) => m.tag).filter((t) => typeof t === "string"));
|
|
151
228
|
const migrationsOut = [...existingMigrations];
|
|
152
|
-
if (!existingMigrationTags.has(
|
|
229
|
+
for (const migration of additions.migrations) if (!existingMigrationTags.has(migration.tag)) {
|
|
230
|
+
migrationsOut.push(migration);
|
|
231
|
+
existingMigrationTags.add(migration.tag);
|
|
232
|
+
}
|
|
153
233
|
merged.migrations = migrationsOut;
|
|
154
234
|
return merged;
|
|
155
235
|
}
|
|
@@ -316,6 +396,9 @@ var CloudflarePlugin = class {
|
|
|
316
396
|
}`;
|
|
317
397
|
}).join("\n\n");
|
|
318
398
|
const { config: userConfig } = await this.getUserConfig(ctx.outputDir);
|
|
399
|
+
const sandboxClassNames = detectSandboxBindings(userConfig);
|
|
400
|
+
const sandboxReExports = sandboxClassNames.map((name) => `export { Sandbox as ${name} } from '@cloudflare/sandbox';`).join("\n");
|
|
401
|
+
const sandboxClassImport = sandboxClassNames.length > 0 ? `import { Sandbox as __FlueCfSandbox } from '@cloudflare/sandbox';` : "";
|
|
319
402
|
return `
|
|
320
403
|
// Auto-generated by @flue/sdk build (cloudflare)
|
|
321
404
|
import { Agent, routeAgentRequest } from 'agents';
|
|
@@ -327,7 +410,7 @@ import {
|
|
|
327
410
|
resolveModel,
|
|
328
411
|
} from '@flue/sdk/internal';
|
|
329
412
|
import { runWithCloudflareContext, cfSandboxToSessionEnv } from '@flue/sdk/cloudflare';
|
|
330
|
-
|
|
413
|
+
${sandboxClassImport ? "\n" + sandboxClassImport : ""}
|
|
331
414
|
${agentImports}
|
|
332
415
|
|
|
333
416
|
// ─── Config ─────────────────────────────────────────────────────────────────
|
|
@@ -371,19 +454,19 @@ async function createLocalEnv() {
|
|
|
371
454
|
|
|
372
455
|
/**
|
|
373
456
|
* Detect and wrap external sandbox instances (e.g. from @cloudflare/sandbox's
|
|
374
|
-
* getSandbox()). Returns SessionEnv if the
|
|
375
|
-
*
|
|
457
|
+
* getSandbox()). Returns SessionEnv if the value is a @cloudflare/sandbox
|
|
458
|
+
* RPC stub, null otherwise.
|
|
459
|
+
*
|
|
460
|
+
* NOTE: We must use \`instanceof\` here, not structural duck-typing. The value
|
|
461
|
+
* returned by \`getSandbox()\` is a workerd RPC Proxy that returns \`true\` for
|
|
462
|
+
* any \`in\` check and \`'function'\` for \`typeof <anything>\`, so structural
|
|
463
|
+
* checks (positive or negative) are unreliable against it. \`instanceof\` walks
|
|
464
|
+
* the prototype chain via the runtime, which the proxy can't fake.
|
|
376
465
|
*/
|
|
377
466
|
function resolveSandbox(sandbox) {
|
|
378
|
-
if (
|
|
379
|
-
sandbox && typeof sandbox === 'object' &&
|
|
380
|
-
typeof sandbox.exec === 'function' &&
|
|
381
|
-
typeof sandbox.readFile === 'function' &&
|
|
382
|
-
typeof sandbox.destroy === 'function' &&
|
|
383
|
-
!('getCwd' in sandbox) && !('fs' in sandbox)
|
|
384
|
-
) {
|
|
467
|
+
${sandboxClassNames.length > 0 ? `if (sandbox instanceof __FlueCfSandbox) {
|
|
385
468
|
return cfSandboxToSessionEnv(sandbox);
|
|
386
|
-
}
|
|
469
|
+
}` : "/* no @cloudflare/sandbox bindings declared in wrangler config */"}
|
|
387
470
|
return null;
|
|
388
471
|
}
|
|
389
472
|
|
|
@@ -613,7 +696,7 @@ ${agentClasses}
|
|
|
613
696
|
// \`@cloudflare/sandbox\` so each user-chosen class_name resolves at the
|
|
614
697
|
// bundle's top level. The binding + container image configuration is owned
|
|
615
698
|
// by the user's wrangler.jsonc.
|
|
616
|
-
${
|
|
699
|
+
${sandboxReExports}
|
|
617
700
|
|
|
618
701
|
// ─── Worker Fetch Handler ───────────────────────────────────────────────────
|
|
619
702
|
|
|
@@ -652,18 +735,16 @@ export default {
|
|
|
652
735
|
name: agentClassName(a.name)
|
|
653
736
|
}));
|
|
654
737
|
const flueSqliteClasses = flueBindings.map((b) => b.class_name);
|
|
738
|
+
const { config: userConfig, path: userConfigPath } = await this.getUserConfig(ctx.outputDir);
|
|
739
|
+
if (userConfigPath) console.log(`[flue] Merging with user wrangler config: ${userConfigPath}`);
|
|
740
|
+
validateUserWranglerConfig(userConfig);
|
|
741
|
+
const flueMigrations = computeFlueMigrations(flueSqliteClasses, userConfig.migrations);
|
|
655
742
|
const additions = {
|
|
656
743
|
defaultName: path.basename(ctx.outputDir) || "flue-agents",
|
|
657
744
|
main: "_entry.ts",
|
|
658
745
|
doBindings: flueBindings,
|
|
659
|
-
|
|
660
|
-
tag: "flue-v1",
|
|
661
|
-
new_sqlite_classes: flueSqliteClasses
|
|
662
|
-
}
|
|
746
|
+
migrations: flueMigrations
|
|
663
747
|
};
|
|
664
|
-
const { config: userConfig, path: userConfigPath } = await this.getUserConfig(ctx.outputDir);
|
|
665
|
-
if (userConfigPath) console.log(`[flue] Merging with user wrangler config: ${userConfigPath}`);
|
|
666
|
-
validateUserWranglerConfig(userConfig);
|
|
667
748
|
const sandboxClassNames = detectSandboxBindings(userConfig);
|
|
668
749
|
if (sandboxClassNames.length > 0) {
|
|
669
750
|
assertSandboxPackageInstalled(sandboxClassNames, [ctx.outputDir, ctx.workspaceDir]);
|