@stratum-hq/create 0.4.0 → 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.
@@ -1,7 +1,6 @@
1
1
  import type { StackPreset } from "../matrix.js";
2
2
 
3
3
  export function generatePresetReadme(projectName: string, preset: StackPreset): string {
4
- const devCmd = getDevCommand(preset);
5
4
  const dbStartCmd = getDbStartInfo(preset);
6
5
  const dbSetupNote = getDbSetupNote(preset);
7
6
 
@@ -37,7 +36,7 @@ ${dbSetupNote}
37
36
  ### 4. Run the app
38
37
 
39
38
  \`\`\`bash
40
- ${devCmd}
39
+ npm run dev
41
40
  \`\`\`
42
41
 
43
42
  ## Multi-tenancy
@@ -50,13 +49,6 @@ See the [Stratum docs](https://github.com/stratum-hq/Stratum) for full reference
50
49
  `;
51
50
  }
52
51
 
53
- function getDevCommand(preset: StackPreset): string {
54
- if (preset.framework === "nextjs") return "npm run dev";
55
- if (preset.framework === "nestjs") return "node --env-file=.env src/main.ts";
56
- if (preset.framework === "none") return "node --env-file=.env src/index.ts";
57
- return "node --env-file=.env src/index.ts";
58
- }
59
-
60
52
  function getDbStartInfo(preset: StackPreset): string {
61
53
  switch (preset.database) {
62
54
  case "postgres":
@@ -95,7 +87,7 @@ function getStrategyDescription(strategy: string): string {
95
87
  switch (strategy) {
96
88
  case "rls":
97
89
  return `- **Row-Level Security** -- PostgreSQL RLS policies filter rows by tenant automatically
98
- - Each query sets \`app.current_tenant\` and RLS enforces isolation
90
+ - Each query sets \`app.current_tenant_id\` and RLS enforces isolation
99
91
  - All tenants share one database and schema`;
100
92
  case "schema":
101
93
  return `- **Schema-per-tenant** -- each tenant gets a dedicated PostgreSQL schema
@@ -0,0 +1,48 @@
1
+ import type { Framework, StackPreset } from "../matrix.js";
2
+
3
+ /**
4
+ * Return the source files that a preset writes at the project root and that code in `src/` imports.
5
+ *
6
+ * @param preset - The stack preset of the generated project.
7
+ */
8
+ export function rootSources(preset: StackPreset): string[] {
9
+ // The knex CLI reads knexfile.ts from the project root, and src/stratum-knex.ts imports it.
10
+ return preset.orm === "knex" ? ["knexfile.ts"] : [];
11
+ }
12
+
13
+ /**
14
+ * Return the tsconfig.json content for a generated project.
15
+ *
16
+ * @param framework - The framework of the project. It adds the compiler options that the framework needs.
17
+ * @param extraSources - Source files at the project root that code in `src/` imports.
18
+ * When there are any, tsc compiles from the project root, so the output of `src/` goes to `dist/src/`.
19
+ */
20
+ export function generateTsconfig(framework: Framework, extraSources: string[] = []): string {
21
+ // tsc rejects an imported file outside rootDir with TS6059.
22
+ const rootDir = extraSources.length > 0 ? "." : "src";
23
+ return JSON.stringify(
24
+ {
25
+ compilerOptions: {
26
+ target: "ESNext",
27
+ module: "NodeNext",
28
+ moduleResolution: "NodeNext",
29
+ strict: true,
30
+ outDir: "dist",
31
+ rootDir,
32
+ declaration: true,
33
+ skipLibCheck: true,
34
+ esModuleInterop: true,
35
+ ...(framework === "nestjs"
36
+ ? { experimentalDecorators: true, emitDecoratorMetadata: true }
37
+ : {}),
38
+ ...(framework === "nextjs"
39
+ ? { jsx: "preserve", plugins: [{ name: "next" }] }
40
+ : {}),
41
+ },
42
+ include: ["src", ...extraSources],
43
+ exclude: ["node_modules", "dist"],
44
+ },
45
+ null,
46
+ 2,
47
+ );
48
+ }
package/src/index.ts CHANGED
@@ -1,13 +1,13 @@
1
- #!/usr/bin/env node
2
-
3
1
  import * as fs from "fs";
4
2
  import * as path from "path";
5
3
  import crypto from "node:crypto";
6
4
  import { execSync } from "child_process";
7
- import { fileURLToPath } from "url";
8
5
  import { parsePresetString, isValidPreset } from "./matrix.js";
9
6
  import { createPresetProject } from "./preset-project.js";
7
+ import { STRATUM_RANGES } from "./stratum-versions.js";
10
8
  import { postgresAppRole, postgresAppRoleSql, POSTGRES_APP_PASSWORD } from "./generators/init-sql.js";
9
+ import { generateTsconfig } from "./generators/tsconfig.js";
10
+ import { nextjsTenantMiddleware } from "./generators/middleware.js";
11
11
 
12
12
  // ─── Types ────────────────────────────────────────────────────────────────────
13
13
 
@@ -123,11 +123,13 @@ function generatePackageJson(projectName: string, template: Template): string {
123
123
  "react-dom": "^19.0.0",
124
124
  "@types/react": "^19.0.0",
125
125
  "@types/react-dom": "^19.0.0",
126
+ // The middleware verifies the tenant JWT with jose.
127
+ jose: "^6.2.12",
126
128
  },
127
129
  };
128
130
 
129
131
  const deps = {
130
- "@stratum-hq/lib": "^0.2.0",
132
+ "@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
131
133
  pg: "^8.11.0",
132
134
  ...frameworkDeps[template],
133
135
  };
@@ -141,11 +143,13 @@ function generatePackageJson(projectName: string, template: Template): string {
141
143
  scripts:
142
144
  template === "nextjs"
143
145
  ? { dev: "next dev", build: "next build", start: "next start" }
144
- : { dev: "node --watch src/index.js", build: "tsc", start: "node dist/index.js" },
146
+ : // Node 20 cannot run a .ts file, so dev runs the source through tsx.
147
+ { dev: "tsx watch --env-file=.env src/index.ts", build: "tsc", start: "node dist/index.js" },
145
148
  dependencies: deps,
146
149
  devDependencies: {
147
150
  typescript: "^5.3.0",
148
151
  "@types/node": "^20.11.0",
152
+ ...(template === "nextjs" ? {} : { tsx: "^4.7.0", "@types/pg": "^8.11.0" }),
149
153
  },
150
154
  engines: {
151
155
  node: ">=20.0.0",
@@ -300,7 +304,7 @@ export default function Home() {
300
304
  <p>Multi-tenant app powered by Stratum.</p>
301
305
  <ul>
302
306
  <li>Configure tenants via the Stratum control plane</li>
303
- <li>Tenant is resolved from the subdomain in <code>middleware.ts</code></li>
307
+ <li>The tenant comes from a verified JWT in <code>middleware.ts</code></li>
304
308
  <li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
305
309
  </ul>
306
310
  </main>
@@ -309,37 +313,7 @@ export default function Home() {
309
313
  `;
310
314
  }
311
315
 
312
- function generateNextjsMiddleware(): string {
313
- return `// middleware.ts — tenant resolution via subdomain
314
- import { NextRequest, NextResponse } from "next/server";
315
-
316
- export function middleware(request: NextRequest) {
317
- // The tenant comes from the subdomain the request was routed to. Any
318
- // x-tenant-id the client sent is removed first, so server code that reads
319
- // x-tenant-id only ever sees the value set here. Once you add
320
- // authentication, check that the signed-in user belongs to this tenant.
321
- const hostname = request.headers.get("host") || "";
322
- const tenantId = hostname.split(".")[0];
323
-
324
- const requestHeaders = new Headers(request.headers);
325
- requestHeaders.delete("x-tenant-id");
326
- if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
327
- requestHeaders.set("x-tenant-id", tenantId);
328
- }
329
-
330
- return NextResponse.next({ request: { headers: requestHeaders } });
331
- }
332
-
333
- export const config = {
334
- matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
335
- };
336
- `;
337
- }
338
-
339
316
  function generateReadme(projectName: string, template: Template): string {
340
- const devCmd =
341
- template === "nextjs" ? "npm run dev" : "node --env-file=.env src/index.ts";
342
-
343
317
  return `# ${projectName}
344
318
 
345
319
  A multi-tenant application built with [Stratum](https://github.com/stratum-hq/Stratum).
@@ -368,7 +342,7 @@ npm install
368
342
  ### 4. Run the app
369
343
 
370
344
  \`\`\`bash
371
- ${devCmd}
345
+ npm run dev
372
346
  \`\`\`
373
347
 
374
348
  ## Project structure
@@ -379,14 +353,14 @@ ${projectName}/
379
353
  ├── docker-compose.yml
380
354
  ├── init.sql # DB extensions (uuid-ossp, ltree)
381
355
  ├── .env.example
382
- └── package.json
356
+ ${template === "nextjs" ? "" : "├── tsconfig.json\n"}└── package.json
383
357
  \`\`\`
384
358
 
385
359
  ## Multi-tenancy
386
360
 
387
361
  This project uses Stratum for hierarchical multi-tenancy:
388
362
 
389
- - **Tenant resolution** — via subdomain (see \`middleware.ts\`); bind it to the signed-in user once you add authentication
363
+ - **Tenant resolution** — from the \`tenant_id\` claim of a bearer token verified with \`JWT_SECRET\` (see \`middleware.ts\`); the subdomain is only a display slug
390
364
  - **Config inheritance** — settings flow down the tenant tree with override support
391
365
  - **Permission ABAC** — role-based permissions with tenant-scoped enforcement
392
366
 
@@ -422,11 +396,13 @@ export function createProject(
422
396
  // Server starter file
423
397
  if (template === "express") {
424
398
  writeFile(path.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
399
+ writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
425
400
  } else if (template === "fastify") {
426
401
  writeFile(path.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
402
+ writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
427
403
  } else if (template === "nextjs") {
428
404
  writeFile(path.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
429
- writeFile(path.join(targetDir, "middleware.ts"), generateNextjsMiddleware());
405
+ writeFile(path.join(targetDir, "middleware.ts"), nextjsTenantMiddleware());
430
406
  }
431
407
 
432
408
  // README
@@ -520,25 +496,6 @@ export function main(argv: string[]): void {
520
496
  if (skipInstall) {
521
497
  console.log(" npm install");
522
498
  }
523
- if (preset) {
524
- const parsed = parsePresetString(preset)!;
525
- if (parsed.framework === "nextjs") {
526
- console.log(" npm run dev");
527
- } else if (parsed.framework === "nestjs") {
528
- console.log(" node --env-file=.env src/main.ts");
529
- } else {
530
- console.log(" node --env-file=.env src/index.ts");
531
- }
532
- } else if (template === "nextjs") {
533
- console.log(" npm run dev");
534
- } else {
535
- console.log(" node --env-file=.env src/index.ts");
536
- }
499
+ console.log(" npm run dev");
537
500
  console.log("");
538
501
  }
539
-
540
- // Run when executed directly (not when imported by tests)
541
- const __filename = fileURLToPath(import.meta.url);
542
- if (process.argv[1] === __filename) {
543
- main(process.argv.slice(2));
544
- }
@@ -9,6 +9,7 @@ import { generateDbSetup } from "./generators/db-setup.js";
9
9
  import { generateMiddleware } from "./generators/middleware.js";
10
10
  import { generatePresetPackageJson } from "./generators/package-json.js";
11
11
  import { generatePresetReadme } from "./generators/readme.js";
12
+ import { generateTsconfig, rootSources } from "./generators/tsconfig.js";
12
13
 
13
14
  function writeFile(filePath: string, content: string): void {
14
15
  const dir = path.dirname(filePath);
@@ -114,31 +115,7 @@ export function createPresetProject(
114
115
  // tsconfig.json
115
116
  writeFile(
116
117
  path.join(targetDir, "tsconfig.json"),
117
- JSON.stringify(
118
- {
119
- compilerOptions: {
120
- target: "ESNext",
121
- module: "NodeNext",
122
- moduleResolution: "NodeNext",
123
- strict: true,
124
- outDir: "dist",
125
- rootDir: "src",
126
- declaration: true,
127
- skipLibCheck: true,
128
- esModuleInterop: true,
129
- ...(preset.framework === "nestjs"
130
- ? { experimentalDecorators: true, emitDecoratorMetadata: true }
131
- : {}),
132
- ...(preset.framework === "nextjs"
133
- ? { jsx: "preserve", plugins: [{ name: "next" }] }
134
- : {}),
135
- },
136
- include: ["src"],
137
- exclude: ["node_modules", "dist"],
138
- },
139
- null,
140
- 2,
141
- ),
118
+ generateTsconfig(preset.framework, rootSources(preset)),
142
119
  );
143
120
 
144
121
  // npm install
@@ -0,0 +1,20 @@
1
+ // esbuild inlines these files when it bundles the CLI, so the published package carries
2
+ // the versions of the workspace it was built from. The ranges are fixed at create's build
3
+ // time. When a pinned package gets a major bump, or a minor bump while at 0.x, release
4
+ // create again, or new projects keep the old range.
5
+ import hono from "../../hono/package.json" with { type: "json" };
6
+ import dbAdapters from "../../db-adapters/package.json" with { type: "json" };
7
+ import lib from "../../lib/package.json" with { type: "json" };
8
+ import mongodb from "../../mongodb/package.json" with { type: "json" };
9
+ import mysql from "../../mysql/package.json" with { type: "json" };
10
+ import nestjs from "../../nestjs/package.json" with { type: "json" };
11
+
12
+ /** Caret ranges on the current workspace versions of the packages a generated project uses. */
13
+ export const STRATUM_RANGES = {
14
+ "@stratum-hq/db-adapters": `^${dbAdapters.version}`,
15
+ "@stratum-hq/hono": `^${hono.version}`,
16
+ "@stratum-hq/lib": `^${lib.version}`,
17
+ "@stratum-hq/mongodb": `^${mongodb.version}`,
18
+ "@stratum-hq/mysql": `^${mysql.version}`,
19
+ "@stratum-hq/nestjs": `^${nestjs.version}`,
20
+ } as const;
package/turbo.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "$schema": "https://turbo.build/schema.json",
3
+ "extends": ["//"],
4
+ "tasks": {
5
+ "build": {
6
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/packages/*/package.json"]
7
+ },
8
+ "typecheck": {
9
+ "inputs": ["$TURBO_DEFAULT$", "$TURBO_ROOT$/packages/*/package.json"]
10
+ },
11
+ "test": {
12
+ "inputs": [
13
+ "$TURBO_DEFAULT$",
14
+ "$TURBO_ROOT$/packages/*/package.json",
15
+ "$TURBO_ROOT$/packages/{core,db-adapters,mysql,mongodb}/src/**"
16
+ ]
17
+ }
18
+ }
19
+ }