@stratum-hq/create 0.3.1 → 0.4.1
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 +28 -0
- package/README.md +2 -1
- package/dist/bin.js +3 -0
- package/dist/index.js +712 -156
- package/dist/matrix.js +16 -32
- package/package.json +5 -5
- package/src/bin.ts +7 -0
- package/src/generators/db-setup.ts +94 -31
- package/src/generators/init-sql.ts +50 -3
- package/src/generators/middleware.ts +30 -21
- package/src/generators/package-json.ts +31 -12
- package/src/generators/readme.ts +2 -10
- package/src/generators/tsconfig.ts +48 -0
- package/src/index.ts +29 -42
- package/src/preset-project.ts +10 -28
- package/src/stratum-versions.ts +20 -0
- package/turbo.json +19 -0
package/src/index.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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 {
|
|
8
|
-
import { parsePresetString, isValidPreset, formatPresetString } from "./matrix.js";
|
|
5
|
+
import { parsePresetString, isValidPreset } from "./matrix.js";
|
|
9
6
|
import { createPresetProject } from "./preset-project.js";
|
|
7
|
+
import { STRATUM_RANGES } from "./stratum-versions.js";
|
|
8
|
+
import { postgresAppRole, postgresAppRoleSql, POSTGRES_APP_PASSWORD } from "./generators/init-sql.js";
|
|
9
|
+
import { generateTsconfig } from "./generators/tsconfig.js";
|
|
10
10
|
|
|
11
11
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
12
12
|
|
|
@@ -126,7 +126,7 @@ function generatePackageJson(projectName: string, template: Template): string {
|
|
|
126
126
|
};
|
|
127
127
|
|
|
128
128
|
const deps = {
|
|
129
|
-
"@stratum-hq/lib": "
|
|
129
|
+
"@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
|
|
130
130
|
pg: "^8.11.0",
|
|
131
131
|
...frameworkDeps[template],
|
|
132
132
|
};
|
|
@@ -140,11 +140,13 @@ function generatePackageJson(projectName: string, template: Template): string {
|
|
|
140
140
|
scripts:
|
|
141
141
|
template === "nextjs"
|
|
142
142
|
? { dev: "next dev", build: "next build", start: "next start" }
|
|
143
|
-
:
|
|
143
|
+
: // Node 20 cannot run a .ts file, so dev runs the source through tsx.
|
|
144
|
+
{ dev: "tsx watch --env-file=.env src/index.ts", build: "tsc", start: "node dist/index.js" },
|
|
144
145
|
dependencies: deps,
|
|
145
146
|
devDependencies: {
|
|
146
147
|
typescript: "^5.3.0",
|
|
147
148
|
"@types/node": "^20.11.0",
|
|
149
|
+
...(template === "nextjs" ? {} : { tsx: "^4.7.0", "@types/pg": "^8.11.0" }),
|
|
148
150
|
},
|
|
149
151
|
engines: {
|
|
150
152
|
node: ">=20.0.0",
|
|
@@ -194,7 +196,7 @@ CREATE EXTENSION IF NOT EXISTS "ltree";
|
|
|
194
196
|
-- The ltree extension enables hierarchical tenant trees
|
|
195
197
|
-- uuid-ossp provides uuid_generate_v4() for tenant IDs
|
|
196
198
|
COMMENT ON DATABASE ${dbName} IS 'Multi-tenant database for ${projectName}';
|
|
197
|
-
`;
|
|
199
|
+
${postgresAppRoleSql(dbName)}`;
|
|
198
200
|
}
|
|
199
201
|
|
|
200
202
|
function generateEnv(projectName: string): string {
|
|
@@ -203,8 +205,12 @@ function generateEnv(projectName: string): string {
|
|
|
203
205
|
return `# Environment variables for ${projectName}
|
|
204
206
|
# Copy to .env and fill in values
|
|
205
207
|
|
|
206
|
-
# Database
|
|
207
|
-
|
|
208
|
+
# Database. The app connects as the non-superuser role created in init.sql,
|
|
209
|
+
# so row-level security applies to it.
|
|
210
|
+
DATABASE_URL=postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}
|
|
211
|
+
|
|
212
|
+
# Superuser: bootstrap and migrations only. It bypasses row-level security.
|
|
213
|
+
DATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}
|
|
208
214
|
|
|
209
215
|
# Authentication
|
|
210
216
|
JWT_SECRET=${jwtSecret}
|
|
@@ -295,7 +301,7 @@ export default function Home() {
|
|
|
295
301
|
<p>Multi-tenant app powered by Stratum.</p>
|
|
296
302
|
<ul>
|
|
297
303
|
<li>Configure tenants via the Stratum control plane</li>
|
|
298
|
-
<li>
|
|
304
|
+
<li>Tenant is resolved from the subdomain in <code>middleware.ts</code></li>
|
|
299
305
|
<li>Use <code>@stratum-hq/lib</code> for tenant resolution</li>
|
|
300
306
|
</ul>
|
|
301
307
|
</main>
|
|
@@ -305,18 +311,19 @@ export default function Home() {
|
|
|
305
311
|
}
|
|
306
312
|
|
|
307
313
|
function generateNextjsMiddleware(): string {
|
|
308
|
-
return `// middleware.ts — tenant resolution via subdomain
|
|
314
|
+
return `// middleware.ts — tenant resolution via subdomain
|
|
309
315
|
import { NextRequest, NextResponse } from "next/server";
|
|
310
316
|
|
|
311
317
|
export function middleware(request: NextRequest) {
|
|
318
|
+
// The tenant comes from the subdomain the request was routed to. Any
|
|
319
|
+
// x-tenant-id the client sent is removed first, so server code that reads
|
|
320
|
+
// x-tenant-id only ever sees the value set here. Once you add
|
|
321
|
+
// authentication, check that the signed-in user belongs to this tenant.
|
|
312
322
|
const hostname = request.headers.get("host") || "";
|
|
313
|
-
const
|
|
314
|
-
const headerTenantId = request.headers.get("x-tenant-id");
|
|
315
|
-
const pathTenantId = request.nextUrl.pathname.match(/^\\/tenant\\/([^/]+)/)?.[1];
|
|
316
|
-
|
|
317
|
-
const tenantId = headerTenantId || pathTenantId || subdomain;
|
|
323
|
+
const tenantId = hostname.split(".")[0];
|
|
318
324
|
|
|
319
325
|
const requestHeaders = new Headers(request.headers);
|
|
326
|
+
requestHeaders.delete("x-tenant-id");
|
|
320
327
|
if (tenantId && tenantId !== "localhost" && tenantId !== "www") {
|
|
321
328
|
requestHeaders.set("x-tenant-id", tenantId);
|
|
322
329
|
}
|
|
@@ -331,9 +338,6 @@ export const config = {
|
|
|
331
338
|
}
|
|
332
339
|
|
|
333
340
|
function generateReadme(projectName: string, template: Template): string {
|
|
334
|
-
const devCmd =
|
|
335
|
-
template === "nextjs" ? "npm run dev" : "node --env-file=.env src/index.ts";
|
|
336
|
-
|
|
337
341
|
return `# ${projectName}
|
|
338
342
|
|
|
339
343
|
A multi-tenant application built with [Stratum](https://github.com/stratum-hq/Stratum).
|
|
@@ -362,7 +366,7 @@ npm install
|
|
|
362
366
|
### 4. Run the app
|
|
363
367
|
|
|
364
368
|
\`\`\`bash
|
|
365
|
-
|
|
369
|
+
npm run dev
|
|
366
370
|
\`\`\`
|
|
367
371
|
|
|
368
372
|
## Project structure
|
|
@@ -373,14 +377,14 @@ ${projectName}/
|
|
|
373
377
|
├── docker-compose.yml
|
|
374
378
|
├── init.sql # DB extensions (uuid-ossp, ltree)
|
|
375
379
|
├── .env.example
|
|
376
|
-
└── package.json
|
|
380
|
+
${template === "nextjs" ? "" : "├── tsconfig.json\n"}└── package.json
|
|
377
381
|
\`\`\`
|
|
378
382
|
|
|
379
383
|
## Multi-tenancy
|
|
380
384
|
|
|
381
385
|
This project uses Stratum for hierarchical multi-tenancy:
|
|
382
386
|
|
|
383
|
-
- **Tenant resolution** — via
|
|
387
|
+
- **Tenant resolution** — via subdomain (see \`middleware.ts\`); bind it to the signed-in user once you add authentication
|
|
384
388
|
- **Config inheritance** — settings flow down the tenant tree with override support
|
|
385
389
|
- **Permission ABAC** — role-based permissions with tenant-scoped enforcement
|
|
386
390
|
|
|
@@ -416,8 +420,10 @@ export function createProject(
|
|
|
416
420
|
// Server starter file
|
|
417
421
|
if (template === "express") {
|
|
418
422
|
writeFile(path.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
|
|
423
|
+
writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
|
|
419
424
|
} else if (template === "fastify") {
|
|
420
425
|
writeFile(path.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
|
|
426
|
+
writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
|
|
421
427
|
} else if (template === "nextjs") {
|
|
422
428
|
writeFile(path.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
|
|
423
429
|
writeFile(path.join(targetDir, "middleware.ts"), generateNextjsMiddleware());
|
|
@@ -514,25 +520,6 @@ export function main(argv: string[]): void {
|
|
|
514
520
|
if (skipInstall) {
|
|
515
521
|
console.log(" npm install");
|
|
516
522
|
}
|
|
517
|
-
|
|
518
|
-
const parsed = parsePresetString(preset)!;
|
|
519
|
-
if (parsed.framework === "nextjs") {
|
|
520
|
-
console.log(" npm run dev");
|
|
521
|
-
} else if (parsed.framework === "nestjs") {
|
|
522
|
-
console.log(" node --env-file=.env src/main.ts");
|
|
523
|
-
} else {
|
|
524
|
-
console.log(" node --env-file=.env src/index.ts");
|
|
525
|
-
}
|
|
526
|
-
} else if (template === "nextjs") {
|
|
527
|
-
console.log(" npm run dev");
|
|
528
|
-
} else {
|
|
529
|
-
console.log(" node --env-file=.env src/index.ts");
|
|
530
|
-
}
|
|
523
|
+
console.log(" npm run dev");
|
|
531
524
|
console.log("");
|
|
532
525
|
}
|
|
533
|
-
|
|
534
|
-
// Run when executed directly (not when imported by tests)
|
|
535
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
536
|
-
if (process.argv[1] === __filename) {
|
|
537
|
-
main(process.argv.slice(2));
|
|
538
|
-
}
|
package/src/preset-project.ts
CHANGED
|
@@ -4,11 +4,12 @@ import crypto from "node:crypto";
|
|
|
4
4
|
import { execSync } from "child_process";
|
|
5
5
|
import type { StackPreset } from "./matrix.js";
|
|
6
6
|
import { generatePresetDockerCompose } from "./generators/docker-compose.js";
|
|
7
|
-
import { generatePresetInitSql } from "./generators/init-sql.js";
|
|
7
|
+
import { generatePresetInitSql, postgresAppRole, POSTGRES_APP_PASSWORD } from "./generators/init-sql.js";
|
|
8
8
|
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);
|
|
@@ -24,9 +25,14 @@ function generatePresetEnv(projectName: string, preset: StackPreset): string {
|
|
|
24
25
|
const jwtSecret = crypto.randomBytes(32).toString("base64url");
|
|
25
26
|
|
|
26
27
|
let dbUrl: string;
|
|
28
|
+
let adminUrlLine = "";
|
|
27
29
|
switch (preset.database) {
|
|
28
30
|
case "postgres":
|
|
29
|
-
|
|
31
|
+
// The app connects as the non-superuser role created in init.sql, so
|
|
32
|
+
// row-level security applies to it. The superuser URL is for bootstrap
|
|
33
|
+
// and migrations only.
|
|
34
|
+
dbUrl = `postgres://${postgresAppRole(dbName)}:${POSTGRES_APP_PASSWORD}@localhost:5432/${dbName}`;
|
|
35
|
+
adminUrlLine = `\n# Superuser: bootstrap and migrations only. It bypasses row-level security.\nDATABASE_ADMIN_URL=postgres://${dbName}:dev_password@localhost:5432/${dbName}\n`;
|
|
30
36
|
break;
|
|
31
37
|
case "mongodb":
|
|
32
38
|
dbUrl = `mongodb://${dbName}:dev_password@localhost:27017/${dbName}?authSource=admin`;
|
|
@@ -43,7 +49,7 @@ function generatePresetEnv(projectName: string, preset: StackPreset): string {
|
|
|
43
49
|
|
|
44
50
|
# Database
|
|
45
51
|
${urlKey}=${dbUrl}
|
|
46
|
-
|
|
52
|
+
${adminUrlLine}
|
|
47
53
|
# Authentication
|
|
48
54
|
JWT_SECRET=${jwtSecret}
|
|
49
55
|
|
|
@@ -109,31 +115,7 @@ export function createPresetProject(
|
|
|
109
115
|
// tsconfig.json
|
|
110
116
|
writeFile(
|
|
111
117
|
path.join(targetDir, "tsconfig.json"),
|
|
112
|
-
|
|
113
|
-
{
|
|
114
|
-
compilerOptions: {
|
|
115
|
-
target: "ESNext",
|
|
116
|
-
module: "NodeNext",
|
|
117
|
-
moduleResolution: "NodeNext",
|
|
118
|
-
strict: true,
|
|
119
|
-
outDir: "dist",
|
|
120
|
-
rootDir: "src",
|
|
121
|
-
declaration: true,
|
|
122
|
-
skipLibCheck: true,
|
|
123
|
-
esModuleInterop: true,
|
|
124
|
-
...(preset.framework === "nestjs"
|
|
125
|
-
? { experimentalDecorators: true, emitDecoratorMetadata: true }
|
|
126
|
-
: {}),
|
|
127
|
-
...(preset.framework === "nextjs"
|
|
128
|
-
? { jsx: "preserve", plugins: [{ name: "next" }] }
|
|
129
|
-
: {}),
|
|
130
|
-
},
|
|
131
|
-
include: ["src"],
|
|
132
|
-
exclude: ["node_modules", "dist"],
|
|
133
|
-
},
|
|
134
|
-
null,
|
|
135
|
-
2,
|
|
136
|
-
),
|
|
118
|
+
generateTsconfig(preset.framework, rootSources(preset)),
|
|
137
119
|
);
|
|
138
120
|
|
|
139
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
|
+
}
|