@tulipes/core 0.1.1 → 0.1.3
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/README.md +1 -0
- package/dist/boot/app-files.js +6 -3
- package/dist/boot/app-files.js.map +1 -1
- package/dist/cli/init.d.ts +12 -0
- package/dist/cli/init.js +529 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/main.js +4 -0
- package/dist/cli/main.js.map +1 -1
- package/dist/modules/explorer.d.ts +8 -5
- package/dist/modules/explorer.js +37 -6
- package/dist/modules/explorer.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -247,6 +247,7 @@ router.get("/users/:id", async (req) => {
|
|
|
247
247
|
## The CLI
|
|
248
248
|
|
|
249
249
|
```
|
|
250
|
+
tulipes init [dir] scaffold a new application — boots with zero infrastructure
|
|
250
251
|
tulipes sync regenerate types/config.d.ts and .env.example
|
|
251
252
|
tulipes env:check validate env against every meta contract — CI gate, exit 1 on failure
|
|
252
253
|
tulipes new module <name> scaffold a module (scope auto-detected from siblings)
|
package/dist/boot/app-files.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { BootReport } from "../errors/boot-error.js";
|
|
4
|
-
import { detectSourceExtension } from "../modules/explorer.js";
|
|
5
4
|
import { importFile } from "./import-file.js";
|
|
6
5
|
export async function loadAppConfigSeed(rootDir, ctx, report) {
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
// Whichever extension exists wins (.ts first) — the app decides its own
|
|
7
|
+
// world; core's compilation state must never influence discovery.
|
|
8
|
+
const file = [".ts", ".js"]
|
|
9
|
+
.map((ext) => join(rootDir, "config", `app.config${ext}`))
|
|
10
|
+
.find((candidate) => existsSync(candidate));
|
|
11
|
+
if (!file)
|
|
9
12
|
return;
|
|
10
13
|
const exported = await importFile(file, "config", "app", report);
|
|
11
14
|
if (!exported)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-files.js","sourceRoot":"","sources":["../../src/boot/app-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"app-files.js","sourceRoot":"","sources":["../../src/boot/app-files.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AACrD,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAqB9C,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,OAAe,EACf,GAAQ,EACR,MAAkB;IAElB,wEAAwE;IACxE,kEAAkE;IAClE,MAAM,IAAI,GAAG,CAAC,KAAK,EAAE,KAAK,CAAC;SACxB,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,QAAQ,EAAE,aAAa,GAAG,EAAE,CAAC,CAAC;SACzD,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC;IAC9C,IAAI,CAAC,IAAI;QAAE,OAAO;IAElB,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAwB,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC;IACxF,IAAI,CAAC,QAAQ;QAAE,OAAO;IAEtB,IAAI,OAAO,QAAQ,CAAC,OAAO,KAAK,UAAU,EAAE,CAAC;QAC3C,MAAM,CAAC,GAAG,CAAC;YACT,KAAK,EAAE,QAAQ;YACf,OAAO,EAAE,0EAA0E;SACpF,CAAC,CAAC;QACH,OAAO;IACT,CAAC;IAED,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAO,QAAQ,CAAC,OAAuB,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QACtE,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,EAAE,IAAI,IAAI,EAAE,CAAC,CAAC;IACxC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,GAAG,CAAC;YACT,KAAK,EAAE,QAAQ;YACf,OAAO,EAAE,qBAAsB,KAAe,CAAC,OAAO,EAAE;SACzD,CAAC,CAAC;IACL,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `tulipes init [dir]` — scaffold a complete application that boots with
|
|
3
|
+
* ZERO infrastructure: the generated core module declares no MONGO_URI or
|
|
4
|
+
* REDIS_URL, and with no models and no queue files the pipeline skips both
|
|
5
|
+
* engines legitimately. `yarn install && yarn dev` must always succeed on a
|
|
6
|
+
* bare machine; databases arrive later as one meta.variables.json entry.
|
|
7
|
+
*
|
|
8
|
+
* Three modules ship: core (sys 0 — env contract, roles, request ids),
|
|
9
|
+
* security (sys 10 — headers, pino logging, body parsing) and hello (the
|
|
10
|
+
* app-tier proof route). Templates live inline, like `new module`.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runInit(rootDir: string, target?: string): Promise<void>;
|
package/dist/cli/init.js
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/**
|
|
5
|
+
* `tulipes init [dir]` — scaffold a complete application that boots with
|
|
6
|
+
* ZERO infrastructure: the generated core module declares no MONGO_URI or
|
|
7
|
+
* REDIS_URL, and with no models and no queue files the pipeline skips both
|
|
8
|
+
* engines legitimately. `yarn install && yarn dev` must always succeed on a
|
|
9
|
+
* bare machine; databases arrive later as one meta.variables.json entry.
|
|
10
|
+
*
|
|
11
|
+
* Three modules ship: core (sys 0 — env contract, roles, request ids),
|
|
12
|
+
* security (sys 10 — headers, pino logging, body parsing) and hello (the
|
|
13
|
+
* app-tier proof route). Templates live inline, like `new module`.
|
|
14
|
+
*/
|
|
15
|
+
export async function runInit(rootDir, target) {
|
|
16
|
+
const dir = resolve(rootDir, target ?? ".");
|
|
17
|
+
const name = sanitizeName(basename(dir));
|
|
18
|
+
if (existsSync(dir) && readdirSync(dir).some((entry) => entry !== ".git")) {
|
|
19
|
+
console.error(`refusing to init: ${dir} is not empty`);
|
|
20
|
+
process.exitCode = 1;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const coreVersion = ownVersion();
|
|
24
|
+
const files = renderProject(name, coreVersion);
|
|
25
|
+
for (const [relPath, content] of files) {
|
|
26
|
+
const filePath = join(dir, relPath);
|
|
27
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
28
|
+
writeFileSync(filePath, content);
|
|
29
|
+
console.log(` create ${relPath}`);
|
|
30
|
+
}
|
|
31
|
+
console.log([
|
|
32
|
+
"",
|
|
33
|
+
`Project "${name}" is ready. Next:`,
|
|
34
|
+
"",
|
|
35
|
+
target && target !== "." ? ` cd ${target}` : undefined,
|
|
36
|
+
" corepack enable # once per machine — activates the pinned yarn 4",
|
|
37
|
+
" yarn install",
|
|
38
|
+
" yarn dev",
|
|
39
|
+
"",
|
|
40
|
+
" → http://localhost:3000/hello",
|
|
41
|
+
"",
|
|
42
|
+
"No database or redis needed to boot — add MONGO_URI / REDIS_URL to",
|
|
43
|
+
"modules/core/meta.variables.json when your first model or queue lands.",
|
|
44
|
+
].filter((line) => line !== undefined).join("\n"));
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The version of the core running this CLI. Resolved relative to this file
|
|
48
|
+
* (src/cli or dist/cli — both sit two levels below the package root) since
|
|
49
|
+
* the exports map deliberately doesn't expose package.json.
|
|
50
|
+
*/
|
|
51
|
+
function ownVersion() {
|
|
52
|
+
const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "package.json");
|
|
53
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
54
|
+
return pkg.version;
|
|
55
|
+
}
|
|
56
|
+
function sanitizeName(raw) {
|
|
57
|
+
const name = raw.toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
58
|
+
return name || "tulipes-app";
|
|
59
|
+
}
|
|
60
|
+
const json = (value) => JSON.stringify(value, null, 2) + "\n";
|
|
61
|
+
function renderProject(name, coreVersion) {
|
|
62
|
+
const core = `^${coreVersion}`;
|
|
63
|
+
return [
|
|
64
|
+
["package.json", json({
|
|
65
|
+
name,
|
|
66
|
+
private: true,
|
|
67
|
+
type: "module",
|
|
68
|
+
// Pins yarn 4 through corepack; without it a global classic yarn
|
|
69
|
+
// would handle the workspaces and produce a v1 lockfile.
|
|
70
|
+
packageManager: "yarn@4.18.0",
|
|
71
|
+
workspaces: ["modules/*"],
|
|
72
|
+
scripts: {
|
|
73
|
+
dev: "tulipes dev",
|
|
74
|
+
worker: "tsx worker.ts",
|
|
75
|
+
sync: "tulipes sync",
|
|
76
|
+
check: "tulipes env:check",
|
|
77
|
+
typecheck: "tsc --noEmit",
|
|
78
|
+
},
|
|
79
|
+
dependencies: {
|
|
80
|
+
"@tulipes/core": core,
|
|
81
|
+
express: "^5",
|
|
82
|
+
},
|
|
83
|
+
devDependencies: {
|
|
84
|
+
"@types/express": "^5",
|
|
85
|
+
"@types/node": "^24",
|
|
86
|
+
tsx: "^4",
|
|
87
|
+
typescript: "^7",
|
|
88
|
+
},
|
|
89
|
+
})],
|
|
90
|
+
["tsconfig.json", json({
|
|
91
|
+
compilerOptions: {
|
|
92
|
+
target: "ES2023",
|
|
93
|
+
lib: ["ES2023"],
|
|
94
|
+
module: "NodeNext",
|
|
95
|
+
moduleResolution: "NodeNext",
|
|
96
|
+
strict: true,
|
|
97
|
+
noUncheckedIndexedAccess: true,
|
|
98
|
+
verbatimModuleSyntax: true,
|
|
99
|
+
skipLibCheck: true,
|
|
100
|
+
forceConsistentCasingInFileNames: true,
|
|
101
|
+
noEmit: true,
|
|
102
|
+
},
|
|
103
|
+
include: ["app.ts", "worker.ts", "types", "modules", "config"],
|
|
104
|
+
})],
|
|
105
|
+
[".yarnrc.yml", [
|
|
106
|
+
`nodeLinker: node-modules`,
|
|
107
|
+
``,
|
|
108
|
+
`# tsx (esbuild) and bullmq's native helpers need their install scripts;`,
|
|
109
|
+
`# stated explicitly so a restrictive global yarn config can't break dev.`,
|
|
110
|
+
`enableScripts: true`,
|
|
111
|
+
``,
|
|
112
|
+
`# Yarn's release-age gate quarantines freshly published packages, which`,
|
|
113
|
+
`# would block brand-new @tulipes releases from installing. Preapprove the`,
|
|
114
|
+
`# framework scope only — the gate stays active for everything else.`,
|
|
115
|
+
`npmPreapprovedPackages:`,
|
|
116
|
+
` - "@tulipes/*"`,
|
|
117
|
+
``,
|
|
118
|
+
].join("\n")],
|
|
119
|
+
[".gitignore", [
|
|
120
|
+
"node_modules/",
|
|
121
|
+
"dist/",
|
|
122
|
+
"*.log",
|
|
123
|
+
"",
|
|
124
|
+
"# local env overrides — the committed .envs/.env.<mode> files hold",
|
|
125
|
+
"# dev defaults; real secrets ride the container environment",
|
|
126
|
+
".env.local",
|
|
127
|
+
".envs/.env.*.local",
|
|
128
|
+
"",
|
|
129
|
+
].join("\n")],
|
|
130
|
+
["app.ts", [
|
|
131
|
+
`import { boot } from "@tulipes/core/boot";`,
|
|
132
|
+
``,
|
|
133
|
+
`await boot({ rootDir: import.meta.dirname, mode: "web" });`,
|
|
134
|
+
``,
|
|
135
|
+
].join("\n")],
|
|
136
|
+
["worker.ts", [
|
|
137
|
+
`import { boot } from "@tulipes/core/boot";`,
|
|
138
|
+
``,
|
|
139
|
+
`// Worker mode refuses to start until at least one module registers a`,
|
|
140
|
+
`// queue processor — a consumer with nothing to consume is a`,
|
|
141
|
+
`// misconfigured deployment, not a valid idle state.`,
|
|
142
|
+
`await boot({ rootDir: import.meta.dirname, mode: "worker" });`,
|
|
143
|
+
``,
|
|
144
|
+
].join("\n")],
|
|
145
|
+
["config/app.config.ts", [
|
|
146
|
+
`import type { AppConfigFn } from "@tulipes/core/boot";`,
|
|
147
|
+
``,
|
|
148
|
+
`/**`,
|
|
149
|
+
` * ── The app-wide config seed ──────────────────────────────────────────`,
|
|
150
|
+
` *`,
|
|
151
|
+
` * Runs FIRST in boot phase 6, before any module.config.ts factory, and`,
|
|
152
|
+
` * whatever it returns becomes the base of the global config. Every`,
|
|
153
|
+
` * module then reads it through \`ctx.config.<key>\`:`,
|
|
154
|
+
` *`,
|
|
155
|
+
` * export default function usersRoutes({ config }: Ctx) {`,
|
|
156
|
+
` * const router = Router();`,
|
|
157
|
+
` * router.get(\`\${config.api!.prefix}/users\`, handler);`,
|
|
158
|
+
` * }`,
|
|
159
|
+
` *`,
|
|
160
|
+
` * The framework itself reads exactly ONE field: \`app.name\`, for the`,
|
|
161
|
+
` * startup banner. Everything else below is convention — YOUR keys, read`,
|
|
162
|
+
` * by YOUR modules. Rename them, delete them, invent new ones freely;`,
|
|
163
|
+
` * nothing in core depends on them.`,
|
|
164
|
+
` *`,
|
|
165
|
+
` * Config that belongs to one feature does NOT go here — it goes in that`,
|
|
166
|
+
` * module's \`module.config.ts\`, where codegen types it automatically.`,
|
|
167
|
+
` * This file is for values two or more modules share.`,
|
|
168
|
+
` *`,
|
|
169
|
+
` * Values can be hardcoded (below) or driven by the environment. To make`,
|
|
170
|
+
` * any of them env-driven, declare the variable in a module's`,
|
|
171
|
+
` * meta.variables.json first, then read it here with`,
|
|
172
|
+
` * \`Environment.get("NAME")\` — reads of undeclared variables throw on`,
|
|
173
|
+
` * purpose, so a typo can never boot.`,
|
|
174
|
+
` */`,
|
|
175
|
+
`// \`satisfies\` (not a type annotation) so TypeScript keeps the exact`,
|
|
176
|
+
`// shape of this object for the augmentation at the bottom of the file.`,
|
|
177
|
+
`const appConfig = ((Environment) => ({`,
|
|
178
|
+
` /**`,
|
|
179
|
+
` * Identity. \`name\` is the one field core consumes — it titles the`,
|
|
180
|
+
` * startup banner ("▲ ${name}"). The rest is yours: stamp \`version\``,
|
|
181
|
+
` * into a /health payload, switch behaviour on \`env\`, etc.`,
|
|
182
|
+
` */`,
|
|
183
|
+
` app: {`,
|
|
184
|
+
` name: ${JSON.stringify(name)},`,
|
|
185
|
+
` version: "0.1.0",`,
|
|
186
|
+
` description: "A Tulipes API",`,
|
|
187
|
+
` // development | staging | production | test — resolved from APP_ENV`,
|
|
188
|
+
` env: Environment.appEnv,`,
|
|
189
|
+
` // Read a declared variable (PORT lives in modules/core/meta.variables.json)`,
|
|
190
|
+
` port: Environment.get("PORT"),`,
|
|
191
|
+
` },`,
|
|
192
|
+
``,
|
|
193
|
+
` /**`,
|
|
194
|
+
` * Route prefixing. Modules build their paths from this instead of`,
|
|
195
|
+
` * hardcoding "/api/v1", so versioning the whole API is a one-line`,
|
|
196
|
+
` * change here: \`router.get(\\\`\${config.api!.prefix}/users\\\`, …)\`.`,
|
|
197
|
+
` */`,
|
|
198
|
+
` api: {`,
|
|
199
|
+
` prefix: "/api/v1",`,
|
|
200
|
+
` // Where the docs/collection live, if you expose them`,
|
|
201
|
+
` docsPath: "/api/docs",`,
|
|
202
|
+
` },`,
|
|
203
|
+
``,
|
|
204
|
+
` /**`,
|
|
205
|
+
` * HTTP behaviour consumed by the sys security module`,
|
|
206
|
+
` * (modules/security/routes/security.routes.ts). Change the body limit`,
|
|
207
|
+
` * or CORS origins here rather than editing that file.`,
|
|
208
|
+
` */`,
|
|
209
|
+
` http: {`,
|
|
210
|
+
` // Passed to express.json({ limit }) — a parser without a cap is a`,
|
|
211
|
+
` // memory-exhaustion invitation.`,
|
|
212
|
+
` bodyLimit: "1mb",`,
|
|
213
|
+
` // Browser origins allowed to call this API. Add a cors() middleware`,
|
|
214
|
+
` // in the security module and feed it this list.`,
|
|
215
|
+
` corsOrigins: ["http://localhost:5173"],`,
|
|
216
|
+
` // Set true behind nginx/a load balancer so req.ip is the real client`,
|
|
217
|
+
` // (app.set("trust proxy", config.http!.trustProxy)).`,
|
|
218
|
+
` trustProxy: false,`,
|
|
219
|
+
` // Seconds a request may run before you abort it, if you add a timeout`,
|
|
220
|
+
` // middleware.`,
|
|
221
|
+
` requestTimeout: 30,`,
|
|
222
|
+
` },`,
|
|
223
|
+
``,
|
|
224
|
+
` /**`,
|
|
225
|
+
` * List-endpoint defaults, so every module paginates identically and`,
|
|
226
|
+
` * no caller can ask for 10 000 rows at once.`,
|
|
227
|
+
` */`,
|
|
228
|
+
` pagination: {`,
|
|
229
|
+
` defaultLimit: 20,`,
|
|
230
|
+
` maxLimit: 100,`,
|
|
231
|
+
` },`,
|
|
232
|
+
``,
|
|
233
|
+
` /**`,
|
|
234
|
+
` * Cross-cutting security knobs. Secrets themselves never live here —`,
|
|
235
|
+
` * declare them as \`"type": "secret"\` variables in meta.variables.json`,
|
|
236
|
+
` * (redacted in logs and the banner) and read them where needed.`,
|
|
237
|
+
` */`,
|
|
238
|
+
` security: {`,
|
|
239
|
+
` // Token lifetimes for an auth module, in seconds`,
|
|
240
|
+
` accessTokenTtl: 900, // 15 minutes`,
|
|
241
|
+
` refreshTokenTtl: 2_592_000, // 30 days`,
|
|
242
|
+
` // bcrypt/argon cost factor`,
|
|
243
|
+
` passwordRounds: 12,`,
|
|
244
|
+
` // Rate limiting, if you mount a limiter in the security module`,
|
|
245
|
+
` rateLimit: { windowSeconds: 60, max: 100 },`,
|
|
246
|
+
` },`,
|
|
247
|
+
``,
|
|
248
|
+
` /**`,
|
|
249
|
+
` * Public-facing URLs. Anything that builds a link a human will click —`,
|
|
250
|
+
` * password-reset emails, webhook callbacks, OAuth redirects — reads`,
|
|
251
|
+
` * these instead of guessing the host from a request.`,
|
|
252
|
+
` */`,
|
|
253
|
+
` urls: {`,
|
|
254
|
+
` api: \`http://localhost:\${Environment.get("PORT")}\`,`,
|
|
255
|
+
` frontend: "http://localhost:5173",`,
|
|
256
|
+
` },`,
|
|
257
|
+
``,
|
|
258
|
+
` /**`,
|
|
259
|
+
` * Upload constraints shared by every module that accepts files.`,
|
|
260
|
+
` */`,
|
|
261
|
+
` uploads: {`,
|
|
262
|
+
` maxSizeMb: 10,`,
|
|
263
|
+
` allowedMimeTypes: ["image/png", "image/jpeg", "application/pdf"],`,
|
|
264
|
+
` },`,
|
|
265
|
+
``,
|
|
266
|
+
` /**`,
|
|
267
|
+
` * Feature flags. Cheap way to ship dark code: a module checks`,
|
|
268
|
+
` * \`config.features!.signup\` before mounting a route. Flip per`,
|
|
269
|
+
` * environment by reading a declared boolean variable instead of a`,
|
|
270
|
+
` * literal.`,
|
|
271
|
+
` */`,
|
|
272
|
+
` features: {`,
|
|
273
|
+
` signup: true,`,
|
|
274
|
+
` maintenanceMode: false,`,
|
|
275
|
+
` },`,
|
|
276
|
+
``,
|
|
277
|
+
` /**`,
|
|
278
|
+
` * Localization defaults for modules that render text (emails, errors).`,
|
|
279
|
+
` */`,
|
|
280
|
+
` i18n: {`,
|
|
281
|
+
` defaultLocale: "en",`,
|
|
282
|
+
` supportedLocales: ["en", "fr"],`,
|
|
283
|
+
` },`,
|
|
284
|
+
`})) satisfies AppConfigFn;`,
|
|
285
|
+
``,
|
|
286
|
+
`export default appConfig;`,
|
|
287
|
+
``,
|
|
288
|
+
`/**`,
|
|
289
|
+
` * Types the keys above at every call site: \`ctx.config.api?.prefix\` is a`,
|
|
290
|
+
` * string, not \`unknown\`. \`tulipes sync\` does this automatically for`,
|
|
291
|
+
` * module.config.ts factories; the app-level seed types itself here.`,
|
|
292
|
+
` * Optional (Partial) because the config object is empty until this`,
|
|
293
|
+
` * factory has run.`,
|
|
294
|
+
` */`,
|
|
295
|
+
`declare module "@tulipes/core/config" {`,
|
|
296
|
+
` interface GlobalConfig extends Partial<ReturnType<typeof appConfig>> {}`,
|
|
297
|
+
`}`,
|
|
298
|
+
``,
|
|
299
|
+
].join("\n")],
|
|
300
|
+
[".envs/.env.development", [
|
|
301
|
+
`# Development mode — committed on purpose; values here are dev defaults.`,
|
|
302
|
+
`# Real deployments inject variables through the container environment,`,
|
|
303
|
+
`# which always wins over this file.`,
|
|
304
|
+
``,
|
|
305
|
+
`PORT=3000`,
|
|
306
|
+
`LOG_LEVEL=debug`,
|
|
307
|
+
``,
|
|
308
|
+
].join("\n")],
|
|
309
|
+
// ── modules/core — sys tier, priority 0: the very first router ─────────
|
|
310
|
+
["modules/core/package.json", json({
|
|
311
|
+
name: `@app/core`,
|
|
312
|
+
version: "0.0.0",
|
|
313
|
+
private: true,
|
|
314
|
+
type: "module",
|
|
315
|
+
tulipes: { tier: "sys", priority: 0 },
|
|
316
|
+
dependencies: { "@tulipes/core": core },
|
|
317
|
+
})],
|
|
318
|
+
["modules/core/meta.variables.json", json({
|
|
319
|
+
variables: [
|
|
320
|
+
{
|
|
321
|
+
name: "PORT",
|
|
322
|
+
type: "number",
|
|
323
|
+
group: "http",
|
|
324
|
+
description: "HTTP port the web process listens on",
|
|
325
|
+
default: 3000,
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
})],
|
|
329
|
+
["modules/core/module.acl.ts", [
|
|
330
|
+
`import type { AclBuilder } from "@tulipes/core/acl";`,
|
|
331
|
+
``,
|
|
332
|
+
`/**`,
|
|
333
|
+
` * The sys core module owns the global role vocabulary. Business modules`,
|
|
334
|
+
` * never define roles — they attach grants to these.`,
|
|
335
|
+
` */`,
|
|
336
|
+
`export default function coreAcl(acl: AclBuilder): void {`,
|
|
337
|
+
` acl.defineRole("admin").defineRole("user");`,
|
|
338
|
+
``,
|
|
339
|
+
` // admin can do everything, per resource-namespace wildcard rules.`,
|
|
340
|
+
` acl.allow("admin", "*");`,
|
|
341
|
+
`}`,
|
|
342
|
+
``,
|
|
343
|
+
].join("\n")],
|
|
344
|
+
["modules/core/routes/core.routes.ts", [
|
|
345
|
+
`import { randomUUID } from "node:crypto";`,
|
|
346
|
+
`import type { Ctx } from "@tulipes/core/boot";`,
|
|
347
|
+
``,
|
|
348
|
+
`/**`,
|
|
349
|
+
` * Global middleware lives HERE: the core module is tier "sys",`,
|
|
350
|
+
` * priority 0 — the first router of the whole pipeline — so mounting on`,
|
|
351
|
+
` * ctx.app puts this ahead of everything, security's stack included.`,
|
|
352
|
+
` */`,
|
|
353
|
+
`export default function coreRoutes({ app }: Ctx): void {`,
|
|
354
|
+
` app!.use((_req, res, next) => {`,
|
|
355
|
+
` res.setHeader("X-Request-Id", randomUUID());`,
|
|
356
|
+
` next();`,
|
|
357
|
+
` });`,
|
|
358
|
+
`}`,
|
|
359
|
+
``,
|
|
360
|
+
].join("\n")],
|
|
361
|
+
// ── modules/security — sys tier, priority 10: the hardening stack ──────
|
|
362
|
+
["modules/security/package.json", json({
|
|
363
|
+
name: `@app/security`,
|
|
364
|
+
version: "0.0.0",
|
|
365
|
+
private: true,
|
|
366
|
+
type: "module",
|
|
367
|
+
tulipes: { tier: "sys", priority: 10 },
|
|
368
|
+
dependencies: {
|
|
369
|
+
"@tulipes/core": core,
|
|
370
|
+
express: "^5",
|
|
371
|
+
pino: "^9",
|
|
372
|
+
"pino-http": "^10",
|
|
373
|
+
"pino-pretty": "^13",
|
|
374
|
+
},
|
|
375
|
+
})],
|
|
376
|
+
["modules/security/meta.variables.json", json({
|
|
377
|
+
variables: [
|
|
378
|
+
{
|
|
379
|
+
name: "LOG_LEVEL",
|
|
380
|
+
type: "enum",
|
|
381
|
+
enum: ["fatal", "error", "warn", "info", "debug", "trace"],
|
|
382
|
+
group: "logging",
|
|
383
|
+
description: "Minimum pino log level",
|
|
384
|
+
default: "info",
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
})],
|
|
388
|
+
["modules/security/helpers/security-headers.ts", [
|
|
389
|
+
`import type { RequestHandler } from "express";`,
|
|
390
|
+
``,
|
|
391
|
+
`/**`,
|
|
392
|
+
` * Baseline hardening applied to every response before anything else`,
|
|
393
|
+
` * runs. Dependency-free — swap in the full helmet suite here if this`,
|
|
394
|
+
` * app ever needs it.`,
|
|
395
|
+
` */`,
|
|
396
|
+
`export function securityHeaders(): RequestHandler {`,
|
|
397
|
+
` return (_req, res, next) => {`,
|
|
398
|
+
` res.setHeader("X-Content-Type-Options", "nosniff");`,
|
|
399
|
+
` res.setHeader("X-Frame-Options", "DENY");`,
|
|
400
|
+
` res.setHeader("Referrer-Policy", "no-referrer");`,
|
|
401
|
+
` // An API serves no active content; a restrictive CSP costs nothing`,
|
|
402
|
+
` // and defuses any HTML-ish response a bug might produce.`,
|
|
403
|
+
` res.setHeader("Content-Security-Policy", "default-src 'none'");`,
|
|
404
|
+
` next();`,
|
|
405
|
+
` };`,
|
|
406
|
+
`}`,
|
|
407
|
+
``,
|
|
408
|
+
].join("\n")],
|
|
409
|
+
["modules/security/helpers/logger.ts", [
|
|
410
|
+
`import { pino, type Logger } from "pino";`,
|
|
411
|
+
`import type { Environment } from "@tulipes/core/env";`,
|
|
412
|
+
``,
|
|
413
|
+
`let instance: Logger | undefined;`,
|
|
414
|
+
``,
|
|
415
|
+
`/**`,
|
|
416
|
+
` * The app's pino logger — JSON lines in production, pretty-printed in`,
|
|
417
|
+
` * development. Level comes from LOG_LEVEL so a deploy can turn on`,
|
|
418
|
+
` * debug without a code change.`,
|
|
419
|
+
` */`,
|
|
420
|
+
`export function logger(environment: Environment): Logger {`,
|
|
421
|
+
` if (instance) return instance;`,
|
|
422
|
+
``,
|
|
423
|
+
` instance = pino({`,
|
|
424
|
+
` level: String(environment.get("LOG_LEVEL")),`,
|
|
425
|
+
` ...(environment.appEnv === "development" && {`,
|
|
426
|
+
` transport: {`,
|
|
427
|
+
` target: "pino-pretty",`,
|
|
428
|
+
` options: { translateTime: "HH:MM:ss", ignore: "pid,hostname" },`,
|
|
429
|
+
` },`,
|
|
430
|
+
` }),`,
|
|
431
|
+
` });`,
|
|
432
|
+
` return instance;`,
|
|
433
|
+
`}`,
|
|
434
|
+
``,
|
|
435
|
+
].join("\n")],
|
|
436
|
+
["modules/security/routes/security.routes.ts", [
|
|
437
|
+
`import express from "express";`,
|
|
438
|
+
`import { pinoHttp } from "pino-http";`,
|
|
439
|
+
`import type { Ctx } from "@tulipes/core/boot";`,
|
|
440
|
+
``,
|
|
441
|
+
`import { securityHeaders } from "../helpers/security-headers.js";`,
|
|
442
|
+
`import { logger } from "../helpers/logger.js";`,
|
|
443
|
+
``,
|
|
444
|
+
`/**`,
|
|
445
|
+
` * The app's request-hardening stack. Sys tier, so this mounts ahead of`,
|
|
446
|
+
` * every app-tier router — the framework core mounts no middleware of`,
|
|
447
|
+
` * its own; this module IS the pipeline's head.`,
|
|
448
|
+
` */`,
|
|
449
|
+
`export default function securityRoutes({ app, Environment }: Ctx): void {`,
|
|
450
|
+
` app!.use(securityHeaders());`,
|
|
451
|
+
``,
|
|
452
|
+
` app!.use(`,
|
|
453
|
+
` pinoHttp({`,
|
|
454
|
+
` logger: logger(Environment),`,
|
|
455
|
+
` // The core module (priority 0) stamps X-Request-Id first — reuse`,
|
|
456
|
+
` // it so log lines and response headers tell one story.`,
|
|
457
|
+
` genReqId: (_req, res) => String(res.getHeader("X-Request-Id") ?? ""),`,
|
|
458
|
+
` serializers: {`,
|
|
459
|
+
` req: (req: { method: string; url: string }) => ({`,
|
|
460
|
+
` method: req.method,`,
|
|
461
|
+
` url: req.url,`,
|
|
462
|
+
` }),`,
|
|
463
|
+
` res: (res: { statusCode: number }) => ({ status: res.statusCode }),`,
|
|
464
|
+
` },`,
|
|
465
|
+
` }),`,
|
|
466
|
+
` );`,
|
|
467
|
+
``,
|
|
468
|
+
` // Body-size cap is a security control too — a parser without a limit`,
|
|
469
|
+
` // is a memory-exhaustion invitation.`,
|
|
470
|
+
` app!.use(express.json({ limit: "1mb" }));`,
|
|
471
|
+
`}`,
|
|
472
|
+
``,
|
|
473
|
+
].join("\n")],
|
|
474
|
+
// ── modules/hello — app tier: the proof route ──────────────────────────
|
|
475
|
+
["modules/hello/package.json", json({
|
|
476
|
+
name: `@app/hello`,
|
|
477
|
+
version: "0.0.0",
|
|
478
|
+
private: true,
|
|
479
|
+
type: "module",
|
|
480
|
+
tulipes: { tier: "app", priority: 100, dependsOn: [] },
|
|
481
|
+
dependencies: { "@tulipes/core": core, express: "^5" },
|
|
482
|
+
})],
|
|
483
|
+
["modules/hello/routes/hello.routes.ts", [
|
|
484
|
+
`import { Router } from "express";`,
|
|
485
|
+
`import type { Ctx } from "@tulipes/core/boot";`,
|
|
486
|
+
``,
|
|
487
|
+
`export default function helloRoutes({ config }: Ctx): Router {`,
|
|
488
|
+
` const router = Router();`,
|
|
489
|
+
``,
|
|
490
|
+
` router.get("/hello", (_req, res) => {`,
|
|
491
|
+
` res.json({ hello: "world", app: config.app?.name });`,
|
|
492
|
+
` });`,
|
|
493
|
+
``,
|
|
494
|
+
` return router;`,
|
|
495
|
+
`}`,
|
|
496
|
+
``,
|
|
497
|
+
].join("\n")],
|
|
498
|
+
["README.md", [
|
|
499
|
+
`# ${name}`,
|
|
500
|
+
``,
|
|
501
|
+
`A [Tulipes](https://www.npmjs.com/package/@tulipes/core) application.`,
|
|
502
|
+
``,
|
|
503
|
+
`## Run`,
|
|
504
|
+
``,
|
|
505
|
+
"```sh",
|
|
506
|
+
`corepack enable # once per machine — activates the pinned yarn 4`,
|
|
507
|
+
`yarn install`,
|
|
508
|
+
`yarn dev # boot the web process → http://localhost:3000/hello`,
|
|
509
|
+
"```",
|
|
510
|
+
``,
|
|
511
|
+
`Boots with no database and no redis: the pipeline skips engines no`,
|
|
512
|
+
`module asks for. When your first model lands, declare \`MONGO_URI\``,
|
|
513
|
+
`(type \`url\`, required) in \`modules/core/meta.variables.json\` and set`,
|
|
514
|
+
`it in \`.envs/.env.development\`; same with \`REDIS_URL\` for queues.`,
|
|
515
|
+
``,
|
|
516
|
+
`## Daily commands`,
|
|
517
|
+
``,
|
|
518
|
+
`| command | does |`,
|
|
519
|
+
`|---|---|`,
|
|
520
|
+
`| \`yarn dev\` | regenerate types, run web process under tsx watch |`,
|
|
521
|
+
`| \`yarn worker\` | run the queue-consumer process (needs a queue first) |`,
|
|
522
|
+
`| \`yarn sync\` | regenerate \`types/config.d.ts\` + \`.env.example\` |`,
|
|
523
|
+
`| \`yarn check\` | validate env against every module contract — CI gate |`,
|
|
524
|
+
`| \`yarn tulipes new module <name>\` | scaffold a module |`,
|
|
525
|
+
``,
|
|
526
|
+
].join("\n")],
|
|
527
|
+
];
|
|
528
|
+
}
|
|
529
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/cli/init.ts"],"names":[],"mappings":"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"}
|
package/dist/cli/main.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runInit } from "./init.js";
|
|
1
2
|
import { runSync } from "./sync.js";
|
|
2
3
|
import { runEnvCheck } from "./env-check.js";
|
|
3
4
|
import { runNewModule } from "./new-module.js";
|
|
@@ -10,6 +11,7 @@ import { runDev } from "./dev.js";
|
|
|
10
11
|
const HELP = `tulipes — module-based express framework
|
|
11
12
|
|
|
12
13
|
Usage:
|
|
14
|
+
tulipes init [dir] scaffold a new application (boots with zero infrastructure)
|
|
13
15
|
tulipes sync regenerate types/config.d.ts and .env.example
|
|
14
16
|
tulipes env:check validate env against every meta.variables.json (CI-friendly)
|
|
15
17
|
tulipes new module <name> scaffold a module under ./modules
|
|
@@ -19,6 +21,8 @@ export async function main(argv) {
|
|
|
19
21
|
const [command, ...rest] = argv;
|
|
20
22
|
const rootDir = process.cwd();
|
|
21
23
|
switch (command) {
|
|
24
|
+
case "init":
|
|
25
|
+
return runInit(rootDir, rest[0]);
|
|
22
26
|
case "sync":
|
|
23
27
|
return runSync(rootDir);
|
|
24
28
|
case "env:check":
|
package/dist/cli/main.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC;;;;GAIG;AACH,MAAM,IAAI,GAAG
|
|
1
|
+
{"version":3,"file":"main.js","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAC7C,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAC/C,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAElC;;;;GAIG;AACH,MAAM,IAAI,GAAG;;;;;;;;CAQZ,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,IAAI,CAAC,IAAuB;IAChD,MAAM,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;IAChC,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAE9B,QAAQ,OAAO,EAAE,CAAC;QAChB,KAAK,MAAM;YACT,OAAO,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACnC,KAAK,MAAM;YACT,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC;QAC1B,KAAK,WAAW;YACd,OAAO,WAAW,CAAC,OAAO,CAAC,CAAC;QAC9B,KAAK,KAAK;YACR,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;gBACrC,OAAO,CAAC,KAAK,CAAC,kCAAkC,CAAC,CAAC;gBAClD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;gBACrB,OAAO;YACT,CAAC;YACD,OAAO,YAAY,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;QACxC,KAAK,KAAK;YACR,OAAO,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,IAAI,QAAQ,CAAC,CAAC;QAC9C,KAAK,SAAS,CAAC;QACf,KAAK,MAAM,CAAC;QACZ,KAAK,QAAQ;YACX,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAClB,OAAO;QACT;YACE,OAAO,CAAC,KAAK,CAAC,oBAAoB,OAAO,QAAQ,IAAI,EAAE,CAAC,CAAC;YACzD,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;IACzB,CAAC;AACH,CAAC"}
|
|
@@ -12,11 +12,14 @@ import type { ModuleManifest } from "./manifest.js";
|
|
|
12
12
|
export type SourceExtension = ".ts" | ".js";
|
|
13
13
|
/**
|
|
14
14
|
* In dev the app runs straight from TypeScript (via tsx) so conventions
|
|
15
|
-
* resolve to .ts files; a compiled deployment runs .js
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
15
|
+
* resolve to .ts files; a compiled deployment runs .js. Crucially the
|
|
16
|
+
* verdict must describe the APP's files, never core's own compilation —
|
|
17
|
+
* the published core is always .js while the app it boots is usually .ts.
|
|
18
|
+
*
|
|
19
|
+
* Heuristic fallback (no module dirs to probe): the entry script's
|
|
20
|
+
* extension, then core's own file. The URL alone isn't trustworthy there:
|
|
21
|
+
* tsx's tsImport presents modules under a .js URL while only the .ts
|
|
22
|
+
* source exists on disk, so a .js verdict is confirmed against the disk.
|
|
20
23
|
*/
|
|
21
24
|
export declare function detectSourceExtension(): SourceExtension;
|
|
22
25
|
export interface ModuleFiles {
|
package/dist/modules/explorer.js
CHANGED
|
@@ -3,13 +3,19 @@ import { join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
/**
|
|
5
5
|
* In dev the app runs straight from TypeScript (via tsx) so conventions
|
|
6
|
-
* resolve to .ts files; a compiled deployment runs .js
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* resolve to .ts files; a compiled deployment runs .js. Crucially the
|
|
7
|
+
* verdict must describe the APP's files, never core's own compilation —
|
|
8
|
+
* the published core is always .js while the app it boots is usually .ts.
|
|
9
|
+
*
|
|
10
|
+
* Heuristic fallback (no module dirs to probe): the entry script's
|
|
11
|
+
* extension, then core's own file. The URL alone isn't trustworthy there:
|
|
12
|
+
* tsx's tsImport presents modules under a .js URL while only the .ts
|
|
13
|
+
* source exists on disk, so a .js verdict is confirmed against the disk.
|
|
11
14
|
*/
|
|
12
15
|
export function detectSourceExtension() {
|
|
16
|
+
const entry = process.argv[1];
|
|
17
|
+
if (entry?.endsWith(".ts") || entry?.endsWith(".mts"))
|
|
18
|
+
return ".ts";
|
|
13
19
|
const self = fileURLToPath(import.meta.url);
|
|
14
20
|
if (self.endsWith(".ts"))
|
|
15
21
|
return ".ts";
|
|
@@ -17,6 +23,31 @@ export function detectSourceExtension() {
|
|
|
17
23
|
return ".ts";
|
|
18
24
|
return ".js";
|
|
19
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The authoritative verdict: look at what the modules actually contain.
|
|
28
|
+
* Only when a project has conventional files in BOTH extensions (or none
|
|
29
|
+
* at all) does the runtime heuristic break the tie.
|
|
30
|
+
*/
|
|
31
|
+
function probeSourceExtension(modules) {
|
|
32
|
+
let sawTs = false;
|
|
33
|
+
let sawJs = false;
|
|
34
|
+
for (const module of modules) {
|
|
35
|
+
for (const ext of [".ts", ".js"]) {
|
|
36
|
+
const hit = existsSync(join(module.dir, `module.config${ext}`)) ||
|
|
37
|
+
existsSync(join(module.dir, `module.acl${ext}`)) ||
|
|
38
|
+
COLLECTIONS.some(([, dirName, suffix]) => listWithSuffix(join(module.dir, dirName), suffix + ext).length > 0);
|
|
39
|
+
if (ext === ".ts")
|
|
40
|
+
sawTs ||= hit;
|
|
41
|
+
else
|
|
42
|
+
sawJs ||= hit;
|
|
43
|
+
}
|
|
44
|
+
if (sawTs && sawJs)
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
if (sawTs !== sawJs)
|
|
48
|
+
return sawTs ? ".ts" : ".js";
|
|
49
|
+
return detectSourceExtension();
|
|
50
|
+
}
|
|
20
51
|
/** dir name → file suffix, e.g. routes/users.routes.ts */
|
|
21
52
|
const COLLECTIONS = [
|
|
22
53
|
["routeFiles", "routes", ".routes"],
|
|
@@ -26,7 +57,7 @@ const COLLECTIONS = [
|
|
|
26
57
|
["socketFiles", "sockets", ".sockets"],
|
|
27
58
|
];
|
|
28
59
|
export function exploreModules(modules, options = {}) {
|
|
29
|
-
const ext = options.sourceExtension ??
|
|
60
|
+
const ext = options.sourceExtension ?? probeSourceExtension(modules);
|
|
30
61
|
return modules.map((module) => {
|
|
31
62
|
const files = {
|
|
32
63
|
module,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"explorer.js","sourceRoot":"","sources":["../../src/modules/explorer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAClD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAiBzC
|
|
1
|
+
{"version":3,"file":"explorer.js","sourceRoot":"","sources":["../../src/modules/explorer.ts"],"names":[],"mappings":"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"}
|