@suluk/platform 0.1.2 → 0.1.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@suluk/platform",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
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
6
  "access": "public"
package/src/catalog.ts CHANGED
@@ -21,13 +21,19 @@ export interface CatalogEntry {
21
21
  export const CATALOG: Record<string, CatalogEntry> = {
22
22
  app: { mount: { kind: "base" } },
23
23
  auth: { mount: { kind: "middleware", symbol: "mountAuthRoutes", from: "./auth" }, provision: { symbol: "authProvision", from: "./src/provision/auth" } },
24
- credits: { mount: { kind: "route", path: "/credits", symbol: "creditsRoutes", from: "./routes/credits" }, provision: { symbol: "creditsProvision", from: "./src/provision/credits" } },
25
- keys: { mount: { kind: "route", path: "/keys", symbol: "keysRoutes", from: "./routes/keys" }, provision: { symbol: "keysProvision", from: "./src/provision/keys" } },
26
- billing: { mount: { kind: "route", path: "/billing", symbol: "billingRoutes", from: "./routes/billing" }, provision: { symbol: "billingProvision", from: "./src/provision/billing" } },
27
- cost: { mount: { kind: "route", path: "/cost", symbol: "costRoutes", from: "./routes/cost" }, provision: { symbol: "costProvision", from: "./src/provision/cost" } },
28
- erasure: { mount: { kind: "route", path: "/erasure", symbol: "erasureRoutes", from: "./routes/erasure" }, provision: { symbol: "erasureProvision", from: "./src/provision/erasure" } },
29
- email: { mount: { kind: "route", path: "/email", symbol: "emailRoutes", from: "./routes/email" } }, // stateless binding no provision fragment (C052)
30
- logs: { mount: { kind: "route", path: "/logs", symbol: "logsRoutes", from: "./routes/logs" }, provision: { symbol: "logsProvision", from: "./src/provision/logs" } },
24
+ // feature routes mount under /api/* where the caller-resolution + cors + rate-limit middleware live (toolfactory parity).
25
+ contract: { mount: { kind: "route", path: "/api", symbol: "contractRoutes", from: "./routes/contract" } }, // serves GET /api/openapi.json (derived); stateless
26
+ credits: { mount: { kind: "route", path: "/api/credits", symbol: "creditsRoutes", from: "./routes/credits" }, provision: { symbol: "creditsProvision", from: "./src/provision/credits" } },
27
+ keys: { mount: { kind: "route", path: "/api/keys", symbol: "keysRoutes", from: "./routes/keys" }, provision: { symbol: "keysProvision", from: "./src/provision/keys" } },
28
+ billing: { mount: { kind: "route", path: "/api/billing", symbol: "billingRoutes", from: "./routes/billing" }, provision: { symbol: "billingProvision", from: "./src/provision/billing" } },
29
+ cost: { mount: { kind: "route", path: "/api/cost", symbol: "costRoutes", from: "./routes/cost" }, provision: { symbol: "costProvision", from: "./src/provision/cost" } },
30
+ erasure: { mount: { kind: "route", path: "/api/erasure", symbol: "erasureRoutes", from: "./routes/erasure" }, provision: { symbol: "erasureProvision", from: "./src/provision/erasure" } },
31
+ email: { mount: { kind: "route", path: "/api/email", symbol: "emailRoutes", from: "./routes/email" } }, // stateless binding — no provision fragment (C052)
32
+ webhooks: { mount: { kind: "route", path: "/api/webhooks", symbol: "webhooksRoutes", from: "./routes/webhooks" }, provision: { symbol: "webhooksProvision", from: "./src/provision/webhooks" } },
33
+ // cross-cutting MIDDLEWARE (apply globally via app.use, emitted before any route) — not routed resources.
34
+ "rate-limit": { mount: { kind: "middleware", symbol: "mountRateLimit", from: "./services/rate-limit" } },
35
+ i18n: { mount: { kind: "middleware", symbol: "mountI18n", from: "./services/i18n" } },
36
+ logs: { mount: { kind: "route", path: "/api/logs", symbol: "logsRoutes", from: "./routes/logs" }, provision: { symbol: "logsProvision", from: "./src/provision/logs" } },
31
37
  // dev/CI tooling — pulled in as files, no runtime mount, no provision fragment.
32
38
  journeys: { mount: { kind: "dev" } },
33
39
  audit: { mount: { kind: "dev" } },
package/src/plan.ts CHANGED
@@ -30,17 +30,21 @@ export function planPlatform(manifest: PlatformManifest): PlatformPlan {
30
30
 
31
31
  function buildEntry(services: string[]): string {
32
32
  const imports = ['import { createApp } from "./app";'];
33
- const body: string[] = ["const app = createApp();"];
33
+ const middleware: string[] = [];
34
+ const routes: string[] = [];
35
+ // TWO passes: ALL middleware mounts (app.use / handler) emit BEFORE any route mount, so a cross-cutting concern
36
+ // (auth, rate-limit, i18n) applies to every route regardless of where it sits in the manifest.
34
37
  for (const s of services) {
35
38
  const m = CATALOG[s].mount;
36
39
  if (m.kind === "middleware") {
37
40
  imports.push(`import { ${m.symbol} } from "${m.from}";`);
38
- body.push(`${m.symbol}(app);`);
41
+ middleware.push(`${m.symbol}(app);`);
39
42
  } else if (m.kind === "route") {
40
43
  imports.push(`import { ${m.symbol} } from "${m.from}";`);
41
- body.push(`app.route("${m.path}", ${m.symbol}());`);
44
+ routes.push(`app.route("${m.path}", ${m.symbol}());`);
42
45
  }
43
46
  }
47
+ const body = ["const app = createApp();", ...middleware, ...routes];
44
48
  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\nexport default app;\n`;
45
49
  }
46
50
 
package/test/plan.test.ts CHANGED
@@ -20,8 +20,8 @@ describe("planPlatform — manifest → shadcn adds + entry + provision.config",
20
20
  expect(plan.entry).toContain('import { mountAuthRoutes } from "./auth";');
21
21
  expect(plan.entry).toContain("mountAuthRoutes(app);");
22
22
  expect(plan.entry).toContain('import { creditsRoutes } from "./routes/credits";');
23
- expect(plan.entry).toContain('app.route("/credits", creditsRoutes());');
24
- expect(plan.entry).toContain('app.route("/billing", billingRoutes());');
23
+ expect(plan.entry).toContain('app.route("/api/credits", creditsRoutes());');
24
+ expect(plan.entry).toContain('app.route("/api/billing", billingRoutes());');
25
25
  expect(plan.entry).toContain("export default app;");
26
26
  });
27
27
 
@@ -42,7 +42,7 @@ describe("cost (route + provision) + dev modules (journeys/audit — files only)
42
42
 
43
43
  test("cost mounts a /cost route and contributes a provision fragment", () => {
44
44
  expect(plan.entry).toContain('import { costRoutes } from "./routes/cost";');
45
- expect(plan.entry).toContain('app.route("/cost", costRoutes());');
45
+ expect(plan.entry).toContain('app.route("/api/cost", costRoutes());');
46
46
  expect(plan.provisionConfig).toContain('import { costProvision } from "./src/provision/cost";');
47
47
  expect(plan.provisionConfig).toContain("mergeProvision([authProvision, creditsProvision, costProvision, logsProvision])");
48
48
  });
@@ -50,7 +50,7 @@ describe("cost (route + provision) + dev modules (journeys/audit — files only)
50
50
  test("erasure mounts an /erasure route and contributes a provision fragment", () => {
51
51
  const p = planPlatform(definePlatform({ name: "e", registry: "acme/reg", services: ["auth", "erasure"] }));
52
52
  expect(p.entry).toContain('import { erasureRoutes } from "./routes/erasure";');
53
- expect(p.entry).toContain('app.route("/erasure", erasureRoutes());');
53
+ expect(p.entry).toContain('app.route("/api/erasure", erasureRoutes());');
54
54
  expect(p.provisionConfig).toContain('import { erasureProvision } from "./src/provision/erasure";');
55
55
  expect(p.provisionConfig).toContain("mergeProvision([authProvision, erasureProvision])");
56
56
  });
@@ -58,12 +58,39 @@ describe("cost (route + provision) + dev modules (journeys/audit — files only)
58
58
  test("email mounts an /email route but contributes NO provision fragment (stateless binding)", () => {
59
59
  const p = planPlatform(definePlatform({ name: "m", registry: "acme/reg", services: ["auth", "email", "credits"] }));
60
60
  expect(p.entry).toContain('import { emailRoutes } from "./routes/email";');
61
- expect(p.entry).toContain('app.route("/email", emailRoutes());');
61
+ expect(p.entry).toContain('app.route("/api/email", emailRoutes());');
62
62
  // email has no provision fragment, so the merge is just auth + credits.
63
63
  expect(p.provisionConfig).not.toContain("emailProvision");
64
64
  expect(p.provisionConfig).toContain("mergeProvision([authProvision, creditsProvision])");
65
65
  });
66
66
 
67
+ test("webhooks mounts a /webhooks route and contributes a provision fragment", () => {
68
+ const p = planPlatform(definePlatform({ name: "w", registry: "acme/reg", services: ["auth", "webhooks"] }));
69
+ expect(p.entry).toContain('import { webhooksRoutes } from "./routes/webhooks";');
70
+ expect(p.entry).toContain('app.route("/api/webhooks", webhooksRoutes());');
71
+ expect(p.provisionConfig).toContain("mergeProvision([authProvision, webhooksProvision])");
72
+ });
73
+
74
+ test("rate-limit + i18n are MIDDLEWARE mounts (app.use, no route, no provision) emitted BEFORE routes", () => {
75
+ const p = planPlatform(definePlatform({ name: "mw", registry: "acme/reg", services: ["auth", "credits", "rate-limit", "i18n"] }));
76
+ expect(p.entry).toContain("mountRateLimit(app);");
77
+ expect(p.entry).toContain("mountI18n(app);");
78
+ // no route, no provision for either.
79
+ expect(p.entry).not.toContain('app.route("/rate-limit"');
80
+ expect(p.provisionConfig).toContain("mergeProvision([authProvision, creditsProvision])");
81
+ // two-pass ordering: every middleware mount precedes every route mount, so global middleware applies to all routes.
82
+ const lastMw = Math.max(p.entry.indexOf("mountRateLimit(app);"), p.entry.indexOf("mountI18n(app);"), p.entry.indexOf("mountAuthRoutes(app);"));
83
+ expect(lastMw).toBeLessThan(p.entry.indexOf('app.route("/api/credits"'));
84
+ });
85
+
86
+ test("contract mounts at /api (serving /api/openapi.json) with NO provision; feature routes are under /api/*", () => {
87
+ const p = planPlatform(definePlatform({ name: "c", registry: "acme/reg", services: ["auth", "contract", "credits"] }));
88
+ expect(p.entry).toContain('import { contractRoutes } from "./routes/contract";');
89
+ expect(p.entry).toContain('app.route("/api", contractRoutes());');
90
+ expect(p.entry).toContain('app.route("/api/credits", creditsRoutes());'); // toolfactory-parity /api/* prefix
91
+ expect(p.provisionConfig).not.toContain("contractProvision");
92
+ });
93
+
67
94
  test("dev modules add shadcn refs but NO entry mount and NO provision fragment", () => {
68
95
  expect(plan.adds).toContain("acme/reg/journeys");
69
96
  expect(plan.adds).toContain("acme/reg/audit");