okengine 0.10.1 → 0.10.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/README.md +1 -1
- package/package.json +1 -1
- package/site/content/docs/ai/mcp.mdx +0 -1
- package/site/content/docs/elements/ai.mdx +0 -6
- package/site/content/docs/elements/channel.mdx +0 -6
- package/site/content/docs/elements/clock.mdx +2 -8
- package/site/content/docs/elements/flow.mdx +10 -17
- package/site/content/docs/elements/gate.mdx +4 -10
- package/site/content/docs/elements/signal.mdx +4 -13
- package/site/content/docs/elements/store.mdx +9 -16
- package/site/content/docs/elements/vault.mdx +1 -7
- package/site/content/docs/get-started/basic-usage.mdx +1 -9
- package/site/content/docs/get-started/introduction.mdx +1 -1
- package/site/content/docs/get-started/why.mdx +1 -2
- package/site/content/docs/index.mdx +0 -1
- package/site/content/docs/meta.json +0 -1
- package/site/content/docs/reference/cli.md +12 -12
- package/site/content/docs/reference/fx.mdx +6 -7
- 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/ai-setup.test.ts +3 -1
- package/src/cli/ai-setup/apply.ts +13 -0
- package/src/cli/build.test.ts +67 -0
- package/src/cli/build.ts +33 -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 +90 -5
- package/src/cli/dev.ts +39 -16
- package/src/cli/doctor.ts +98 -5
- package/src/cli/load-config.images.test.ts +31 -1
- package/src/cli/load-config.ts +2 -2
- package/src/cli/safe-defaults.test.ts +1 -1
- 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 +15 -3
- package/src/compiler/fixtures/raw/raw-unannotated.ts +2 -4
- package/src/compiler/fixtures/skyport/oke.config.ts +4 -2
- 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 +111 -0
- package/src/config/driver-defaults.test.ts +122 -0
- package/src/config/driver-defaults.ts +186 -0
- package/src/config/index.ts +97 -7
- 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/images-config.test.ts +168 -0
- 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/channel.ts +12 -4
- package/src/kernel/boot-bind/clock.ts +3 -3
- package/src/kernel/boot-bind/gate.ts +7 -4
- package/src/kernel/boot-bind/honor-config.test.ts +5 -5
- package/src/kernel/boot-bind/journal.ts +3 -3
- package/src/kernel/boot-bind/signal.ts +3 -1
- package/src/kernel/boot-bind/store.test.ts +19 -0
- package/src/kernel/boot-bind/store.ts +24 -9
- package/src/kernel/boot-bind/vault.ts +3 -3
- 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
- package/site/content/docs/console/access.mdx +0 -29
- package/site/content/docs/console/ai.mdx +0 -35
- package/site/content/docs/console/architecture.mdx +0 -35
- package/site/content/docs/console/channels.mdx +0 -37
- package/site/content/docs/console/clock.mdx +0 -51
- package/site/content/docs/console/flows.mdx +0 -31
- package/site/content/docs/console/gates.mdx +0 -73
- package/site/content/docs/console/index.mdx +0 -54
- package/site/content/docs/console/manifest-diff.mdx +0 -34
- package/site/content/docs/console/meta.json +0 -24
- package/site/content/docs/console/overview.mdx +0 -40
- package/site/content/docs/console/plugins.mdx +0 -41
- package/site/content/docs/console/privacy.mdx +0 -32
- package/site/content/docs/console/runs.mdx +0 -50
- package/site/content/docs/console/signals.mdx +0 -40
- package/site/content/docs/console/store.mdx +0 -32
- package/site/content/docs/console/tenancy.mdx +0 -32
- package/site/content/docs/console/traces.mdx +0 -38
- package/site/content/docs/console/vault.mdx +0 -41
package/src/compiler/extract.ts
CHANGED
|
@@ -1022,8 +1022,20 @@ function collectConfig(opts: AstNode | undefined, scope: ProjectScope): void {
|
|
|
1022
1022
|
const images = objectProp(opts, "images");
|
|
1023
1023
|
if (images) {
|
|
1024
1024
|
const map: Record<string, string> = {};
|
|
1025
|
+
// Nested like `drivers`: `store.*` / `channel.*` flatten to dotted keys;
|
|
1026
|
+
// everything else (`vault`, `ai`, `pgdog`, `proxy`) is already flat.
|
|
1027
|
+
for (const nestKey of ["store", "channel"] as const) {
|
|
1028
|
+
const nested = objectProp(images, nestKey);
|
|
1029
|
+
if (!nested || nested.type !== "ObjectExpression") continue;
|
|
1030
|
+
for (const prop of objectProperties(nested)) {
|
|
1031
|
+
const key = propKey(prop);
|
|
1032
|
+
const val = stringArg((prop as AstNode & { value?: AstNode }).value);
|
|
1033
|
+
if (key && val) map[`${nestKey}.${key}`] = val;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1025
1036
|
for (const prop of objectProperties(images)) {
|
|
1026
1037
|
const key = propKey(prop);
|
|
1038
|
+
if (key === "store" || key === "channel") continue;
|
|
1027
1039
|
const val = stringArg((prop as AstNode & { value?: AstNode }).value);
|
|
1028
1040
|
if (key && val) map[key] = val;
|
|
1029
1041
|
}
|
|
@@ -1159,11 +1171,11 @@ function registerFlow(args: {
|
|
|
1159
1171
|
scope: ProjectScope;
|
|
1160
1172
|
exportName: string | undefined;
|
|
1161
1173
|
}): void {
|
|
1162
|
-
const opts = objectArg(args.flowCall.arguments[
|
|
1174
|
+
const opts = objectArg(args.flowCall.arguments[1]);
|
|
1163
1175
|
if (!opts) return;
|
|
1164
1176
|
|
|
1165
1177
|
const name =
|
|
1166
|
-
|
|
1178
|
+
stringArg(args.flowCall.arguments[0]) ??
|
|
1167
1179
|
args.exportName ??
|
|
1168
1180
|
`flow_${Object.keys(args.scope.flows).length + 1}`;
|
|
1169
1181
|
|
|
@@ -1232,7 +1244,7 @@ function registerFlow(args: {
|
|
|
1232
1244
|
const errors = parseErrors(objectProp(opts, "errors"));
|
|
1233
1245
|
if (errors) flow.errors = errors;
|
|
1234
1246
|
|
|
1235
|
-
if (effects
|
|
1247
|
+
if (effects) flow.effects = effects;
|
|
1236
1248
|
|
|
1237
1249
|
const line = lineAt(args.file.source, args.flowCall.start ?? 0);
|
|
1238
1250
|
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,10 @@ export default defineConfig({
|
|
|
15
15
|
ai: { prod: { driver: "anthropic" } },
|
|
16
16
|
},
|
|
17
17
|
images: {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
store: {
|
|
19
|
+
sql: "pgvector/pgvector:pg17",
|
|
20
|
+
kv: "valkey/valkey:8-alpine",
|
|
21
|
+
},
|
|
20
22
|
},
|
|
21
23
|
i18n: { locales: ["en", "ar"], default: "ar", dir: { ar: "rtl" } },
|
|
22
24
|
tenancy: { isolation: "row" },
|
|
@@ -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,111 @@
|
|
|
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 { mkdir, readdir, rename, writeFile } 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
|
+
/** Filename of the on-disk `.adopt()` barrel under the flows directory. */
|
|
41
|
+
export const ADOPT_BARREL_FILE = "generated.ts";
|
|
42
|
+
|
|
43
|
+
/** A folder name usable as an `export * as <name>` binding. */
|
|
44
|
+
const VALID_UNIT_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Write the adopt barrel atomically: `generated.ts.tmp` → rename to
|
|
48
|
+
* `generated.ts`. Concurrent readers (e.g. `bun --hot` racing a save) never
|
|
49
|
+
* observe a torn/partial file — plain `writeFile` on the destination would
|
|
50
|
+
* truncate first.
|
|
51
|
+
*
|
|
52
|
+
* @param rootDir - Project root
|
|
53
|
+
* @param source - Full barrel source from {@link generateAdoptBarrel}
|
|
54
|
+
* @param flowsDir - Flows directory relative to root (default `"src/flows"`)
|
|
55
|
+
*/
|
|
56
|
+
export async function writeAdoptBarrel(
|
|
57
|
+
rootDir: string,
|
|
58
|
+
source: string,
|
|
59
|
+
flowsDir = "src/flows",
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const dir = join(rootDir, flowsDir);
|
|
62
|
+
const target = join(dir, ADOPT_BARREL_FILE);
|
|
63
|
+
const tmp = `${target}.tmp`;
|
|
64
|
+
await mkdir(dir, { recursive: true });
|
|
65
|
+
await writeFile(tmp, source);
|
|
66
|
+
await rename(tmp, target);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Scan `<rootDir>/<flowsDir>/*` and emit a barrel re-exporting every unit
|
|
71
|
+
* folder's `index.ts` under its folder name — the same shape
|
|
72
|
+
* `.adopt({ main, notes })` already takes by hand.
|
|
73
|
+
*
|
|
74
|
+
* Never throws: a missing/unreadable flows directory yields an empty
|
|
75
|
+
* barrel, same posture as the rest of the lazy AoT tooling (best-effort,
|
|
76
|
+
* boot-time checks are what enforce staleness — see
|
|
77
|
+
* `assertAdoptBarrelFresh` in `kernel/boot.ts`).
|
|
78
|
+
*
|
|
79
|
+
* @param options - Root dir / flows dir override
|
|
80
|
+
*/
|
|
81
|
+
export async function generateAdoptBarrel(
|
|
82
|
+
options: GenerateAdoptBarrelOptions,
|
|
83
|
+
): Promise<AdoptBarrelResult> {
|
|
84
|
+
const flowsDir = options.flowsDir ?? "src/flows";
|
|
85
|
+
const abs = join(options.rootDir, flowsDir);
|
|
86
|
+
|
|
87
|
+
const units: string[] = [];
|
|
88
|
+
const skipped: string[] = [];
|
|
89
|
+
try {
|
|
90
|
+
const dirents = await readdir(abs, { withFileTypes: true });
|
|
91
|
+
for (const d of dirents.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
92
|
+
if (!d.isDirectory()) continue;
|
|
93
|
+
const hasBarrel = await Bun.file(join(abs, d.name, "index.ts")).exists();
|
|
94
|
+
if (!hasBarrel) {
|
|
95
|
+
skipped.push(d.name);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!VALID_UNIT_NAME.test(d.name)) {
|
|
99
|
+
skipped.push(d.name);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
units.push(d.name);
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Missing flows dir — empty barrel, not a throw (mirrors extractManifest).
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const lines = units.map((u) => `export * as ${u} from "./${u}/index.ts";`);
|
|
109
|
+
const source = lines.length > 0 ? `${GENERATED_HEADER}${lines.join("\n")}\n` : GENERATED_HEADER;
|
|
110
|
+
return { source, units, skipped };
|
|
111
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Driver-map defaults — per-key merge onto the real established defaults,
|
|
3
|
+
* never a whole-object replace and never leaking across driver maps.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, expect, test } from "bun:test";
|
|
7
|
+
import { mergeEnvDriverMap } from "./index.ts";
|
|
8
|
+
import {
|
|
9
|
+
CHANNEL_EMAIL_DEFAULTS,
|
|
10
|
+
CHANNEL_SMS_DEFAULTS,
|
|
11
|
+
CLOCK_DEFAULTS,
|
|
12
|
+
JOURNAL_DEFAULTS,
|
|
13
|
+
resolveEffectiveDrivers,
|
|
14
|
+
SIGNAL_DEFAULTS,
|
|
15
|
+
STORE_FILES_DEFAULTS,
|
|
16
|
+
STORE_KV_DEFAULTS,
|
|
17
|
+
STORE_SQL_DEFAULTS,
|
|
18
|
+
VAULT_DEFAULTS,
|
|
19
|
+
} from "./driver-defaults.ts";
|
|
20
|
+
|
|
21
|
+
describe("mergeEnvDriverMap", () => {
|
|
22
|
+
test("an unset key keeps the real default, not a sibling key's value", () => {
|
|
23
|
+
const merged = mergeEnvDriverMap({ local: "pglite" }, STORE_SQL_DEFAULTS);
|
|
24
|
+
expect(merged).toEqual({
|
|
25
|
+
local: "pglite",
|
|
26
|
+
docker: "postgres",
|
|
27
|
+
test: "memory",
|
|
28
|
+
prod: "postgres",
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("undefined override yields the untouched default map", () => {
|
|
33
|
+
expect(mergeEnvDriverMap(undefined, STORE_KV_DEFAULTS)).toEqual(STORE_KV_DEFAULTS);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a fully-specified override wins on every key", () => {
|
|
37
|
+
const override = { local: "a", docker: "b", test: "c", prod: "d" };
|
|
38
|
+
expect(mergeEnvDriverMap(override, STORE_SQL_DEFAULTS)).toEqual(override);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("resolveEffectiveDrivers — the drivers.store.sql.local discussion scenario", () => {
|
|
43
|
+
test("`{ store: { sql: { local: 'pglite' } } }` resolves sql to the real map, pglite only on local", () => {
|
|
44
|
+
const effective = resolveEffectiveDrivers({
|
|
45
|
+
store: { sql: { local: "pglite" } },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(effective.store.sql).toEqual({
|
|
49
|
+
local: "pglite",
|
|
50
|
+
docker: "postgres",
|
|
51
|
+
test: "memory",
|
|
52
|
+
prod: "postgres",
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("every other driver stays at its full, real, untouched default map", () => {
|
|
57
|
+
const effective = resolveEffectiveDrivers({
|
|
58
|
+
store: { sql: { local: "pglite" } },
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
expect(effective.store.kv).toEqual(STORE_KV_DEFAULTS);
|
|
62
|
+
expect(effective.store.files).toEqual(STORE_FILES_DEFAULTS);
|
|
63
|
+
expect(effective.signal).toEqual(SIGNAL_DEFAULTS);
|
|
64
|
+
expect(effective.clock).toEqual(CLOCK_DEFAULTS);
|
|
65
|
+
expect(effective.journal).toEqual(JOURNAL_DEFAULTS);
|
|
66
|
+
expect(effective.vault).toEqual(VAULT_DEFAULTS);
|
|
67
|
+
expect(effective.channel.email).toEqual(CHANNEL_EMAIL_DEFAULTS);
|
|
68
|
+
expect(effective.channel.sms).toEqual(CHANNEL_SMS_DEFAULTS);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("overriding one driver's map has zero effect on a sibling driver's defaults", () => {
|
|
72
|
+
const effective = resolveEffectiveDrivers({
|
|
73
|
+
store: { kv: { local: "redis" } },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// The overridden driver (kv) reflects the override on its one set key.
|
|
77
|
+
expect(effective.store.kv).toEqual({
|
|
78
|
+
local: "redis",
|
|
79
|
+
docker: "redis",
|
|
80
|
+
test: "memory",
|
|
81
|
+
prod: "redis",
|
|
82
|
+
});
|
|
83
|
+
// sql / files (same element, different facet) stay fully at real defaults.
|
|
84
|
+
expect(effective.store.sql).toEqual(STORE_SQL_DEFAULTS);
|
|
85
|
+
expect(effective.store.files).toEqual(STORE_FILES_DEFAULTS);
|
|
86
|
+
// Unrelated elements stay fully at real defaults too.
|
|
87
|
+
expect(effective.signal).toEqual(SIGNAL_DEFAULTS);
|
|
88
|
+
expect(effective.vault).toEqual(VAULT_DEFAULTS);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("overriding one env key leaves the other three at real defaults for that driver", () => {
|
|
92
|
+
const effective = resolveEffectiveDrivers({
|
|
93
|
+
vault: { local: "keychain" },
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
expect(effective.vault).toEqual({
|
|
97
|
+
local: "keychain",
|
|
98
|
+
docker: "env",
|
|
99
|
+
test: "memory",
|
|
100
|
+
prod: "env",
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("no drivers config at all yields the real, untouched defaults for every map", () => {
|
|
105
|
+
const effective = resolveEffectiveDrivers(undefined);
|
|
106
|
+
expect(effective).toEqual({
|
|
107
|
+
store: {
|
|
108
|
+
sql: STORE_SQL_DEFAULTS,
|
|
109
|
+
kv: STORE_KV_DEFAULTS,
|
|
110
|
+
files: STORE_FILES_DEFAULTS,
|
|
111
|
+
},
|
|
112
|
+
signal: SIGNAL_DEFAULTS,
|
|
113
|
+
clock: CLOCK_DEFAULTS,
|
|
114
|
+
journal: JOURNAL_DEFAULTS,
|
|
115
|
+
vault: VAULT_DEFAULTS,
|
|
116
|
+
channel: {
|
|
117
|
+
email: CHANNEL_EMAIL_DEFAULTS,
|
|
118
|
+
sms: CHANNEL_SMS_DEFAULTS,
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
});
|