@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.
- package/CHANGELOG.md +23 -0
- package/README.md +7 -2
- package/dist/bin.js +3 -0
- package/dist/index.js +784 -183
- package/package.json +3 -3
- package/src/bin.ts +7 -0
- package/src/generators/db-setup.ts +91 -26
- package/src/generators/init-sql.ts +4 -1
- package/src/generators/middleware.ts +177 -57
- package/src/generators/package-json.ts +35 -12
- package/src/generators/readme.ts +2 -10
- package/src/generators/tsconfig.ts +48 -0
- package/src/index.ts +17 -60
- package/src/preset-project.ts +2 -25
- package/src/stratum-versions.ts +20 -0
- package/turbo.json +19 -0
package/src/generators/readme.ts
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": "
|
|
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
|
-
:
|
|
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>
|
|
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
|
-
|
|
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** —
|
|
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"),
|
|
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
|
-
|
|
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
|
-
}
|
package/src/preset-project.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|