@stratum-hq/create 0.4.0 → 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 +17 -0
- package/README.md +2 -1
- package/dist/bin.js +3 -0
- package/dist/index.js +612 -96
- 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/package-json.ts +31 -12
- package/src/generators/readme.ts +2 -10
- package/src/generators/tsconfig.ts +48 -0
- package/src/index.ts +11 -30
- package/src/preset-project.ts +2 -25
- package/src/stratum-versions.ts +20 -0
- package/turbo.json +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stratum-hq/create",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Create a new Stratum multi-tenancy project",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"multi-tenancy",
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
"stratum"
|
|
17
17
|
],
|
|
18
18
|
"bin": {
|
|
19
|
-
"create-stratum": "./dist/
|
|
19
|
+
"create-stratum": "./dist/bin.js"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": "./dist/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"type": "module",
|
|
26
26
|
"scripts": {
|
|
27
|
-
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@stratum-hq/* && esbuild src/matrix.ts --bundle --platform=node --format=esm --outfile=dist/matrix.js --external:@stratum-hq/*",
|
|
27
|
+
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:@stratum-hq/* && esbuild src/matrix.ts --bundle --platform=node --format=esm --outfile=dist/matrix.js --external:@stratum-hq/* && esbuild src/bin.ts --platform=node --format=esm --outfile=dist/bin.js",
|
|
28
28
|
"typecheck": "tsc --noEmit",
|
|
29
29
|
"lint": "eslint .",
|
|
30
30
|
"test": "vitest run"
|
package/src/bin.ts
ADDED
|
@@ -203,60 +203,120 @@ export default config;
|
|
|
203
203
|
},
|
|
204
204
|
{
|
|
205
205
|
filename: "src/stratum-knex.ts",
|
|
206
|
-
content:
|
|
206
|
+
content: preset.database === "postgres" ? KNEX_POSTGRES_SCOPE : knexColumnScope(preset.database),
|
|
207
|
+
},
|
|
208
|
+
];
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// PostgreSQL does not accept a bind parameter in SET, so the generated code
|
|
212
|
+
// calls set_config. The third argument (is_local = true) ends the setting with
|
|
213
|
+
// the transaction, so a pooled connection never keeps another tenant's ID.
|
|
214
|
+
const KNEX_POSTGRES_SCOPE = `// Knex with Stratum tenant-scoped queries
|
|
215
|
+
import createKnex, { type Knex } from "knex";
|
|
216
|
+
import config from "../knexfile.js";
|
|
217
|
+
|
|
218
|
+
const knex = createKnex(config);
|
|
219
|
+
|
|
220
|
+
// Run fn in a transaction that sets app.current_tenant_id, the setting that
|
|
221
|
+
// the RLS policies read. The setting ends with the transaction, so run every
|
|
222
|
+
// tenant query through trx, not through knex.
|
|
223
|
+
export async function withTenantScope<T>(
|
|
224
|
+
tenantId: string,
|
|
225
|
+
fn: (trx: Knex.Transaction) => Promise<T>,
|
|
226
|
+
): Promise<T> {
|
|
227
|
+
return knex.transaction(async (trx) => {
|
|
228
|
+
await trx.raw("SELECT set_config('app.current_tenant_id', ?, true)", [tenantId]);
|
|
229
|
+
return fn(trx);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Usage:
|
|
234
|
+
// const orders = await withTenantScope(currentTenantId, (trx) => trx("orders").select());
|
|
235
|
+
|
|
236
|
+
export { knex };
|
|
237
|
+
`;
|
|
238
|
+
|
|
239
|
+
function knexColumnScope(database: string): string {
|
|
240
|
+
return `// Knex with Stratum tenant-scoped queries
|
|
207
241
|
import Knex from "knex";
|
|
208
242
|
import config from "../knexfile.js";
|
|
209
243
|
|
|
210
244
|
const knex = Knex(config);
|
|
211
245
|
|
|
212
246
|
// Create a tenant-scoped query builder.
|
|
213
|
-
// For RLS strategy, set the session variable before queries.
|
|
214
247
|
export async function withTenantScope(tenantId: string, fn: (db: typeof knex) => Promise<void>) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
await fn(knex);
|
|
218
|
-
} finally {
|
|
219
|
-
${preset.database === "postgres" ? ` await knex.raw("RESET app.current_tenant");` : ` // Scope cleanup not needed for column-based isolation`}
|
|
220
|
-
}
|
|
248
|
+
// For ${database}, scope queries by tenant_id column
|
|
249
|
+
await fn(knex);
|
|
221
250
|
}
|
|
222
251
|
|
|
223
252
|
export { knex };
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
253
|
+
`;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// @stratum-hq/mongodb has no Mongoose connection helper. The generated code uses
|
|
257
|
+
// Mongoose directly and copies the tenant names of the @stratum-hq/mongodb
|
|
258
|
+
// adapters, so MongoDatabaseAdapter and MongoCollectionAdapter find the same
|
|
259
|
+
// data, for example for purgeTenantData.
|
|
260
|
+
function generateMongooseSetup(preset: StackPreset): DbSetupFile[] {
|
|
261
|
+
const tenantAccess =
|
|
262
|
+
preset.strategy === "collection"
|
|
263
|
+
? `// Return the tenant's model for a base collection. The tenant's documents are
|
|
264
|
+
// in the collection {baseCollection}_{tenantSlug}, the name that
|
|
265
|
+
// MongoCollectionAdapter from @stratum-hq/mongodb uses.
|
|
266
|
+
export function getTenantModel<T>(
|
|
267
|
+
baseCollection: string,
|
|
268
|
+
schema: mongoose.Schema<T>,
|
|
269
|
+
tenantSlug: string,
|
|
270
|
+
) {
|
|
271
|
+
const name = \`\${baseCollection}_\${assertSlug(tenantSlug)}\`;
|
|
272
|
+
return mainConnection.models[name] ?? mainConnection.model(name, schema, name);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Usage:
|
|
276
|
+
// const Order = getTenantModel("orders", OrderSchema, "tenant_abc");
|
|
277
|
+
// const orders = await Order.find();`
|
|
278
|
+
: `// Return a connection to the tenant's own database. The database name is
|
|
279
|
+
// stratum_tenant_{tenantSlug}, the name that MongoDatabaseAdapter from
|
|
280
|
+
// @stratum-hq/mongodb uses. useCache returns the same connection on each call.
|
|
281
|
+
export function getTenantConnection(tenantSlug: string) {
|
|
282
|
+
return mainConnection.useDb(\`stratum_tenant_\${assertSlug(tenantSlug)}\`, { useCache: true });
|
|
227
283
|
}
|
|
228
284
|
|
|
229
|
-
|
|
285
|
+
// Usage:
|
|
286
|
+
// const conn = getTenantConnection("tenant_abc");
|
|
287
|
+
// const Order = conn.model("Order", OrderSchema);
|
|
288
|
+
// const orders = await Order.find();`;
|
|
289
|
+
|
|
230
290
|
return [
|
|
231
291
|
{
|
|
232
292
|
filename: "src/stratum-mongoose.ts",
|
|
233
293
|
content: `// Mongoose with Stratum multi-tenant support
|
|
234
294
|
import mongoose from "mongoose";
|
|
235
|
-
import { createTenantConnection } from "@stratum-hq/mongodb";
|
|
236
295
|
|
|
237
296
|
// Main connection (used for tenant metadata)
|
|
238
297
|
const mainConnection = mongoose.createConnection(
|
|
239
298
|
process.env.MONGODB_URI || "mongodb://localhost:27017/main",
|
|
240
299
|
);
|
|
241
300
|
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
301
|
+
// A tenant slug becomes part of a MongoDB name, so it must match the Stratum
|
|
302
|
+
// slug rule: a lowercase letter, then lowercase letters, digits or underscores.
|
|
303
|
+
const SLUG_PATTERN = /^[a-z][a-z0-9_]{0,62}$/;
|
|
304
|
+
|
|
305
|
+
function assertSlug(tenantSlug: string): string {
|
|
306
|
+
if (!SLUG_PATTERN.test(tenantSlug)) {
|
|
307
|
+
throw new Error(\`Invalid tenant slug: "\${tenantSlug}"\`);
|
|
308
|
+
}
|
|
309
|
+
return tenantSlug;
|
|
247
310
|
}
|
|
248
311
|
|
|
312
|
+
${tenantAccess}
|
|
313
|
+
|
|
249
314
|
// Define schemas that work across tenant connections
|
|
250
315
|
export const TenantSchema = new mongoose.Schema({
|
|
251
316
|
name: { type: String, required: true },
|
|
252
317
|
createdAt: { type: Date, default: Date.now },
|
|
253
318
|
});
|
|
254
319
|
|
|
255
|
-
// Usage:
|
|
256
|
-
// const conn = getTenantConnection("tenant-abc");
|
|
257
|
-
// const Order = conn.model("Order", OrderSchema);
|
|
258
|
-
// const orders = await Order.find();
|
|
259
|
-
|
|
260
320
|
export { mainConnection };
|
|
261
321
|
`,
|
|
262
322
|
},
|
|
@@ -276,7 +336,11 @@ const pool = mysql.createPool({
|
|
|
276
336
|
});
|
|
277
337
|
|
|
278
338
|
// Execute a query scoped to a tenant by filtering on tenant_id
|
|
279
|
-
export async function tenantQuery(
|
|
339
|
+
export async function tenantQuery(
|
|
340
|
+
tenantId: string,
|
|
341
|
+
sql: string,
|
|
342
|
+
params: (string | number | bigint | boolean | Date | null)[] = [],
|
|
343
|
+
) {
|
|
280
344
|
const [rows] = await pool.execute(sql, [...params, tenantId]);
|
|
281
345
|
return rows;
|
|
282
346
|
}
|
|
@@ -302,9 +366,10 @@ const pool = new Pool({
|
|
|
302
366
|
});
|
|
303
367
|
|
|
304
368
|
// Create a tenant-scoped pool.
|
|
305
|
-
//
|
|
369
|
+
// Each query runs in a transaction that sets app.current_tenant_id, the
|
|
370
|
+
// setting that the RLS policies read.
|
|
306
371
|
export function getTenantPool(tenantId: string) {
|
|
307
|
-
return createTenantPool(pool, tenantId);
|
|
372
|
+
return createTenantPool(pool, () => tenantId);
|
|
308
373
|
}
|
|
309
374
|
|
|
310
375
|
// Usage:
|
|
@@ -65,8 +65,11 @@ function generatePostgresInit(projectName: string, dbName: string, strategy: str
|
|
|
65
65
|
-- ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
|
|
66
66
|
-- ALTER TABLE your_table FORCE ROW LEVEL SECURITY;
|
|
67
67
|
-- CREATE POLICY tenant_isolation ON your_table
|
|
68
|
-
-- USING (tenant_id = current_setting('app.current_tenant_id')::uuid);
|
|
68
|
+
-- USING (tenant_id = NULLIF(current_setting('app.current_tenant_id', true), '')::uuid);
|
|
69
69
|
--
|
|
70
|
+
-- A pooled connection reads the setting as '' after a tenant transaction ends,
|
|
71
|
+
-- or as NULL before the first one. NULLIF makes both return no rows; a bare
|
|
72
|
+
-- ::uuid cast of '' raises an error.
|
|
70
73
|
-- FORCE makes the policy apply to the table owner too; without it, a table
|
|
71
74
|
-- created by the application role is not isolated for that role.
|
|
72
75
|
-- The Stratum db-adapters package sets app.current_tenant_id automatically.
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import type { StackPreset } from "../matrix.js";
|
|
2
|
+
import { rootSources } from "./tsconfig.js";
|
|
3
|
+
import { STRATUM_RANGES } from "../stratum-versions.js";
|
|
2
4
|
|
|
3
5
|
export function generatePresetPackageJson(projectName: string, preset: StackPreset): string {
|
|
4
6
|
const deps: Record<string, string> = {
|
|
5
|
-
"@stratum-hq/lib": "
|
|
7
|
+
"@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
|
|
6
8
|
};
|
|
7
9
|
const devDeps: Record<string, string> = {
|
|
8
10
|
typescript: "^5.3.0",
|
|
9
11
|
"@types/node": "^20.11.0",
|
|
10
12
|
};
|
|
13
|
+
// NestJS injection needs the decorator metadata that tsc emits and tsx does not.
|
|
14
|
+
if (preset.framework === "nestjs") {
|
|
15
|
+
devDeps["tsc-watch"] = "^7.2.0";
|
|
16
|
+
} else if (preset.framework !== "nextjs") {
|
|
17
|
+
devDeps["tsx"] = "^4.7.0";
|
|
18
|
+
}
|
|
11
19
|
|
|
12
20
|
// Database driver deps
|
|
13
|
-
addDatabaseDeps(deps, preset);
|
|
21
|
+
addDatabaseDeps(deps, devDeps, preset);
|
|
14
22
|
|
|
15
23
|
// ORM deps
|
|
16
24
|
addOrmDeps(deps, devDeps, preset);
|
|
@@ -41,9 +49,14 @@ export function generatePresetPackageJson(projectName: string, preset: StackPres
|
|
|
41
49
|
);
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
function addDatabaseDeps(deps: Record<string, string>, preset: StackPreset): void {
|
|
52
|
+
function addDatabaseDeps(deps: Record<string, string>, devDeps: Record<string, string>, preset: StackPreset): void {
|
|
45
53
|
switch (preset.database) {
|
|
46
54
|
case "postgres":
|
|
55
|
+
// The generated code for these ORMs imports pg itself. The strict tsc
|
|
56
|
+
// build fails with TS7016 when the pg types are not installed.
|
|
57
|
+
if (preset.orm === "pg" || preset.orm === "prisma" || preset.orm === "drizzle") {
|
|
58
|
+
devDeps["@types/pg"] = "^8.11.0";
|
|
59
|
+
}
|
|
47
60
|
if (preset.orm !== "prisma" && preset.orm !== "drizzle" && preset.orm !== "sequelize") {
|
|
48
61
|
deps["pg"] = "^8.11.0";
|
|
49
62
|
}
|
|
@@ -139,19 +152,19 @@ function addFrameworkDeps(deps: Record<string, string>, devDeps: Record<string,
|
|
|
139
152
|
|
|
140
153
|
function addStratumDeps(deps: Record<string, string>, preset: StackPreset): void {
|
|
141
154
|
if (preset.database === "postgres" && preset.orm !== "mongoose") {
|
|
142
|
-
deps["@stratum-hq/db-adapters"] = "
|
|
155
|
+
deps["@stratum-hq/db-adapters"] = STRATUM_RANGES["@stratum-hq/db-adapters"];
|
|
143
156
|
}
|
|
144
157
|
if (preset.database === "mongodb") {
|
|
145
|
-
deps["@stratum-hq/mongodb"] = "
|
|
158
|
+
deps["@stratum-hq/mongodb"] = STRATUM_RANGES["@stratum-hq/mongodb"];
|
|
146
159
|
}
|
|
147
160
|
if (preset.database === "mysql") {
|
|
148
|
-
deps["@stratum-hq/mysql"] = "
|
|
161
|
+
deps["@stratum-hq/mysql"] = STRATUM_RANGES["@stratum-hq/mysql"];
|
|
149
162
|
}
|
|
150
163
|
if (preset.framework === "hono") {
|
|
151
|
-
deps["@stratum-hq/hono"] = "
|
|
164
|
+
deps["@stratum-hq/hono"] = STRATUM_RANGES["@stratum-hq/hono"];
|
|
152
165
|
}
|
|
153
166
|
if (preset.framework === "nestjs") {
|
|
154
|
-
deps["@stratum-hq/nestjs"] = "
|
|
167
|
+
deps["@stratum-hq/nestjs"] = STRATUM_RANGES["@stratum-hq/nestjs"];
|
|
155
168
|
}
|
|
156
169
|
}
|
|
157
170
|
|
|
@@ -159,10 +172,16 @@ function getScripts(preset: StackPreset): Record<string, string> {
|
|
|
159
172
|
if (preset.framework === "nextjs") {
|
|
160
173
|
return { dev: "next dev", build: "next build", start: "next start" };
|
|
161
174
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
175
|
+
const entry = preset.framework === "nestjs" ? "main" : "index";
|
|
176
|
+
// When tsc compiles from the project root, src/ is emitted to dist/src/.
|
|
177
|
+
const emittedDir = rootSources(preset).length > 0 ? "dist/src" : "dist";
|
|
178
|
+
// Node 20 cannot run a .ts file. NestJS compiles with tsc, which keeps the
|
|
179
|
+
// decorator metadata its injection needs; the other frameworks run through tsx.
|
|
180
|
+
const dev =
|
|
181
|
+
preset.framework === "nestjs"
|
|
182
|
+
? `tsc-watch --onSuccess "node --env-file=.env ${emittedDir}/${entry}.js"`
|
|
183
|
+
: `tsx watch --env-file=.env src/${entry}.ts`;
|
|
184
|
+
return { dev, build: "tsc", start: `node ${emittedDir}/${entry}.js` };
|
|
166
185
|
}
|
|
167
186
|
|
|
168
187
|
function sortKeys(obj: Record<string, string>): Record<string, string> {
|
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,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 { 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";
|
|
11
10
|
|
|
12
11
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
13
12
|
|
|
@@ -127,7 +126,7 @@ function generatePackageJson(projectName: string, template: Template): string {
|
|
|
127
126
|
};
|
|
128
127
|
|
|
129
128
|
const deps = {
|
|
130
|
-
"@stratum-hq/lib": "
|
|
129
|
+
"@stratum-hq/lib": STRATUM_RANGES["@stratum-hq/lib"],
|
|
131
130
|
pg: "^8.11.0",
|
|
132
131
|
...frameworkDeps[template],
|
|
133
132
|
};
|
|
@@ -141,11 +140,13 @@ function generatePackageJson(projectName: string, template: Template): string {
|
|
|
141
140
|
scripts:
|
|
142
141
|
template === "nextjs"
|
|
143
142
|
? { dev: "next dev", build: "next build", start: "next start" }
|
|
144
|
-
:
|
|
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" },
|
|
145
145
|
dependencies: deps,
|
|
146
146
|
devDependencies: {
|
|
147
147
|
typescript: "^5.3.0",
|
|
148
148
|
"@types/node": "^20.11.0",
|
|
149
|
+
...(template === "nextjs" ? {} : { tsx: "^4.7.0", "@types/pg": "^8.11.0" }),
|
|
149
150
|
},
|
|
150
151
|
engines: {
|
|
151
152
|
node: ">=20.0.0",
|
|
@@ -337,9 +338,6 @@ export const config = {
|
|
|
337
338
|
}
|
|
338
339
|
|
|
339
340
|
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
341
|
return `# ${projectName}
|
|
344
342
|
|
|
345
343
|
A multi-tenant application built with [Stratum](https://github.com/stratum-hq/Stratum).
|
|
@@ -368,7 +366,7 @@ npm install
|
|
|
368
366
|
### 4. Run the app
|
|
369
367
|
|
|
370
368
|
\`\`\`bash
|
|
371
|
-
|
|
369
|
+
npm run dev
|
|
372
370
|
\`\`\`
|
|
373
371
|
|
|
374
372
|
## Project structure
|
|
@@ -379,7 +377,7 @@ ${projectName}/
|
|
|
379
377
|
├── docker-compose.yml
|
|
380
378
|
├── init.sql # DB extensions (uuid-ossp, ltree)
|
|
381
379
|
├── .env.example
|
|
382
|
-
└── package.json
|
|
380
|
+
${template === "nextjs" ? "" : "├── tsconfig.json\n"}└── package.json
|
|
383
381
|
\`\`\`
|
|
384
382
|
|
|
385
383
|
## Multi-tenancy
|
|
@@ -422,8 +420,10 @@ export function createProject(
|
|
|
422
420
|
// Server starter file
|
|
423
421
|
if (template === "express") {
|
|
424
422
|
writeFile(path.join(targetDir, "src", "index.ts"), generateExpressServer(projectName));
|
|
423
|
+
writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
|
|
425
424
|
} else if (template === "fastify") {
|
|
426
425
|
writeFile(path.join(targetDir, "src", "index.ts"), generateFastifyServer(projectName));
|
|
426
|
+
writeFile(path.join(targetDir, "tsconfig.json"), generateTsconfig(template));
|
|
427
427
|
} else if (template === "nextjs") {
|
|
428
428
|
writeFile(path.join(targetDir, "src", "app", "page.tsx"), generateNextjsPage(projectName));
|
|
429
429
|
writeFile(path.join(targetDir, "middleware.ts"), generateNextjsMiddleware());
|
|
@@ -520,25 +520,6 @@ export function main(argv: string[]): void {
|
|
|
520
520
|
if (skipInstall) {
|
|
521
521
|
console.log(" npm install");
|
|
522
522
|
}
|
|
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
|
-
}
|
|
523
|
+
console.log(" npm run dev");
|
|
537
524
|
console.log("");
|
|
538
525
|
}
|
|
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
|
+
}
|