okengine 0.10.1 → 0.10.2
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 +1 -1
- package/package.json +1 -1
- package/site/content/docs/elements/clock.mdx +2 -2
- package/site/content/docs/elements/flow.mdx +10 -10
- package/site/content/docs/elements/gate.mdx +4 -4
- package/site/content/docs/elements/signal.mdx +4 -7
- package/site/content/docs/elements/store.mdx +1 -1
- package/site/content/docs/elements/vault.mdx +1 -1
- package/site/content/docs/get-started/basic-usage.mdx +1 -3
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/reference/cli.md +12 -12
- package/site/content/docs/reference/fx.mdx +3 -4
- package/src/auth/auth.test.ts +1 -2
- package/src/auth/bindings.ts +5 -15
- package/src/auth/gate-auth.test.ts +2 -2
- package/src/cli/ai-setup/apply.ts +13 -0
- package/src/cli/build.test.ts +67 -0
- package/src/cli/build.ts +34 -0
- package/src/cli/db-seed.ts +1 -1
- package/src/cli/dev-controls.test.ts +43 -48
- package/src/cli/dev-controls.ts +23 -172
- package/src/cli/dev.test.ts +2 -4
- package/src/cli/dev.ts +37 -16
- package/src/client/notes-contract.test.ts +3 -4
- package/src/compiler/aot.test.ts +1 -2
- package/src/compiler/extract.test.ts +76 -21
- package/src/compiler/extract.ts +3 -3
- package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
- package/src/compiler/fixtures/skyport/src/flows/bookings/index.ts +4 -8
- package/src/compiler/fixtures/skyport/src/flows/payments/index.ts +1 -2
- package/src/compiler/fixtures/skyport/src/flows/support/index.ts +1 -2
- package/src/compiler/fixtures/skyport.expected.json +3 -3
- package/src/compiler/fixtures/triggers/five-triggers.ts +5 -10
- package/src/compiler/generate-adopt.test.ts +85 -0
- package/src/compiler/generate-adopt.ts +85 -0
- package/src/console/server/dry-run.audit.test.ts +2 -2
- package/src/console/server/flows.ts +50 -150
- package/src/docker/compose.ts +13 -3
- package/src/docker/recipes/llama-cpp.ts +10 -1
- package/src/docker/stack-id.ts +1 -0
- package/src/elements/channel/declare.ts +27 -1
- package/src/elements/clock/chaos-child.ts +3 -6
- package/src/elements/clock/chaos.test.ts +1 -2
- package/src/elements/clock.test.ts +2 -4
- package/src/elements/signal/declare.ts +25 -1
- package/src/elements/store/declare.ts +27 -1
- package/src/elements/store/resource.ts +6 -11
- package/src/elements/store/upsert-app.test.ts +1 -2
- package/src/elements/vault/declare.ts +29 -1
- package/src/kernel/adopt-barrel-fresh.test.ts +103 -0
- package/src/kernel/app.ts +66 -9
- package/src/kernel/auto-registry.test.ts +198 -0
- package/src/kernel/boot-bind/honor-config.test.ts +5 -5
- package/src/kernel/boot.test.ts +19 -15
- package/src/kernel/boot.ts +81 -1
- package/src/kernel/call.test.ts +5 -10
- package/src/kernel/compensate.test.ts +3 -6
- package/src/kernel/concurrency.test.ts +3 -5
- package/src/kernel/correlation.test.ts +4 -8
- package/src/kernel/edge.test.ts +1 -1
- package/src/kernel/effects-stamping.test.ts +4 -7
- package/src/kernel/element-registries.ts +27 -0
- package/src/kernel/errors.ts +11 -0
- package/src/kernel/flow.test.ts +8 -15
- package/src/kernel/flow.ts +12 -14
- package/src/kernel/hook-timing.test.ts +2 -2
- package/src/kernel/hooks.test.ts +5 -10
- package/src/kernel/horizontal-child.ts +5 -10
- package/src/kernel/journal-boot.test.ts +2 -4
- package/src/kernel/pipeline.test.ts +6 -12
- package/src/kernel/plugin/capabilities.test.ts +1 -1
- package/src/kernel/plugin/decorate.test.ts +3 -8
- package/src/kernel/plugin/scoping.test.ts +6 -16
- package/src/kernel/plugin-elements.test.ts +1 -1
- package/src/kernel/plugin-needs.test.ts +1 -1
- package/src/kernel/ready.test.ts +1 -2
- package/src/kernel/registry-isolation.test.ts +5 -5
- package/src/kernel/triggers.ts +1 -1
- package/src/plugins/anonymous.ts +1 -3
- package/src/plugins/compression.test.ts +6 -8
- package/src/plugins/config-source.test.ts +1 -1
- package/src/plugins/config-source.ts +1 -2
- package/src/plugins/cors.test.ts +10 -10
- package/src/plugins/csrf.test.ts +2 -2
- package/src/plugins/headers.test.ts +14 -15
- package/src/plugins/ip-allowlist.test.ts +9 -9
- package/src/plugins/magic-link.ts +2 -6
- package/src/plugins/maintenance-mode.test.ts +6 -6
- package/src/plugins/otp.ts +5 -15
- package/src/plugins/passkey.ts +4 -12
- package/src/plugins/two-factor.ts +3 -9
- package/src/plugins/username.ts +2 -6
- package/src/release/measure.ts +1 -1
- package/src/runs/runs.test.ts +3 -5
- package/src/runtime/serve.test.ts +2 -4
- package/src/test/create-test-app.test.ts +3 -7
- package/src/test/provisions.integration.test.ts +4 -9
- package/src/test/reset-element-registries.ts +31 -0
|
@@ -123,8 +123,7 @@ describe("extractManifest — performance", () => {
|
|
|
123
123
|
parts.push(`
|
|
124
124
|
export const flow_${i} = on(
|
|
125
125
|
http.get("/f/${i}"),
|
|
126
|
-
flow({
|
|
127
|
-
name: "synth.flow_${i}",
|
|
126
|
+
flow("synth.flow_${i}", {
|
|
128
127
|
do: async (input, fx) => {
|
|
129
128
|
await fx.store(db).insert(items).values(input);
|
|
130
129
|
const rows = await fx.store(db).select().from(items);
|
|
@@ -166,8 +165,7 @@ export const daily = { name: "daily" };
|
|
|
166
165
|
|
|
167
166
|
export const listLinks = on(
|
|
168
167
|
http.get("/links"),
|
|
169
|
-
flow({
|
|
170
|
-
name: "rel.root",
|
|
168
|
+
flow("rel.root", {
|
|
171
169
|
do: async (_input, fx) => {
|
|
172
170
|
return fx.store(db).select().from(links);
|
|
173
171
|
},
|
|
@@ -178,8 +176,7 @@ export const listLinks = on(
|
|
|
178
176
|
// relations, so the related table must NOT appear in effects.
|
|
179
177
|
export const withDaily = on(
|
|
180
178
|
http.get("/links/with-daily"),
|
|
181
|
-
flow({
|
|
182
|
-
name: "rel.with",
|
|
179
|
+
flow("rel.with", {
|
|
183
180
|
do: async (_input, fx) => {
|
|
184
181
|
return fx.store(db).findMany({ with: { daily: true } });
|
|
185
182
|
},
|
|
@@ -354,9 +351,7 @@ export const files = store.files("uploads");
|
|
|
354
351
|
|
|
355
352
|
export const attach = on(
|
|
356
353
|
http.post("/attach").gate(gate.public),
|
|
357
|
-
flow({
|
|
358
|
-
name: "notes.attach",
|
|
359
|
-
unit: "notes",
|
|
354
|
+
flow("notes.attach", {
|
|
360
355
|
do: async (input, fx) => {
|
|
361
356
|
await fx.store(files).put("key", input.text);
|
|
362
357
|
return { ok: true };
|
|
@@ -377,9 +372,7 @@ export const files = store.files("uploads");
|
|
|
377
372
|
|
|
378
373
|
export const attach = on(
|
|
379
374
|
http.post("/attach").gate(gate.public),
|
|
380
|
-
flow({
|
|
381
|
-
name: "notes.attachImage",
|
|
382
|
-
unit: "notes",
|
|
375
|
+
flow("notes.attachImage", {
|
|
383
376
|
do: async (input, fx) => {
|
|
384
377
|
await fx.store(files).putImage("key", input.bytes);
|
|
385
378
|
return { ok: true };
|
|
@@ -400,9 +393,7 @@ export const files = store.files("uploads");
|
|
|
400
393
|
|
|
401
394
|
export const sweep = on(
|
|
402
395
|
every("1d"),
|
|
403
|
-
flow({
|
|
404
|
-
name: "notes.sweep",
|
|
405
|
-
unit: "notes",
|
|
396
|
+
flow("notes.sweep", {
|
|
406
397
|
do: async (_input, fx) => {
|
|
407
398
|
const keys = await fx.store(files).list();
|
|
408
399
|
return { count: keys.length };
|
|
@@ -429,9 +420,7 @@ export const noteCreatedMail = mail.template("note-created", {
|
|
|
429
420
|
|
|
430
421
|
export const onCreated = on(
|
|
431
422
|
http.post("/hook").gate(gate.public),
|
|
432
|
-
flow({
|
|
433
|
-
name: "notes.onCreated",
|
|
434
|
-
unit: "notes",
|
|
423
|
+
flow("notes.onCreated", {
|
|
435
424
|
do: async (payload, fx) => {
|
|
436
425
|
await fx.send(noteCreatedMail, { to: "you@localhost", data: payload });
|
|
437
426
|
},
|
|
@@ -454,9 +443,7 @@ export const otpTemplate = otp.template("otp-code", {});
|
|
|
454
443
|
|
|
455
444
|
export const send = on(
|
|
456
445
|
http.post("/otp").gate(gate.public),
|
|
457
|
-
flow({
|
|
458
|
-
name: "notes.sendOtp",
|
|
459
|
-
unit: "notes",
|
|
446
|
+
flow("notes.sendOtp", {
|
|
460
447
|
do: async (payload, fx) => {
|
|
461
448
|
await fx.send(otpTemplate, { to: "+10000000000", data: payload });
|
|
462
449
|
},
|
|
@@ -469,3 +456,71 @@ export const send = on(
|
|
|
469
456
|
expect(manifest.channels?.["otp-code"]?.medium).toBe("sms");
|
|
470
457
|
});
|
|
471
458
|
});
|
|
459
|
+
|
|
460
|
+
describe("extractManifest — flow(name, options) positional signature", () => {
|
|
461
|
+
test("bare flow(name, {...}) extracts name, in, out, effects", async () => {
|
|
462
|
+
const source = `
|
|
463
|
+
import { flow } from "okengine";
|
|
464
|
+
|
|
465
|
+
export const chargeOrder = flow("checkout.chargeOrder", {
|
|
466
|
+
in: ChargeIn,
|
|
467
|
+
out: ChargeOut,
|
|
468
|
+
effects: { reads: ["sql:orders"], writes: ["sql:orders"] },
|
|
469
|
+
do: async (input, fx) => {
|
|
470
|
+
return { ok: true };
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
`;
|
|
474
|
+
const manifest = await extractFromSources({ "src/flows/checkout.ts": source });
|
|
475
|
+
|
|
476
|
+
expect(manifest.flows?.["checkout.chargeOrder"]).toBeDefined();
|
|
477
|
+
expect(manifest.flows?.["checkout.chargeOrder"]?.in).toBeDefined();
|
|
478
|
+
expect(manifest.flows?.["checkout.chargeOrder"]?.out).toBeDefined();
|
|
479
|
+
expect(manifest.flows?.["checkout.chargeOrder"]?.effects?.reads).toEqual(["sql:orders"]);
|
|
480
|
+
expect(manifest.flows?.["checkout.chargeOrder"]?.effects?.writes).toEqual(["sql:orders"]);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("on(trigger, flow(name, {...})) extracts the same shape as bare flow()", async () => {
|
|
484
|
+
const source = `
|
|
485
|
+
import { on, http, flow } from "okengine";
|
|
486
|
+
|
|
487
|
+
export const getOrder = on(
|
|
488
|
+
http.get("/orders/:id"),
|
|
489
|
+
flow("checkout.getOrder", {
|
|
490
|
+
out: OrderOut,
|
|
491
|
+
effects: { reads: ["sql:orders"] },
|
|
492
|
+
do: async (input, fx) => {
|
|
493
|
+
return { id: input.id };
|
|
494
|
+
},
|
|
495
|
+
}),
|
|
496
|
+
);
|
|
497
|
+
`;
|
|
498
|
+
const manifest = await extractFromSources({ "src/flows/checkout.ts": source });
|
|
499
|
+
|
|
500
|
+
expect(manifest.flows?.["checkout.getOrder"]).toBeDefined();
|
|
501
|
+
expect(manifest.flows?.["checkout.getOrder"]?.trigger?.http?.method).toBe("GET");
|
|
502
|
+
expect(manifest.flows?.["checkout.getOrder"]?.out).toBeDefined();
|
|
503
|
+
expect(manifest.flows?.["checkout.getOrder"]?.effects?.reads).toEqual(["sql:orders"]);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
test("a stray `name:` key inside the options object is ignored — the positional arg wins", async () => {
|
|
507
|
+
// Regression guard for the exact class of AST-shape assumption that broke
|
|
508
|
+
// silently before (channel-alias tracking, `.put()` inference): the
|
|
509
|
+
// compiler must read the flow's identity from the first positional
|
|
510
|
+
// argument, never from a `name` property on the options object, even
|
|
511
|
+
// when one happens to be present.
|
|
512
|
+
const source = `
|
|
513
|
+
import { flow } from "okengine";
|
|
514
|
+
|
|
515
|
+
export const decoy = flow("checkout.realName", {
|
|
516
|
+
name: "checkout.decoyName",
|
|
517
|
+
effects: { reads: ["sql:orders"] },
|
|
518
|
+
do: async () => ({ ok: true }),
|
|
519
|
+
});
|
|
520
|
+
`;
|
|
521
|
+
const manifest = await extractFromSources({ "src/flows/checkout.ts": source });
|
|
522
|
+
|
|
523
|
+
expect(manifest.flows?.["checkout.realName"]).toBeDefined();
|
|
524
|
+
expect(manifest.flows?.["checkout.decoyName"]).toBeUndefined();
|
|
525
|
+
});
|
|
526
|
+
});
|
package/src/compiler/extract.ts
CHANGED
|
@@ -1159,11 +1159,11 @@ function registerFlow(args: {
|
|
|
1159
1159
|
scope: ProjectScope;
|
|
1160
1160
|
exportName: string | undefined;
|
|
1161
1161
|
}): void {
|
|
1162
|
-
const opts = objectArg(args.flowCall.arguments[
|
|
1162
|
+
const opts = objectArg(args.flowCall.arguments[1]);
|
|
1163
1163
|
if (!opts) return;
|
|
1164
1164
|
|
|
1165
1165
|
const name =
|
|
1166
|
-
|
|
1166
|
+
stringArg(args.flowCall.arguments[0]) ??
|
|
1167
1167
|
args.exportName ??
|
|
1168
1168
|
`flow_${Object.keys(args.scope.flows).length + 1}`;
|
|
1169
1169
|
|
|
@@ -1232,7 +1232,7 @@ function registerFlow(args: {
|
|
|
1232
1232
|
const errors = parseErrors(objectProp(opts, "errors"));
|
|
1233
1233
|
if (errors) flow.errors = errors;
|
|
1234
1234
|
|
|
1235
|
-
if (effects
|
|
1235
|
+
if (effects) flow.effects = effects;
|
|
1236
1236
|
|
|
1237
1237
|
const line = lineAt(args.file.source, args.flowCall.start ?? 0);
|
|
1238
1238
|
flow.source = `${args.file.path}:${line}`;
|
|
@@ -5,8 +5,7 @@ import { on, flow, http } from "okengine";
|
|
|
5
5
|
*/
|
|
6
6
|
export const escapeHatch = on(
|
|
7
7
|
http.get("/raw"),
|
|
8
|
-
flow({
|
|
9
|
-
name: "raw.unannotated",
|
|
8
|
+
flow("raw.unannotated", {
|
|
10
9
|
do: async (_, fx) => {
|
|
11
10
|
return fx.raw("select 1");
|
|
12
11
|
},
|
|
@@ -18,8 +17,7 @@ export const escapeHatch = on(
|
|
|
18
17
|
*/
|
|
19
18
|
export const annotated = on(
|
|
20
19
|
http.get("/raw-ok"),
|
|
21
|
-
flow({
|
|
22
|
-
name: "raw.annotated",
|
|
20
|
+
flow("raw.annotated", {
|
|
23
21
|
effects: { reads: ["sql:orders"] },
|
|
24
22
|
do: async (_, fx) => {
|
|
25
23
|
return fx.raw("select * from orders");
|
|
@@ -15,8 +15,7 @@ void 0;
|
|
|
15
15
|
|
|
16
16
|
export const create = on(
|
|
17
17
|
http.post("/bookings").gate(member, canBook, fair),
|
|
18
|
-
flow({
|
|
19
|
-
name: "bookings.create",
|
|
18
|
+
flow("bookings.create", {
|
|
20
19
|
in: BookingIn,
|
|
21
20
|
out: BookingOut,
|
|
22
21
|
errors: { FlightFull },
|
|
@@ -35,8 +34,7 @@ export const create = on(
|
|
|
35
34
|
|
|
36
35
|
export const mine = on(
|
|
37
36
|
http.get("/bookings").gate(member).live(),
|
|
38
|
-
flow({
|
|
39
|
-
name: "bookings.mine",
|
|
37
|
+
flow("bookings.mine", {
|
|
40
38
|
live: true,
|
|
41
39
|
do: (_, fx) =>
|
|
42
40
|
fx
|
|
@@ -47,13 +45,11 @@ export const mine = on(
|
|
|
47
45
|
}),
|
|
48
46
|
);
|
|
49
47
|
|
|
50
|
-
export const getBooking = flow({
|
|
51
|
-
name: "bookings.getBooking",
|
|
48
|
+
export const getBooking = flow("bookings.getBooking", {
|
|
52
49
|
do: ({ id }, fx) => fx.store(db).findById(bookings, id),
|
|
53
50
|
});
|
|
54
51
|
|
|
55
|
-
export const refundBooking = flow({
|
|
56
|
-
name: "bookings.refundBooking",
|
|
52
|
+
export const refundBooking = flow("bookings.refundBooking", {
|
|
57
53
|
do: async ({ id }, fx) => {
|
|
58
54
|
await fx.store(db).update(bookings).set({ status: "refunded" }).where({ id });
|
|
59
55
|
return fx.store(db).findById(bookings, id);
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { flow } from "okengine";
|
|
2
2
|
import { stripeKey } from "../../vault.ts";
|
|
3
3
|
|
|
4
|
-
export const chargeBooking = flow({
|
|
5
|
-
name: "payments.chargeBooking",
|
|
4
|
+
export const chargeBooking = flow("payments.chargeBooking", {
|
|
6
5
|
durable: true,
|
|
7
6
|
do: async ({ orderId }, fx) => {
|
|
8
7
|
// Journal only serializable step values — a Redacted is not journaled.
|
|
@@ -6,8 +6,7 @@ import { tickets } from "../../schema.ts";
|
|
|
6
6
|
|
|
7
7
|
export const createTicket = on(
|
|
8
8
|
http.post("/tickets").gate(member),
|
|
9
|
-
flow({
|
|
10
|
-
name: "support.createTicket",
|
|
9
|
+
flow("support.createTicket", {
|
|
11
10
|
pii: "masked",
|
|
12
11
|
cost: { estimatePerCall: 0.011, budget: 0.02 },
|
|
13
12
|
do: async (input, fx) => {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"effects": {
|
|
33
33
|
"reads": ["sql:bookings"]
|
|
34
34
|
},
|
|
35
|
-
"source": "src/flows/bookings/index.ts:
|
|
35
|
+
"source": "src/flows/bookings/index.ts:48"
|
|
36
36
|
},
|
|
37
37
|
"bookings.mine": {
|
|
38
38
|
"trigger": {
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"effects": {
|
|
46
46
|
"reads": ["sql:bookings"]
|
|
47
47
|
},
|
|
48
|
-
"source": "src/flows/bookings/index.ts:
|
|
48
|
+
"source": "src/flows/bookings/index.ts:37",
|
|
49
49
|
"live": true,
|
|
50
50
|
"cacheKeys": "computed:sql:bookings/userId"
|
|
51
51
|
},
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"reads": ["sql:bookings"],
|
|
55
55
|
"writes": ["sql:bookings"]
|
|
56
56
|
},
|
|
57
|
-
"source": "src/flows/bookings/index.ts:
|
|
57
|
+
"source": "src/flows/bookings/index.ts:52"
|
|
58
58
|
},
|
|
59
59
|
"payments.chargeBooking": {
|
|
60
60
|
"effects": {
|
|
@@ -16,8 +16,7 @@ export const linkClicked = signal("link-clicked", {
|
|
|
16
16
|
// ① HTTP
|
|
17
17
|
export const create = on(
|
|
18
18
|
http.post("/orders"),
|
|
19
|
-
flow({
|
|
20
|
-
name: "triggers.http",
|
|
19
|
+
flow("triggers.http", {
|
|
21
20
|
do: async (input, fx) => {
|
|
22
21
|
await fx.store(db).insert(orders).values(input);
|
|
23
22
|
return { ok: true };
|
|
@@ -28,8 +27,7 @@ export const create = on(
|
|
|
28
27
|
// ② CLOCK / every
|
|
29
28
|
on(
|
|
30
29
|
every("10m"),
|
|
31
|
-
flow({
|
|
32
|
-
name: "triggers.every",
|
|
30
|
+
flow("triggers.every", {
|
|
33
31
|
do: (_, fx) => fx.store(db).delete(links).where({ createdAt: 0 }),
|
|
34
32
|
}),
|
|
35
33
|
);
|
|
@@ -37,8 +35,7 @@ on(
|
|
|
37
35
|
// ③ SIGNAL
|
|
38
36
|
on(
|
|
39
37
|
linkClicked,
|
|
40
|
-
flow({
|
|
41
|
-
name: "triggers.signal",
|
|
38
|
+
flow("triggers.signal", {
|
|
42
39
|
do: async ({ code }, fx) => {
|
|
43
40
|
const clicks = await fx.store(db).increment(links, code, "clicks");
|
|
44
41
|
await fx.emit(linkClicked, { code, clicks });
|
|
@@ -49,8 +46,7 @@ on(
|
|
|
49
46
|
// ④ CDC
|
|
50
47
|
on(
|
|
51
48
|
table("orders").changed("status"),
|
|
52
|
-
flow({
|
|
53
|
-
name: "triggers.cdc",
|
|
49
|
+
flow("triggers.cdc", {
|
|
54
50
|
do: ({ before, after }, fx) => fx.store(db).insert(orders).values({ before, after }),
|
|
55
51
|
}),
|
|
56
52
|
);
|
|
@@ -58,8 +54,7 @@ on(
|
|
|
58
54
|
// ⑤ INTERNAL / call-only
|
|
59
55
|
export const stats = on(
|
|
60
56
|
internal,
|
|
61
|
-
flow({
|
|
62
|
-
name: "triggers.internal",
|
|
57
|
+
flow("triggers.internal", {
|
|
63
58
|
do: async ({ code }, fx) => {
|
|
64
59
|
const [row] = await fx.store(db).select().from(links).where({ code });
|
|
65
60
|
return row?.clicks ?? 0;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.adopt()` barrel generator — real filesystem before/after proofs.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, expect, test } from "bun:test";
|
|
6
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
7
|
+
import { tmpdir } from "node:os";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { generateAdoptBarrel } from "./generate-adopt.ts";
|
|
10
|
+
|
|
11
|
+
async function makeUnit(root: string, name: string, withIndex = true): Promise<void> {
|
|
12
|
+
const dir = join(root, "src/flows", name);
|
|
13
|
+
await mkdir(dir, { recursive: true });
|
|
14
|
+
if (withIndex) await writeFile(join(dir, "index.ts"), "export const nothing = 1;\n");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("generateAdoptBarrel", () => {
|
|
18
|
+
test("emits one `export * as <unit>` per flows folder with an index.ts barrel", async () => {
|
|
19
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
|
|
20
|
+
try {
|
|
21
|
+
await makeUnit(root, "notes");
|
|
22
|
+
await makeUnit(root, "main");
|
|
23
|
+
const result = await generateAdoptBarrel({ rootDir: root });
|
|
24
|
+
expect(result.units).toEqual(["main", "notes"]);
|
|
25
|
+
expect(result.source).toContain('export * as main from "./main/index.ts";');
|
|
26
|
+
expect(result.source).toContain('export * as notes from "./notes/index.ts";');
|
|
27
|
+
expect(result.skipped).toEqual([]);
|
|
28
|
+
} finally {
|
|
29
|
+
await rm(root, { recursive: true, force: true });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("skips a unit folder with no index.ts barrel (never crashes)", async () => {
|
|
34
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
|
|
35
|
+
try {
|
|
36
|
+
await makeUnit(root, "notes");
|
|
37
|
+
await makeUnit(root, "scratch", false);
|
|
38
|
+
const result = await generateAdoptBarrel({ rootDir: root });
|
|
39
|
+
expect(result.units).toEqual(["notes"]);
|
|
40
|
+
expect(result.skipped).toEqual(["scratch"]);
|
|
41
|
+
} finally {
|
|
42
|
+
await rm(root, { recursive: true, force: true });
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("missing flows directory yields an empty barrel, never throws", async () => {
|
|
47
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
|
|
48
|
+
try {
|
|
49
|
+
const result = await generateAdoptBarrel({ rootDir: root });
|
|
50
|
+
expect(result.units).toEqual([]);
|
|
51
|
+
expect(result.source).toContain("AUTO-GENERATED");
|
|
52
|
+
} finally {
|
|
53
|
+
await rm(root, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("real before/after: generated barrel matches the hand-written form it replaces", async () => {
|
|
58
|
+
const root = await mkdtemp(join(tmpdir(), "oke-adopt-gen-"));
|
|
59
|
+
try {
|
|
60
|
+
await mkdir(join(root, "src/flows/main"), { recursive: true });
|
|
61
|
+
await mkdir(join(root, "src/flows/notes"), { recursive: true });
|
|
62
|
+
await writeFile(join(root, "src/flows/main/index.ts"), 'export const root = "main-flow";\n');
|
|
63
|
+
await writeFile(
|
|
64
|
+
join(root, "src/flows/notes/index.ts"),
|
|
65
|
+
'export const create = "notes-flow";\n',
|
|
66
|
+
);
|
|
67
|
+
const result = await generateAdoptBarrel({ rootDir: root });
|
|
68
|
+
const generatedPath = join(root, "src/flows/generated.ts");
|
|
69
|
+
await writeFile(generatedPath, result.source);
|
|
70
|
+
|
|
71
|
+
// Same shape as `import * as main from "./flows/main"; import * as
|
|
72
|
+
// notes from "./flows/notes"; .adopt({ main, notes })` — a namespace
|
|
73
|
+
// object keyed by unit, one key per flows folder, real modules loaded.
|
|
74
|
+
const generatedModule = (await import(generatedPath)) as {
|
|
75
|
+
main: { root: string };
|
|
76
|
+
notes: { create: string };
|
|
77
|
+
};
|
|
78
|
+
expect(Object.keys(generatedModule).sort()).toEqual(["main", "notes"]);
|
|
79
|
+
expect(generatedModule.main.root).toBe("main-flow");
|
|
80
|
+
expect(generatedModule.notes.create).toBe("notes-flow");
|
|
81
|
+
} finally {
|
|
82
|
+
await rm(root, { recursive: true, force: true });
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `.adopt()` route-barrel generator — the disk-file alternative to a
|
|
3
|
+
* virtual-module Bun plugin (investigated and rejected: `Bun.plugin()`'s
|
|
4
|
+
* `onResolve` is never invoked for an unresolvable specifier during a plain
|
|
5
|
+
* runtime `import()`, so `oke dev` and `oke build` would resolve a virtual
|
|
6
|
+
* module differently — measured, not assumed).
|
|
7
|
+
*
|
|
8
|
+
* A real file on disk is an ordinary import: identical resolution under
|
|
9
|
+
* `oke dev`'s runtime `import()` and `oke build`'s `Bun.build()`. Convention
|
|
10
|
+
* (validated against every current example app — both `create-oke`
|
|
11
|
+
* templates and the `skyport` compiler fixture, 7/7): every unit under
|
|
12
|
+
* `<rootDir>/src/flows/*` has an `index.ts` barrel.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { readdir } from "node:fs/promises";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
|
|
18
|
+
/** Options for {@link generateAdoptBarrel}. */
|
|
19
|
+
export interface GenerateAdoptBarrelOptions {
|
|
20
|
+
/** Project root (contains `src/flows`). */
|
|
21
|
+
readonly rootDir: string;
|
|
22
|
+
/** Flows directory relative to {@link rootDir} (default `"src/flows"`). */
|
|
23
|
+
readonly flowsDir?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Generated barrel source plus the unit names it covers. */
|
|
27
|
+
export interface AdoptBarrelResult {
|
|
28
|
+
/** Full `.ts` source — write verbatim to the generated file. */
|
|
29
|
+
readonly source: string;
|
|
30
|
+
/** Unit (folder) names included, sorted. */
|
|
31
|
+
readonly units: readonly string[];
|
|
32
|
+
/** Folders under `flowsDir` skipped for lacking an `index.ts` barrel. */
|
|
33
|
+
readonly skipped: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const GENERATED_HEADER =
|
|
37
|
+
"// AUTO-GENERATED by `oke dev` / `oke build` — do not edit by hand.\n" +
|
|
38
|
+
"// Regenerated from every `src/flows/<unit>/index.ts` unit folder.\n";
|
|
39
|
+
|
|
40
|
+
/** A folder name usable as an `export * as <name>` binding. */
|
|
41
|
+
const VALID_UNIT_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scan `<rootDir>/<flowsDir>/*` and emit a barrel re-exporting every unit
|
|
45
|
+
* folder's `index.ts` under its folder name — the same shape
|
|
46
|
+
* `.adopt({ main, notes })` already takes by hand.
|
|
47
|
+
*
|
|
48
|
+
* Never throws: a missing/unreadable flows directory yields an empty
|
|
49
|
+
* barrel, same posture as the rest of the lazy AoT tooling (best-effort,
|
|
50
|
+
* boot-time checks are what enforce staleness — see
|
|
51
|
+
* `assertAdoptBarrelFresh` in `kernel/boot.ts`).
|
|
52
|
+
*
|
|
53
|
+
* @param options - Root dir / flows dir override
|
|
54
|
+
*/
|
|
55
|
+
export async function generateAdoptBarrel(
|
|
56
|
+
options: GenerateAdoptBarrelOptions,
|
|
57
|
+
): Promise<AdoptBarrelResult> {
|
|
58
|
+
const flowsDir = options.flowsDir ?? "src/flows";
|
|
59
|
+
const abs = join(options.rootDir, flowsDir);
|
|
60
|
+
|
|
61
|
+
const units: string[] = [];
|
|
62
|
+
const skipped: string[] = [];
|
|
63
|
+
try {
|
|
64
|
+
const dirents = await readdir(abs, { withFileTypes: true });
|
|
65
|
+
for (const d of dirents.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
66
|
+
if (!d.isDirectory()) continue;
|
|
67
|
+
const hasBarrel = await Bun.file(join(abs, d.name, "index.ts")).exists();
|
|
68
|
+
if (!hasBarrel) {
|
|
69
|
+
skipped.push(d.name);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (!VALID_UNIT_NAME.test(d.name)) {
|
|
73
|
+
skipped.push(d.name);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
units.push(d.name);
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
// Missing flows dir — empty barrel, not a throw (mirrors extractManifest).
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const lines = units.map((u) => `export * as ${u} from "./${u}/index.ts";`);
|
|
83
|
+
const source = lines.length > 0 ? `${GENERATED_HEADER}${lines.join("\n")}\n` : GENERATED_HEADER;
|
|
84
|
+
return { source, units, skipped };
|
|
85
|
+
}
|
|
@@ -95,7 +95,7 @@ describe("console dry-run / preview audit", () => {
|
|
|
95
95
|
|
|
96
96
|
test("Channels preview is locale template rendering, not dry-run mutation", async () => {
|
|
97
97
|
const flows = await Bun.file(`${import.meta.dir}/flows.ts`).text();
|
|
98
|
-
const previewIdx = flows.indexOf('
|
|
98
|
+
const previewIdx = flows.indexOf('flow("console.channel.preview"');
|
|
99
99
|
expect(previewIdx).toBeGreaterThan(0);
|
|
100
100
|
const slice = flows.slice(previewIdx, previewIdx + 800);
|
|
101
101
|
expect(slice).not.toContain("withDryRun");
|
|
@@ -104,7 +104,7 @@ describe("console dry-run / preview audit", () => {
|
|
|
104
104
|
|
|
105
105
|
test("Traces dryRun is metadata / logging, not withDryRun execution", async () => {
|
|
106
106
|
const flows = await Bun.file(`${import.meta.dir}/flows.ts`).text();
|
|
107
|
-
const idx = flows.indexOf('
|
|
107
|
+
const idx = flows.indexOf('flow("console.traces.replay"');
|
|
108
108
|
expect(idx).toBeGreaterThan(0);
|
|
109
109
|
const slice = flows.slice(idx, idx + 500);
|
|
110
110
|
expect(slice).not.toContain("withDryRun");
|