@suluk/platform 0.4.2 → 0.5.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/package.json +34 -30
- package/src/generate.ts +6 -0
- package/src/manifest.ts +7 -0
- package/src/plan.ts +70 -10
- package/src/resolve.ts +2 -0
package/package.json
CHANGED
|
@@ -1,35 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suluk/platform",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "The platform generator (C051): write one `definePlatform` manifest → it plans the shadcn-registry adds, generates the wired Hono entry, and merges each module's provision fragment into a single provision.config. The manifest compiles to a shadcn-add list + a C047 provision.config; the generator runs the adds + `@suluk/provision`. Turns the Suluk backend registry into a one-command platform. CANDIDATE tooling.",
|
|
5
5
|
"publishConfig": {
|
|
6
|
-
|
|
7
|
-
},
|
|
8
|
-
"license": "Apache-2.0",
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/MahmoodKhalil57/suluk.git",
|
|
12
|
-
"directory": "tooling/ts/packages/platform"
|
|
13
|
-
},
|
|
14
|
-
"homepage": "https://github.com/MahmoodKhalil57/suluk#readme",
|
|
15
|
-
"bugs": "https://github.com/MahmoodKhalil57/suluk/issues",
|
|
16
|
-
"type": "module",
|
|
17
|
-
"files": [
|
|
18
|
-
"
|
|
19
|
-
"bin"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
}
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"license": "Apache-2.0",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/MahmoodKhalil57/suluk.git",
|
|
12
|
+
"directory": "tooling/ts/packages/platform"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/MahmoodKhalil57/suluk#readme",
|
|
15
|
+
"bugs": "https://github.com/MahmoodKhalil57/suluk/issues",
|
|
16
|
+
"type": "module",
|
|
17
|
+
"files": [
|
|
18
|
+
"src",
|
|
19
|
+
"bin",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"main": "src/index.ts",
|
|
23
|
+
"bin": {
|
|
24
|
+
"suluk-platform": "bin/platform.ts"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./src/index.ts"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@suluk/provision": "^0.1.0"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "latest"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "bun test",
|
|
37
|
+
"typecheck": "tsc --noEmit -p ."
|
|
38
|
+
}
|
|
35
39
|
}
|
package/src/generate.ts
CHANGED
|
@@ -85,6 +85,12 @@ export async function generatePlatform(input: PlatformManifest | Platform, opts:
|
|
|
85
85
|
log("▸ writing provision.config.ts");
|
|
86
86
|
await opts.write("provision.config.ts", plan.provisionConfig);
|
|
87
87
|
written.push("src/index.ts", "provision.config.ts");
|
|
88
|
+
// the bun MOCK-PROVIDER dev server — only when the manifest sets `local: true` (else undefined → not written).
|
|
89
|
+
if (plan.devEntry) {
|
|
90
|
+
log("▸ writing src/dev.ts");
|
|
91
|
+
await opts.write("src/dev.ts", plan.devEntry);
|
|
92
|
+
written.push("src/dev.ts");
|
|
93
|
+
}
|
|
88
94
|
|
|
89
95
|
log(`✓ generated ${name}: ${plan.services.length} services. Next: bun install && suluk-provision apply`);
|
|
90
96
|
return { plan, added, written };
|
package/src/manifest.ts
CHANGED
|
@@ -22,6 +22,10 @@ export interface PlatformManifest {
|
|
|
22
22
|
opts?: Record<string, Record<string, unknown>>;
|
|
23
23
|
/** NON-SECRET config values → generated into `wrangler.toml` `[vars]`. SECRETS never go here (they live in `.env`). */
|
|
24
24
|
vars?: Record<string, string>;
|
|
25
|
+
/** emit the MOCK-PROVIDER dev runtime: a `src/dev.ts` that runs the app under bun with a bun:sqlite DB + JSON-file KV +
|
|
26
|
+
* mocked providers when their keys are absent (mock-until-keyed), and the `dev` script pointed at it. Default false →
|
|
27
|
+
* the scaffold is byte-for-byte the C051 golden. */
|
|
28
|
+
local?: boolean;
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
// ── C053: the open system/brand surface ──────────────────────────────────────────────────────────────────────────────
|
|
@@ -62,6 +66,9 @@ export interface SystemManifest<T extends readonly ServiceRef[] = readonly Servi
|
|
|
62
66
|
serviceOpts?: Partial<{ [K in T[number] as IdOf<K>]: SoOf<K> }>;
|
|
63
67
|
/** inter-service composition edges (Phase 3). */
|
|
64
68
|
wire?: WireDecl[];
|
|
69
|
+
/** emit the MOCK-PROVIDER dev runtime (a `src/dev.ts` bun server with a bun:sqlite DB + JSON KV + mocked providers when
|
|
70
|
+
* keys are absent). A SYSTEM-level property (the app structure), swappable per brand only if a brand overrides it. */
|
|
71
|
+
local?: boolean;
|
|
65
72
|
}
|
|
66
73
|
|
|
67
74
|
/** A BRAND — thin, swappable per deployment. Carries the app identity + the brand-facing opts (→ `[vars]`). */
|
package/src/plan.ts
CHANGED
|
@@ -49,6 +49,9 @@ export interface PlatformPlan {
|
|
|
49
49
|
/** the generated `.env` SCAFFOLD (committed) — a header + the setup steps, NO values. `generate` writes it only if absent
|
|
50
50
|
* (never clobbering the operator's encrypted secrets). Secret VALUES are added encrypted via `suluk-env set`. */
|
|
51
51
|
envScaffold: string;
|
|
52
|
+
/** the generated `src/dev.ts` — the bun MOCK-PROVIDER dev server (bun:sqlite DB + JSON KV + mocked providers when keys
|
|
53
|
+
* absent). Present ONLY when the manifest sets `local: true`; undefined otherwise (so the golden path is unchanged). */
|
|
54
|
+
devEntry?: string;
|
|
52
55
|
}
|
|
53
56
|
|
|
54
57
|
export function planPlatform(input: PlatformManifest | Platform): PlatformPlan {
|
|
@@ -66,18 +69,20 @@ export function planPlatform(input: PlatformManifest | Platform): PlatformPlan {
|
|
|
66
69
|
const env = collectEnv(services, catalog);
|
|
67
70
|
// resolve the wires (a `{system,brand}` platform may carry `wire`; a legacy manifest never does → no wiring → byte-identical).
|
|
68
71
|
const wiring = resolveWiring(services, isPlatform(input) ? input.system.wire ?? [] : [], catalog);
|
|
72
|
+
// the MOCK-PROVIDER dev runtime is OPT-IN (`local: true`); off → every output below is byte-for-byte the C051 golden.
|
|
73
|
+
const local = manifest.local === true;
|
|
69
74
|
return {
|
|
70
75
|
services,
|
|
71
76
|
// a service may override the registry it's pulled from (multi-registry); core services fall back to the system registry.
|
|
72
77
|
adds: services.map((s) => `${catalog[s].registry ?? manifest.registry}/${s}`),
|
|
73
|
-
entry: buildEntry(services, manifest.opts, wiring, catalog),
|
|
78
|
+
entry: buildEntry(services, manifest.opts, wiring, catalog, local),
|
|
74
79
|
provisionConfig: buildProvisionConfig(services, catalog),
|
|
75
|
-
packageJson: buildPackageJson(manifest.name, services, catalog),
|
|
80
|
+
packageJson: buildPackageJson(manifest.name, services, catalog, local),
|
|
76
81
|
tsconfig: buildTsconfig(),
|
|
77
82
|
componentsJson: buildComponentsJson(),
|
|
78
83
|
envExample: buildEnvExample(env),
|
|
79
84
|
wranglerToml: buildWranglerToml(manifest.name, services, env, manifest.vars ?? {}),
|
|
80
|
-
gitignore: buildGitignore(),
|
|
85
|
+
gitignore: buildGitignore(local),
|
|
81
86
|
envCheck: buildEnvCheckScript(env),
|
|
82
87
|
envTs: buildEnvTs(env),
|
|
83
88
|
syncSecrets: buildSyncSecrets(),
|
|
@@ -86,6 +91,7 @@ export function planPlatform(input: PlatformManifest | Platform): PlatformPlan {
|
|
|
86
91
|
provisionScript: buildProvisionScript(env),
|
|
87
92
|
mintTokens: buildMintTokens(env),
|
|
88
93
|
envScaffold: buildEnvScaffold(env),
|
|
94
|
+
...(local ? { devEntry: buildDevEntry(services) } : {}),
|
|
89
95
|
};
|
|
90
96
|
}
|
|
91
97
|
|
|
@@ -445,10 +451,11 @@ export function mergeWranglerToml(generated: string, existing: string | null): s
|
|
|
445
451
|
.join("\n");
|
|
446
452
|
}
|
|
447
453
|
|
|
448
|
-
function buildGitignore(): string {
|
|
454
|
+
function buildGitignore(local = false): string {
|
|
449
455
|
// NOTE: `.env` is NOT ignored — it is COMMITTED with its secret values ENCRYPTED (@suluk/env). The PRIVATE key
|
|
450
456
|
// (`.env.keys`) is what must never be committed; that + `.env.temp`/`.dev.vars` are ignored.
|
|
451
|
-
|
|
457
|
+
// local mode also ignores `.suluk/` (the bun-dev sqlite DB + JSON KV — local dev data, never committed).
|
|
458
|
+
return ["node_modules/", ".env.keys", ".env.temp", ".dev.vars", ".wrangler/", "dist/", ...(local ? [".suluk/"] : []), ""].join("\n");
|
|
452
459
|
}
|
|
453
460
|
|
|
454
461
|
/** Merge the generated .gitignore into an existing one — APPEND any missing entries (never skip-if-present, so an app's
|
|
@@ -514,7 +521,7 @@ process.exit(0);
|
|
|
514
521
|
|
|
515
522
|
/** The framework baseline package.json — name from the manifest, the union of BASE + each service's deps (versions
|
|
516
523
|
* resolved: @suluk/* → "latest", ecosystem → pinned), + the toolchain devDeps + the regenerate/typecheck scripts. */
|
|
517
|
-
export function buildPackageJson(name: string, services: string[], catalog: Record<string, Service> = CORE_SERVICES): string {
|
|
524
|
+
export function buildPackageJson(name: string, services: string[], catalog: Record<string, Service> = CORE_SERVICES, local = false): string {
|
|
518
525
|
const deps = new Set<string>(BASE_DEPS);
|
|
519
526
|
for (const s of services) for (const d of catalog[s]?.deps ?? []) deps.add(d);
|
|
520
527
|
const dependencies: Record<string, string> = {};
|
|
@@ -526,8 +533,10 @@ export function buildPackageJson(name: string, services: string[], catalog: Reco
|
|
|
526
533
|
scripts: {
|
|
527
534
|
generate: "suluk-platform", // re-pull modules + rewrite the scaffold config + src/index.ts + provision.config.ts
|
|
528
535
|
check: "bun run scripts/env-check.ts", // the encrypted-env preflight (keypair present? required secrets set + encrypted?)
|
|
529
|
-
|
|
530
|
-
|
|
536
|
+
// local mode: `dev` runs the bun mock-provider server (no keys needed → no env-check predev); `dev:cf` keeps wrangler.
|
|
537
|
+
...(local ? {} : { predev: "bun run scripts/env-check.ts" }), // (non-local) runs automatically before `dev`
|
|
538
|
+
dev: local ? "bun run --hot src/dev.ts" : "wrangler dev",
|
|
539
|
+
...(local ? { "dev:cf": "wrangler dev" } : {}),
|
|
531
540
|
deploy: "wrangler deploy",
|
|
532
541
|
"env:keygen": "suluk-env keygen", // create the @suluk/env keypair (SULUK_PUBLIC_KEY → .env; private → .env.keys)
|
|
533
542
|
"link-key": "bun run scripts/link-key.ts", // register the private key in ~/.suluk/settings.json (the central store)
|
|
@@ -608,7 +617,7 @@ function buildComponentsJson(): string {
|
|
|
608
617
|
);
|
|
609
618
|
}
|
|
610
619
|
|
|
611
|
-
function buildEntry(services: string[], opts?: Record<string, Record<string, unknown>>, wiring?: Wiring, catalog: Record<string, Service> = CORE_SERVICES): string {
|
|
620
|
+
function buildEntry(services: string[], opts?: Record<string, Record<string, unknown>>, wiring?: Wiring, catalog: Record<string, Service> = CORE_SERVICES, local = false): string {
|
|
612
621
|
const imports = [
|
|
613
622
|
'import { createApp } from "./app";',
|
|
614
623
|
'import { loadEnv } from "@suluk/env";',
|
|
@@ -657,7 +666,10 @@ function buildEntry(services: string[], opts?: Record<string, Record<string, unk
|
|
|
657
666
|
return false; // same symbol + module already imported → dedup
|
|
658
667
|
});
|
|
659
668
|
for (const line of groupImports(safeWireImports)) imports.push(line);
|
|
660
|
-
|
|
669
|
+
// local mode also EXPORTS the wired app so `src/dev.ts` can serve it under bun with mock bindings (the Worker `fetch`
|
|
670
|
+
// export is unchanged). Off → `const app` (byte-identical to the golden). The mock modules are imported ONLY by dev.ts,
|
|
671
|
+
// so `wrangler deploy` (bundling from `src/index.ts`) never pulls bun:sqlite into the Worker.
|
|
672
|
+
const body = [`${local ? "export const app" : "const app"} = createApp();`, ...middleware, ...routes];
|
|
661
673
|
// the @suluk/env bootstrap: the committed .env holds the app's secrets ENCRYPTED. If SULUK_PRIVATE_KEY is set (a wrangler
|
|
662
674
|
// secret), decrypt them into the request env on first use (the runtime path); otherwise this is a no-op and the secrets come
|
|
663
675
|
// from `wrangler secret put` (the `bun run sync-secrets` deploy path). Decrypt once per isolate (env is stable across requests).
|
|
@@ -674,6 +686,54 @@ function buildEntry(services: string[], opts?: Record<string, Record<string, unk
|
|
|
674
686
|
return `// AUTO-GENERATED by @suluk/platform from platform.config.ts — the wired Hono entry. Edit freely.\n${imports.join("\n")}\n\n${body.join("\n")}\n\n${bootstrap.join("\n")}\n`;
|
|
675
687
|
}
|
|
676
688
|
|
|
689
|
+
/**
|
|
690
|
+
* `src/dev.ts` — the bun MOCK-PROVIDER dev server (emitted only when `local: true`). Runs the SAME wired app (imported from
|
|
691
|
+
* `src/index.ts`) under bun with a bun:sqlite `DB` facade + a JSON-file KV, so `bun run dev` needs no Cloudflare account and
|
|
692
|
+
* no wrangler. Mock-until-keyed: it decrypts the committed `.env` if the app has been provisioned (real HTTP providers), else
|
|
693
|
+
* every provider falls to its module's mock. The deployed Worker (`src/index.ts`) imports NONE of this — bun:sqlite stays out.
|
|
694
|
+
*/
|
|
695
|
+
function buildDevEntry(services: string[]): string {
|
|
696
|
+
const usesKv = services.includes("rate-credit");
|
|
697
|
+
const kvImport = usesKv ? "jsonFileKvStore, " : "";
|
|
698
|
+
const kvBind = usesKv ? "\n RATE_CREDIT_KV: jsonFileKvStore(KV_PATH)," : "";
|
|
699
|
+
return `// AUTO-GENERATED by @suluk/platform — the bun MOCK-PROVIDER dev server. Runs the wired app under bun with a
|
|
700
|
+
// bun:sqlite DB + JSON-file KV, so \`bun run dev\` works with ZERO Cloudflare account and no wrangler. A provider goes REAL
|
|
701
|
+
// the moment its key is present (mock-until-keyed): add real keys to .env.temp + \`bun run provision\` and this file uses
|
|
702
|
+
// them. NOTE: src/index.ts (the deployed Worker) imports NONE of these mocks — bun:sqlite never enters the Worker bundle.
|
|
703
|
+
import { app } from "./index";
|
|
704
|
+
import { Database } from "bun:sqlite";
|
|
705
|
+
import { d1FromSqlite, ${kvImport}applyLocalSchema } from "@suluk/cloudflare/local";
|
|
706
|
+
import { loadEnvFile } from "@suluk/env/node";
|
|
707
|
+
|
|
708
|
+
const DB_PATH = process.env.SULUK_DB_PATH ?? ".suluk/dev.sqlite";${usesKv ? '\nconst KV_PATH = process.env.SULUK_KV_PATH ?? ".suluk/dev-kv.json";' : ""}
|
|
709
|
+
const PORT = Number(process.env.PORT ?? 8787);
|
|
710
|
+
|
|
711
|
+
const sqlite = new Database(DB_PATH, { create: true });
|
|
712
|
+
const tables = await applyLocalSchema(sqlite); // discover src/db/*.ts + create the tables from the drizzle schema
|
|
713
|
+
console.log(\`[suluk dev] sqlite \${DB_PATH} — \${tables.length} tables\`);
|
|
714
|
+
|
|
715
|
+
// Real secrets (if this app has been provisioned): decrypt the committed .env with the local private key. Fresh app / no
|
|
716
|
+
// key → {} → every provider mocks. Best-effort: a decryption failure never blocks the mock path.
|
|
717
|
+
let secrets: Record<string, string> = {};
|
|
718
|
+
try { secrets = await loadEnvFile(); } catch {}
|
|
719
|
+
|
|
720
|
+
// The request env: process.env < decrypted secrets < the mock bindings. DB/KV are always local (a bun process can't bind a
|
|
721
|
+
// remote D1/KV); the HTTP providers (Google/Stripe/Resend) use their real key when present, else their module's mock.
|
|
722
|
+
const env: Record<string, unknown> = {
|
|
723
|
+
...process.env,
|
|
724
|
+
...secrets,
|
|
725
|
+
DB: d1FromSqlite(sqlite),${kvBind}
|
|
726
|
+
};
|
|
727
|
+
|
|
728
|
+
const mocked = ["GOOGLE_CLIENT_ID", "STRIPE_SECRET_KEY", "RESEND_API_KEY"].filter((k) => !env[k]);
|
|
729
|
+
if (mocked.length) console.log(\`[suluk dev] mocked (no key): \${mocked.join(", ")}\`);
|
|
730
|
+
|
|
731
|
+
const ctx = { waitUntil() {}, passThroughOnException() {} } as unknown as ExecutionContext;
|
|
732
|
+
Bun.serve({ port: PORT, idleTimeout: 120, fetch: (req) => app.fetch(req, env as Parameters<typeof app.fetch>[1], ctx) });
|
|
733
|
+
console.log(\`[suluk dev] → http://localhost:\${PORT} (mock-until-keyed; provision to go live)\`);
|
|
734
|
+
`;
|
|
735
|
+
}
|
|
736
|
+
|
|
677
737
|
function buildProvisionConfig(services: string[], catalog: Record<string, Service> = CORE_SERVICES): string {
|
|
678
738
|
const frags = services.map((s) => catalog[s].provision).filter((p): p is NonNullable<typeof p> => !!p);
|
|
679
739
|
const imports = frags.map((f) => `import { ${f.symbol} } from "${f.from}";`);
|
package/src/resolve.ts
CHANGED
|
@@ -87,6 +87,7 @@ export function liftLegacy(m: PlatformManifest): Platform {
|
|
|
87
87
|
services: m.services,
|
|
88
88
|
...(Object.keys(globalServiceOpts).length ? { globalServiceOpts } : {}),
|
|
89
89
|
...(m.opts && Object.keys(m.opts).length ? { serviceOpts: m.opts } : {}),
|
|
90
|
+
...(m.local ? { local: true } : {}),
|
|
90
91
|
},
|
|
91
92
|
brand: {
|
|
92
93
|
name: m.name,
|
|
@@ -107,5 +108,6 @@ export function liftSystemBrand(p: Platform): PlatformManifest {
|
|
|
107
108
|
services,
|
|
108
109
|
...(Object.keys(opts).length ? { opts } : {}),
|
|
109
110
|
...(Object.keys(vars).length ? { vars } : {}),
|
|
111
|
+
...(p.system.local ? { local: true } : {}),
|
|
110
112
|
};
|
|
111
113
|
}
|