create-meridian-app 0.1.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/dist/chunk-7IUTX6RW.js +468 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +363 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/package.json +40 -0
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
// src/commands/new.ts
|
|
2
|
+
import path2 from "path";
|
|
3
|
+
import { existsSync as existsSync2 } from "fs";
|
|
4
|
+
import ora from "ora";
|
|
5
|
+
import chalk from "chalk";
|
|
6
|
+
import prompts from "prompts";
|
|
7
|
+
import { execa } from "execa";
|
|
8
|
+
|
|
9
|
+
// src/templates/index.ts
|
|
10
|
+
function renderPackageJson(vars) {
|
|
11
|
+
return JSON.stringify(
|
|
12
|
+
{
|
|
13
|
+
name: vars.name,
|
|
14
|
+
version: "0.1.0",
|
|
15
|
+
private: true,
|
|
16
|
+
type: "module",
|
|
17
|
+
scripts: {
|
|
18
|
+
dev: "meridian dev",
|
|
19
|
+
build: "meridian build",
|
|
20
|
+
start: "node --import tsx/esm src/main.ts",
|
|
21
|
+
"db:migrate": "meridian db:migrate",
|
|
22
|
+
"db:generate": "meridian db:generate"
|
|
23
|
+
},
|
|
24
|
+
dependencies: {
|
|
25
|
+
"@meridianjs/framework": "latest",
|
|
26
|
+
"@meridianjs/framework-utils": "latest",
|
|
27
|
+
"@meridianjs/types": "latest",
|
|
28
|
+
"@meridianjs/event-bus-local": "latest",
|
|
29
|
+
"@meridianjs/user": "latest",
|
|
30
|
+
"@meridianjs/workspace": "latest",
|
|
31
|
+
"@meridianjs/auth": "latest",
|
|
32
|
+
"@meridianjs/project": "latest",
|
|
33
|
+
"@meridianjs/issue": "latest",
|
|
34
|
+
"dotenv": "^16.0.0"
|
|
35
|
+
},
|
|
36
|
+
devDependencies: {
|
|
37
|
+
"create-meridian-app": "latest",
|
|
38
|
+
typescript: "^5.4.0",
|
|
39
|
+
tsx: "^4.0.0",
|
|
40
|
+
"@types/node": "^22.0.0"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
null,
|
|
44
|
+
2
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
function renderTsConfig() {
|
|
48
|
+
return JSON.stringify(
|
|
49
|
+
{
|
|
50
|
+
compilerOptions: {
|
|
51
|
+
target: "ES2022",
|
|
52
|
+
module: "NodeNext",
|
|
53
|
+
moduleResolution: "NodeNext",
|
|
54
|
+
lib: ["ES2022"],
|
|
55
|
+
outDir: "dist",
|
|
56
|
+
rootDir: "src",
|
|
57
|
+
strict: true,
|
|
58
|
+
esModuleInterop: true,
|
|
59
|
+
skipLibCheck: true,
|
|
60
|
+
resolveJsonModule: true,
|
|
61
|
+
declaration: true,
|
|
62
|
+
declarationMap: true,
|
|
63
|
+
sourceMap: true
|
|
64
|
+
},
|
|
65
|
+
include: ["src/**/*"],
|
|
66
|
+
exclude: ["node_modules", "dist"]
|
|
67
|
+
},
|
|
68
|
+
null,
|
|
69
|
+
2
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
function renderMeridianConfig(vars) {
|
|
73
|
+
return `import { defineConfig } from "@meridianjs/framework"
|
|
74
|
+
import dotenv from "dotenv"
|
|
75
|
+
dotenv.config()
|
|
76
|
+
|
|
77
|
+
export default defineConfig({
|
|
78
|
+
projectConfig: {
|
|
79
|
+
databaseUrl: process.env.DATABASE_URL ?? "${vars.databaseUrl}",
|
|
80
|
+
httpPort: Number(process.env.PORT) || ${vars.httpPort},
|
|
81
|
+
jwtSecret: process.env.JWT_SECRET ?? "changeme-replace-in-production",
|
|
82
|
+
},
|
|
83
|
+
modules: [
|
|
84
|
+
{ resolve: "@meridianjs/event-bus-local" },
|
|
85
|
+
{ resolve: "@meridianjs/user" },
|
|
86
|
+
{ resolve: "@meridianjs/workspace" },
|
|
87
|
+
{ resolve: "@meridianjs/auth" },
|
|
88
|
+
{ resolve: "@meridianjs/project" },
|
|
89
|
+
{ resolve: "@meridianjs/issue" },
|
|
90
|
+
],
|
|
91
|
+
})
|
|
92
|
+
`;
|
|
93
|
+
}
|
|
94
|
+
function renderMainTs() {
|
|
95
|
+
return `import { bootstrap } from "@meridianjs/framework"
|
|
96
|
+
import { fileURLToPath } from "node:url"
|
|
97
|
+
import path from "node:path"
|
|
98
|
+
|
|
99
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
100
|
+
const rootDir = path.resolve(__dirname, "..")
|
|
101
|
+
|
|
102
|
+
const app = await bootstrap({ rootDir })
|
|
103
|
+
await app.start()
|
|
104
|
+
|
|
105
|
+
process.on("SIGTERM", async () => {
|
|
106
|
+
await app.stop()
|
|
107
|
+
process.exit(0)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
process.on("SIGINT", async () => {
|
|
111
|
+
await app.stop()
|
|
112
|
+
process.exit(0)
|
|
113
|
+
})
|
|
114
|
+
`;
|
|
115
|
+
}
|
|
116
|
+
function renderMiddlewares() {
|
|
117
|
+
return `import { authenticateJWT } from "@meridianjs/auth"
|
|
118
|
+
|
|
119
|
+
export default {
|
|
120
|
+
routes: [
|
|
121
|
+
{ matcher: "/admin", middlewares: [authenticateJWT] },
|
|
122
|
+
],
|
|
123
|
+
}
|
|
124
|
+
`;
|
|
125
|
+
}
|
|
126
|
+
function renderHelloRoute() {
|
|
127
|
+
return `import type { Request, Response } from "express"
|
|
128
|
+
|
|
129
|
+
export const GET = async (_req: Request, res: Response) => {
|
|
130
|
+
res.json({ message: "Hello from Meridian!", timestamp: new Date().toISOString() })
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
}
|
|
134
|
+
function renderGitIgnore() {
|
|
135
|
+
return `# Dependencies
|
|
136
|
+
node_modules/
|
|
137
|
+
|
|
138
|
+
# Build output
|
|
139
|
+
dist/
|
|
140
|
+
|
|
141
|
+
# Environment variables
|
|
142
|
+
.env
|
|
143
|
+
.env.local
|
|
144
|
+
.env.*.local
|
|
145
|
+
|
|
146
|
+
# Logs
|
|
147
|
+
*.log
|
|
148
|
+
npm-debug.log*
|
|
149
|
+
|
|
150
|
+
# Editor
|
|
151
|
+
.vscode/
|
|
152
|
+
.idea/
|
|
153
|
+
*.swp
|
|
154
|
+
|
|
155
|
+
# OS
|
|
156
|
+
.DS_Store
|
|
157
|
+
Thumbs.db
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
function renderEnvExample(vars) {
|
|
161
|
+
return `# Copy this file to .env and fill in your values
|
|
162
|
+
DATABASE_URL=${vars.databaseUrl}
|
|
163
|
+
PORT=${vars.httpPort}
|
|
164
|
+
JWT_SECRET=changeme-replace-in-production
|
|
165
|
+
`;
|
|
166
|
+
}
|
|
167
|
+
function renderReadme(vars) {
|
|
168
|
+
return `# ${vars.name}
|
|
169
|
+
|
|
170
|
+
A Meridian application.
|
|
171
|
+
|
|
172
|
+
## Getting started
|
|
173
|
+
|
|
174
|
+
\`\`\`bash
|
|
175
|
+
# Install dependencies
|
|
176
|
+
npm install
|
|
177
|
+
|
|
178
|
+
# Set up your database
|
|
179
|
+
cp .env.example .env
|
|
180
|
+
# Edit .env with your database URL
|
|
181
|
+
|
|
182
|
+
# Sync database schema
|
|
183
|
+
npm run db:migrate
|
|
184
|
+
|
|
185
|
+
# Start the development server
|
|
186
|
+
npm run dev
|
|
187
|
+
\`\`\`
|
|
188
|
+
|
|
189
|
+
## Commands
|
|
190
|
+
|
|
191
|
+
| Command | Description |
|
|
192
|
+
|---|---|
|
|
193
|
+
| \`npm run dev\` | Start development server with hot reload |
|
|
194
|
+
| \`npm run build\` | Type-check the project |
|
|
195
|
+
| \`npm run db:migrate\` | Synchronize the database schema |
|
|
196
|
+
| \`npm run db:generate <name>\` | Generate a new migration file |
|
|
197
|
+
|
|
198
|
+
## Project structure
|
|
199
|
+
|
|
200
|
+
\`\`\`
|
|
201
|
+
src/
|
|
202
|
+
main.ts Entry point
|
|
203
|
+
api/
|
|
204
|
+
middlewares.ts Route-level middleware configuration
|
|
205
|
+
admin/ File-based API routes
|
|
206
|
+
modules/ Custom domain modules
|
|
207
|
+
workflows/ DAG workflows with compensation
|
|
208
|
+
subscribers/ Event subscribers
|
|
209
|
+
jobs/ Scheduled background jobs
|
|
210
|
+
links/ Cross-module link definitions
|
|
211
|
+
\`\`\`
|
|
212
|
+
|
|
213
|
+
## Extending Meridian
|
|
214
|
+
|
|
215
|
+
See the [Meridian documentation](https://github.com/meridian/meridian) for guides on:
|
|
216
|
+
- Creating custom modules
|
|
217
|
+
- Building workflows
|
|
218
|
+
- Writing event subscribers
|
|
219
|
+
- Scheduling background jobs
|
|
220
|
+
- Building plugins
|
|
221
|
+
`;
|
|
222
|
+
}
|
|
223
|
+
function renderModuleIndex(name, pascalName) {
|
|
224
|
+
return `import { Module } from "@meridianjs/framework-utils"
|
|
225
|
+
import { ${pascalName}ModuleService } from "./service.js"
|
|
226
|
+
import { ${pascalName} } from "./models/${name}.js"
|
|
227
|
+
import defaultLoader from "./loaders/default.js"
|
|
228
|
+
|
|
229
|
+
export default Module("${name}ModuleService", {
|
|
230
|
+
service: ${pascalName}ModuleService,
|
|
231
|
+
models: [${pascalName}],
|
|
232
|
+
loaders: [defaultLoader],
|
|
233
|
+
linkable: {
|
|
234
|
+
${name}: { tableName: "${name}", primaryKey: "id" },
|
|
235
|
+
},
|
|
236
|
+
})
|
|
237
|
+
`;
|
|
238
|
+
}
|
|
239
|
+
function renderModuleLoader(name, pascalName) {
|
|
240
|
+
return `import { createModuleOrm, createRepository, dmlToEntitySchema } from "@meridianjs/framework-utils"
|
|
241
|
+
import type { LoaderOptions } from "@meridianjs/types"
|
|
242
|
+
import { ${pascalName} } from "../models/${name}.js"
|
|
243
|
+
|
|
244
|
+
const ${pascalName}Schema = dmlToEntitySchema(${pascalName})
|
|
245
|
+
|
|
246
|
+
export default async function defaultLoader({ container }: LoaderOptions): Promise<void> {
|
|
247
|
+
const config = container.resolve("config") as any
|
|
248
|
+
const orm = await createModuleOrm(
|
|
249
|
+
[${pascalName}Schema],
|
|
250
|
+
config.projectConfig.databaseUrl
|
|
251
|
+
)
|
|
252
|
+
const em = orm.em.fork()
|
|
253
|
+
container.register({
|
|
254
|
+
${name}Repository: createRepository(em, "${name}"),
|
|
255
|
+
${name}Orm: orm,
|
|
256
|
+
})
|
|
257
|
+
}
|
|
258
|
+
`;
|
|
259
|
+
}
|
|
260
|
+
function renderModuleModel(name, pascalName) {
|
|
261
|
+
return `import { model } from "@meridianjs/framework-utils"
|
|
262
|
+
|
|
263
|
+
export const ${pascalName} = model("${name}", {
|
|
264
|
+
id: model.id(),
|
|
265
|
+
name: model.text(),
|
|
266
|
+
created_at: model.dateTime(),
|
|
267
|
+
updated_at: model.dateTime(),
|
|
268
|
+
})
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
271
|
+
function renderModuleService(name, pascalName) {
|
|
272
|
+
return `import { MeridianService } from "@meridianjs/framework-utils"
|
|
273
|
+
import type { MeridianContainer } from "@meridianjs/types"
|
|
274
|
+
import { ${pascalName} } from "./models/${name}.js"
|
|
275
|
+
|
|
276
|
+
export class ${pascalName}ModuleService extends MeridianService({ ${pascalName} }) {
|
|
277
|
+
constructor(container: MeridianContainer) {
|
|
278
|
+
super(container)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Add custom service methods here
|
|
282
|
+
}
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
function renderWorkflow(name, pascalName) {
|
|
286
|
+
const camelName = pascalName.charAt(0).toLowerCase() + pascalName.slice(1);
|
|
287
|
+
return `import { createStep, createWorkflow, WorkflowResponse } from "@meridianjs/workflow-engine"
|
|
288
|
+
import type { MeridianContainer } from "@meridianjs/types"
|
|
289
|
+
|
|
290
|
+
interface ${pascalName}WorkflowInput {
|
|
291
|
+
// Define your input shape here
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const ${camelName}Step = createStep(
|
|
295
|
+
"${name}-step",
|
|
296
|
+
async (input: ${pascalName}WorkflowInput, { container }: { container: MeridianContainer }) => {
|
|
297
|
+
// Forward logic: implement your step here
|
|
298
|
+
return { result: null }
|
|
299
|
+
},
|
|
300
|
+
async (_input: ${pascalName}WorkflowInput, _context: { container: MeridianContainer }) => {
|
|
301
|
+
// Compensation logic: runs if a later step fails
|
|
302
|
+
}
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
export const ${camelName}Workflow = createWorkflow(
|
|
306
|
+
"${name}",
|
|
307
|
+
(input: ${pascalName}WorkflowInput) => {
|
|
308
|
+
const result = ${camelName}Step(input)
|
|
309
|
+
return new WorkflowResponse(result)
|
|
310
|
+
}
|
|
311
|
+
)
|
|
312
|
+
`;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/utils.ts
|
|
316
|
+
import path from "path";
|
|
317
|
+
import fs from "fs/promises";
|
|
318
|
+
import { existsSync } from "fs";
|
|
319
|
+
function toPascalCase(str) {
|
|
320
|
+
return str.replace(/[-_](.)/g, (_, c) => c.toUpperCase()).replace(/^(.)/, (_, c) => c.toUpperCase());
|
|
321
|
+
}
|
|
322
|
+
function toKebabCase(str) {
|
|
323
|
+
return str.replace(/([A-Z])/g, "-$1").toLowerCase().replace(/^-/, "");
|
|
324
|
+
}
|
|
325
|
+
async function writeFile(filePath, content) {
|
|
326
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
327
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
328
|
+
}
|
|
329
|
+
async function mkdir(dirPath) {
|
|
330
|
+
await fs.mkdir(dirPath, { recursive: true });
|
|
331
|
+
}
|
|
332
|
+
async function mkdirWithKeep(dirPath) {
|
|
333
|
+
await mkdir(dirPath);
|
|
334
|
+
await writeFile(path.join(dirPath, ".gitkeep"), "");
|
|
335
|
+
}
|
|
336
|
+
function findProjectRoot(startDir = process.cwd()) {
|
|
337
|
+
let dir = startDir;
|
|
338
|
+
while (true) {
|
|
339
|
+
if (existsSync(path.join(dir, "meridian.config.ts"))) {
|
|
340
|
+
return dir;
|
|
341
|
+
}
|
|
342
|
+
const parent = path.dirname(dir);
|
|
343
|
+
if (parent === dir) return null;
|
|
344
|
+
dir = parent;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// src/commands/new.ts
|
|
349
|
+
async function runNew(projectName) {
|
|
350
|
+
console.log();
|
|
351
|
+
console.log(chalk.bold(" Create Meridian App"));
|
|
352
|
+
console.log();
|
|
353
|
+
let name = projectName;
|
|
354
|
+
if (!name) {
|
|
355
|
+
const res = await prompts(
|
|
356
|
+
{
|
|
357
|
+
type: "text",
|
|
358
|
+
name: "name",
|
|
359
|
+
message: "Project name",
|
|
360
|
+
initial: "my-meridian-app",
|
|
361
|
+
validate: (v) => /^[a-z0-9-_]+$/.test(v) || "Use lowercase letters, numbers, hyphens, and underscores only"
|
|
362
|
+
},
|
|
363
|
+
{ onCancel: () => process.exit(0) }
|
|
364
|
+
);
|
|
365
|
+
name = res.name;
|
|
366
|
+
}
|
|
367
|
+
const targetDir = path2.resolve(process.cwd(), name);
|
|
368
|
+
if (existsSync2(targetDir)) {
|
|
369
|
+
const { overwrite } = await prompts(
|
|
370
|
+
{
|
|
371
|
+
type: "confirm",
|
|
372
|
+
name: "overwrite",
|
|
373
|
+
message: `Directory "${name}" already exists. Continue anyway?`,
|
|
374
|
+
initial: false
|
|
375
|
+
},
|
|
376
|
+
{ onCancel: () => process.exit(0) }
|
|
377
|
+
);
|
|
378
|
+
if (!overwrite) {
|
|
379
|
+
console.log(chalk.yellow(" Cancelled."));
|
|
380
|
+
process.exit(0);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const answers = await prompts(
|
|
384
|
+
[
|
|
385
|
+
{
|
|
386
|
+
type: "text",
|
|
387
|
+
name: "databaseUrl",
|
|
388
|
+
message: "Database URL",
|
|
389
|
+
initial: `postgresql://postgres:postgres@localhost:5432/${name}`
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
type: "number",
|
|
393
|
+
name: "httpPort",
|
|
394
|
+
message: "HTTP port",
|
|
395
|
+
initial: 9e3
|
|
396
|
+
},
|
|
397
|
+
{
|
|
398
|
+
type: "confirm",
|
|
399
|
+
name: "installDeps",
|
|
400
|
+
message: "Install dependencies now?",
|
|
401
|
+
initial: true
|
|
402
|
+
}
|
|
403
|
+
],
|
|
404
|
+
{ onCancel: () => process.exit(0) }
|
|
405
|
+
);
|
|
406
|
+
const vars = {
|
|
407
|
+
name,
|
|
408
|
+
databaseUrl: answers.databaseUrl,
|
|
409
|
+
httpPort: answers.httpPort
|
|
410
|
+
};
|
|
411
|
+
const spinner = ora("Scaffolding project...").start();
|
|
412
|
+
try {
|
|
413
|
+
await writeFile(path2.join(targetDir, "package.json"), renderPackageJson(vars));
|
|
414
|
+
await writeFile(path2.join(targetDir, "tsconfig.json"), renderTsConfig());
|
|
415
|
+
await writeFile(path2.join(targetDir, "meridian.config.ts"), renderMeridianConfig(vars));
|
|
416
|
+
await writeFile(path2.join(targetDir, ".gitignore"), renderGitIgnore());
|
|
417
|
+
await writeFile(path2.join(targetDir, ".env.example"), renderEnvExample(vars));
|
|
418
|
+
await writeFile(path2.join(targetDir, "README.md"), renderReadme(vars));
|
|
419
|
+
await writeFile(path2.join(targetDir, "src", "main.ts"), renderMainTs());
|
|
420
|
+
await writeFile(path2.join(targetDir, "src", "api", "middlewares.ts"), renderMiddlewares());
|
|
421
|
+
await writeFile(
|
|
422
|
+
path2.join(targetDir, "src", "api", "admin", "hello", "route.ts"),
|
|
423
|
+
renderHelloRoute()
|
|
424
|
+
);
|
|
425
|
+
await mkdirWithKeep(path2.join(targetDir, "src", "modules"));
|
|
426
|
+
await mkdirWithKeep(path2.join(targetDir, "src", "workflows"));
|
|
427
|
+
await mkdirWithKeep(path2.join(targetDir, "src", "subscribers"));
|
|
428
|
+
await mkdirWithKeep(path2.join(targetDir, "src", "jobs"));
|
|
429
|
+
await mkdirWithKeep(path2.join(targetDir, "src", "links"));
|
|
430
|
+
spinner.succeed("Scaffolded project files");
|
|
431
|
+
} catch (err) {
|
|
432
|
+
spinner.fail("Failed to scaffold project files");
|
|
433
|
+
throw err;
|
|
434
|
+
}
|
|
435
|
+
if (answers.installDeps) {
|
|
436
|
+
const installSpinner = ora("Installing dependencies...").start();
|
|
437
|
+
try {
|
|
438
|
+
await execa("npm", ["install"], { cwd: targetDir, stdio: "pipe" });
|
|
439
|
+
installSpinner.succeed("Dependencies installed");
|
|
440
|
+
} catch (err) {
|
|
441
|
+
installSpinner.warn("npm install failed \u2014 run it manually");
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
console.log();
|
|
445
|
+
console.log(chalk.green(" \u2713 Project created!"));
|
|
446
|
+
console.log();
|
|
447
|
+
console.log(` ${chalk.dim("cd")} ${chalk.cyan(name)}`);
|
|
448
|
+
if (!answers.installDeps) {
|
|
449
|
+
console.log(` ${chalk.dim("npm install")}`);
|
|
450
|
+
}
|
|
451
|
+
console.log(` ${chalk.dim("cp")} .env.example .env ${chalk.dim("# fill in your DATABASE_URL")}`);
|
|
452
|
+
console.log(` ${chalk.cyan("npm run db:migrate")}`);
|
|
453
|
+
console.log(` ${chalk.cyan("npm run dev")}`);
|
|
454
|
+
console.log();
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
export {
|
|
458
|
+
renderModuleIndex,
|
|
459
|
+
renderModuleLoader,
|
|
460
|
+
renderModuleModel,
|
|
461
|
+
renderModuleService,
|
|
462
|
+
renderWorkflow,
|
|
463
|
+
toPascalCase,
|
|
464
|
+
toKebabCase,
|
|
465
|
+
writeFile,
|
|
466
|
+
findProjectRoot,
|
|
467
|
+
runNew
|
|
468
|
+
};
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
findProjectRoot,
|
|
4
|
+
renderModuleIndex,
|
|
5
|
+
renderModuleLoader,
|
|
6
|
+
renderModuleModel,
|
|
7
|
+
renderModuleService,
|
|
8
|
+
renderWorkflow,
|
|
9
|
+
runNew,
|
|
10
|
+
toKebabCase,
|
|
11
|
+
toPascalCase,
|
|
12
|
+
writeFile
|
|
13
|
+
} from "./chunk-7IUTX6RW.js";
|
|
14
|
+
|
|
15
|
+
// src/cli.ts
|
|
16
|
+
import { Command } from "commander";
|
|
17
|
+
|
|
18
|
+
// src/commands/dev.ts
|
|
19
|
+
import path from "path";
|
|
20
|
+
import { existsSync } from "fs";
|
|
21
|
+
import chalk from "chalk";
|
|
22
|
+
import { execa } from "execa";
|
|
23
|
+
async function runDev() {
|
|
24
|
+
const rootDir = findProjectRoot();
|
|
25
|
+
if (!rootDir) {
|
|
26
|
+
console.error(chalk.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const mainTs = path.join(rootDir, "src", "main.ts");
|
|
30
|
+
if (!existsSync(mainTs)) {
|
|
31
|
+
console.error(chalk.red(` \u2716 Entry point not found: ${mainTs}`));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
console.log(chalk.dim(` \u2192 Starting dev server from ${rootDir}`));
|
|
35
|
+
console.log();
|
|
36
|
+
const result = await execa(
|
|
37
|
+
"node",
|
|
38
|
+
["--import", "tsx/esm", mainTs],
|
|
39
|
+
{
|
|
40
|
+
cwd: rootDir,
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
env: {
|
|
43
|
+
...process.env,
|
|
44
|
+
NODE_ENV: process.env.NODE_ENV ?? "development",
|
|
45
|
+
FORCE_COLOR: "1"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
).catch((err) => {
|
|
49
|
+
if (err.signal === "SIGINT" || err.signal === "SIGTERM") {
|
|
50
|
+
process.exit(0);
|
|
51
|
+
}
|
|
52
|
+
throw err;
|
|
53
|
+
});
|
|
54
|
+
process.exit(result.exitCode ?? 0);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/commands/build.ts
|
|
58
|
+
import chalk2 from "chalk";
|
|
59
|
+
import { execa as execa2 } from "execa";
|
|
60
|
+
async function runBuild() {
|
|
61
|
+
const rootDir = findProjectRoot();
|
|
62
|
+
if (!rootDir) {
|
|
63
|
+
console.error(chalk2.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
console.log(chalk2.dim(" \u2192 Type-checking project..."));
|
|
67
|
+
console.log();
|
|
68
|
+
const result = await execa2("npx", ["tsc", "--noEmit"], {
|
|
69
|
+
cwd: rootDir,
|
|
70
|
+
stdio: "inherit"
|
|
71
|
+
}).catch((err) => err);
|
|
72
|
+
if (result.exitCode !== 0) {
|
|
73
|
+
console.log();
|
|
74
|
+
console.error(chalk2.red(" \u2716 Type check failed"));
|
|
75
|
+
process.exit(result.exitCode ?? 1);
|
|
76
|
+
}
|
|
77
|
+
console.log();
|
|
78
|
+
console.log(chalk2.green(" \u2713 Type check passed"));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// src/commands/db-migrate.ts
|
|
82
|
+
import path2 from "path";
|
|
83
|
+
import chalk3 from "chalk";
|
|
84
|
+
import ora from "ora";
|
|
85
|
+
import { execa as execa3 } from "execa";
|
|
86
|
+
async function runDbMigrate() {
|
|
87
|
+
const rootDir = findProjectRoot();
|
|
88
|
+
if (!rootDir) {
|
|
89
|
+
console.error(chalk3.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
const spinner = ora("Synchronizing database schema...").start();
|
|
93
|
+
const script = `
|
|
94
|
+
import { bootstrap } from "@meridianjs/framework"
|
|
95
|
+
import { fileURLToPath } from "node:url"
|
|
96
|
+
import path from "node:path"
|
|
97
|
+
|
|
98
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
99
|
+
|
|
100
|
+
const app = await bootstrap({ rootDir: ${JSON.stringify(rootDir)} })
|
|
101
|
+
// Schema sync already ran during module loading above.
|
|
102
|
+
// Don't start the HTTP server \u2014 just close all connections.
|
|
103
|
+
await app.stop()
|
|
104
|
+
process.exit(0)
|
|
105
|
+
`;
|
|
106
|
+
const scriptPath = path2.join(rootDir, ".meridian-migrate-tmp.mjs");
|
|
107
|
+
const { writeFile: writeFile2, unlink } = await import("fs/promises");
|
|
108
|
+
await writeFile2(scriptPath, script, "utf-8");
|
|
109
|
+
try {
|
|
110
|
+
await execa3("node", ["--import", "tsx/esm", scriptPath], {
|
|
111
|
+
cwd: rootDir,
|
|
112
|
+
stdio: "pipe",
|
|
113
|
+
env: { ...process.env, NODE_ENV: "development" }
|
|
114
|
+
});
|
|
115
|
+
spinner.succeed("Database schema synchronized");
|
|
116
|
+
} catch (err) {
|
|
117
|
+
spinner.fail("Schema migration failed");
|
|
118
|
+
console.error();
|
|
119
|
+
if (err.stderr) console.error(chalk3.red(err.stderr));
|
|
120
|
+
if (err.stdout) console.error(err.stdout);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
} finally {
|
|
123
|
+
await unlink(scriptPath).catch(() => null);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// src/commands/db-generate.ts
|
|
128
|
+
import path3 from "path";
|
|
129
|
+
import fs from "fs/promises";
|
|
130
|
+
import chalk4 from "chalk";
|
|
131
|
+
async function runDbGenerate(migrationName) {
|
|
132
|
+
const rootDir = findProjectRoot();
|
|
133
|
+
if (!rootDir) {
|
|
134
|
+
console.error(chalk4.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
|
|
135
|
+
process.exit(1);
|
|
136
|
+
}
|
|
137
|
+
if (!migrationName) {
|
|
138
|
+
console.error(chalk4.red(" \u2716 Migration name is required. Usage: meridian db:generate <name>"));
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
const safeName = migrationName.toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_");
|
|
142
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
|
|
143
|
+
const fileName = `${timestamp}_${safeName}.ts`;
|
|
144
|
+
const migrationsDir = path3.join(rootDir, "src", "migrations");
|
|
145
|
+
await fs.mkdir(migrationsDir, { recursive: true });
|
|
146
|
+
const content = `/**
|
|
147
|
+
* Migration: ${safeName}
|
|
148
|
+
* Generated at: ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
149
|
+
*
|
|
150
|
+
* This file is a placeholder. Meridian uses MikroORM's updateSchema() by default.
|
|
151
|
+
* For custom migration SQL, implement the up() and down() methods below and
|
|
152
|
+
* run them via a custom script calling \`em.getMigrator().up()\`.
|
|
153
|
+
*/
|
|
154
|
+
|
|
155
|
+
export async function up(): Promise<void> {
|
|
156
|
+
// Write your migration SQL here
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function down(): Promise<void> {
|
|
160
|
+
// Write your rollback SQL here
|
|
161
|
+
}
|
|
162
|
+
`;
|
|
163
|
+
const filePath = path3.join(migrationsDir, fileName);
|
|
164
|
+
await fs.writeFile(filePath, content, "utf-8");
|
|
165
|
+
console.log(chalk4.green(` \u2713 Created migration: src/migrations/${fileName}`));
|
|
166
|
+
console.log();
|
|
167
|
+
console.log(chalk4.dim(" Note: Meridian auto-syncs schema on startup via updateSchema()."));
|
|
168
|
+
console.log(chalk4.dim(" Use this file for custom DDL that requires manual control."));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/commands/generate/module.ts
|
|
172
|
+
import path4 from "path";
|
|
173
|
+
import { existsSync as existsSync2 } from "fs";
|
|
174
|
+
import chalk5 from "chalk";
|
|
175
|
+
async function generateModule(name) {
|
|
176
|
+
const rootDir = findProjectRoot();
|
|
177
|
+
if (!rootDir) {
|
|
178
|
+
console.error(chalk5.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
if (!name) {
|
|
182
|
+
console.error(chalk5.red(" \u2716 Module name is required. Usage: meridian generate module <name>"));
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
const kebab = toKebabCase(name);
|
|
186
|
+
const pascal = toPascalCase(kebab);
|
|
187
|
+
const moduleDir = path4.join(rootDir, "src", "modules", kebab);
|
|
188
|
+
if (existsSync2(moduleDir)) {
|
|
189
|
+
console.error(chalk5.red(` \u2716 Module directory already exists: src/modules/${kebab}`));
|
|
190
|
+
process.exit(1);
|
|
191
|
+
}
|
|
192
|
+
await writeFile(path4.join(moduleDir, "index.ts"), renderModuleIndex(kebab, pascal));
|
|
193
|
+
await writeFile(path4.join(moduleDir, `models/${kebab}.ts`), renderModuleModel(kebab, pascal));
|
|
194
|
+
await writeFile(path4.join(moduleDir, `loaders/default.ts`), renderModuleLoader(kebab, pascal));
|
|
195
|
+
await writeFile(path4.join(moduleDir, "service.ts"), renderModuleService(kebab, pascal));
|
|
196
|
+
console.log(chalk5.green(` \u2713 Generated module: src/modules/${kebab}/`));
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(" Files created:");
|
|
199
|
+
console.log(chalk5.dim(` src/modules/${kebab}/index.ts`));
|
|
200
|
+
console.log(chalk5.dim(` src/modules/${kebab}/models/${kebab}.ts`));
|
|
201
|
+
console.log(chalk5.dim(` src/modules/${kebab}/loaders/default.ts`));
|
|
202
|
+
console.log(chalk5.dim(` src/modules/${kebab}/service.ts`));
|
|
203
|
+
console.log();
|
|
204
|
+
console.log(" Next steps:");
|
|
205
|
+
console.log(chalk5.dim(` 1. Add the module to your meridian.config.ts:`));
|
|
206
|
+
console.log(chalk5.dim(` { resolve: "./src/modules/${kebab}" }`));
|
|
207
|
+
console.log(chalk5.dim(` 2. Run \`npm run db:migrate\` to sync the schema`));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// src/commands/generate/workflow.ts
|
|
211
|
+
import path5 from "path";
|
|
212
|
+
import { existsSync as existsSync3 } from "fs";
|
|
213
|
+
import chalk6 from "chalk";
|
|
214
|
+
async function generateWorkflow(name) {
|
|
215
|
+
const rootDir = findProjectRoot();
|
|
216
|
+
if (!rootDir) {
|
|
217
|
+
console.error(chalk6.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
|
|
218
|
+
process.exit(1);
|
|
219
|
+
}
|
|
220
|
+
if (!name) {
|
|
221
|
+
console.error(chalk6.red(" \u2716 Workflow name is required. Usage: meridian generate workflow <name>"));
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
const kebab = toKebabCase(name);
|
|
225
|
+
const pascal = toPascalCase(kebab);
|
|
226
|
+
const filePath = path5.join(rootDir, "src", "workflows", `${kebab}.ts`);
|
|
227
|
+
if (existsSync3(filePath)) {
|
|
228
|
+
console.error(chalk6.red(` \u2716 Workflow already exists: src/workflows/${kebab}.ts`));
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
const camel = pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
232
|
+
await writeFile(filePath, renderWorkflow(kebab, pascal));
|
|
233
|
+
console.log(chalk6.green(` \u2713 Generated workflow: src/workflows/${kebab}.ts`));
|
|
234
|
+
console.log();
|
|
235
|
+
console.log(" Usage:");
|
|
236
|
+
console.log(chalk6.dim(` import { ${camel}Workflow } from "./workflows/${kebab}.js"`));
|
|
237
|
+
console.log(chalk6.dim(` const { result } = await ${camel}Workflow(req.scope).run({ input: {...} })`));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/commands/generate/plugin.ts
|
|
241
|
+
import path6 from "path";
|
|
242
|
+
import { existsSync as existsSync4 } from "fs";
|
|
243
|
+
import chalk7 from "chalk";
|
|
244
|
+
function renderPluginIndex(_name, pascalName) {
|
|
245
|
+
return `import { fileURLToPath } from "node:url"
|
|
246
|
+
import path from "node:path"
|
|
247
|
+
import type { PluginRegistrationContext } from "@meridianjs/types"
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* The compiled directory of this plugin.
|
|
251
|
+
* The Meridian plugin-loader uses this to auto-scan api/, subscribers/, jobs/.
|
|
252
|
+
*/
|
|
253
|
+
export const pluginRoot: string = path.dirname(fileURLToPath(import.meta.url))
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Called during bootstrap before route/subscriber auto-scan.
|
|
257
|
+
* Use ctx.addModule() to register domain modules.
|
|
258
|
+
*/
|
|
259
|
+
export default async function register(_ctx: PluginRegistrationContext): Promise<void> {
|
|
260
|
+
// Example: register a custom module
|
|
261
|
+
// await ctx.addModule({ resolve: My${pascalName}Module })
|
|
262
|
+
}
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
265
|
+
function renderPluginRoute(name) {
|
|
266
|
+
return `import type { Response } from "express"
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* GET /admin/${name}
|
|
270
|
+
* Example plugin admin route.
|
|
271
|
+
*/
|
|
272
|
+
export const GET = async (req: any, res: Response) => {
|
|
273
|
+
res.json({ plugin: "${name}", status: "active" })
|
|
274
|
+
}
|
|
275
|
+
`;
|
|
276
|
+
}
|
|
277
|
+
async function generatePlugin(name) {
|
|
278
|
+
const rootDir = findProjectRoot();
|
|
279
|
+
if (!rootDir) {
|
|
280
|
+
console.error(chalk7.red(" \u2716 Could not find meridian.config.ts. Are you inside a Meridian project?"));
|
|
281
|
+
process.exit(1);
|
|
282
|
+
}
|
|
283
|
+
if (!name) {
|
|
284
|
+
console.error(chalk7.red(" \u2716 Plugin name is required. Usage: meridian generate plugin <name>"));
|
|
285
|
+
process.exit(1);
|
|
286
|
+
}
|
|
287
|
+
const kebab = toKebabCase(name);
|
|
288
|
+
const pascal = toPascalCase(kebab);
|
|
289
|
+
const pluginDir = path6.join(rootDir, "src", "plugins", kebab);
|
|
290
|
+
if (existsSync4(pluginDir)) {
|
|
291
|
+
console.error(chalk7.red(` \u2716 Plugin directory already exists: src/plugins/${kebab}`));
|
|
292
|
+
process.exit(1);
|
|
293
|
+
}
|
|
294
|
+
await writeFile(path6.join(pluginDir, "index.ts"), renderPluginIndex(kebab, pascal));
|
|
295
|
+
await writeFile(
|
|
296
|
+
path6.join(pluginDir, "api", "admin", kebab, "route.ts"),
|
|
297
|
+
renderPluginRoute(kebab)
|
|
298
|
+
);
|
|
299
|
+
console.log(chalk7.green(` \u2713 Generated plugin: src/plugins/${kebab}/`));
|
|
300
|
+
console.log();
|
|
301
|
+
console.log(" Files created:");
|
|
302
|
+
console.log(chalk7.dim(` src/plugins/${kebab}/index.ts`));
|
|
303
|
+
console.log(chalk7.dim(` src/plugins/${kebab}/api/admin/${kebab}/route.ts`));
|
|
304
|
+
console.log();
|
|
305
|
+
console.log(" Next steps:");
|
|
306
|
+
console.log(chalk7.dim(` 1. Add the plugin to your meridian.config.ts:`));
|
|
307
|
+
console.log(chalk7.dim(` plugins: [{ resolve: "./src/plugins/${kebab}" }]`));
|
|
308
|
+
console.log(chalk7.dim(` 2. Start the dev server: \`npm run dev\``));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/cli.ts
|
|
312
|
+
var program = new Command();
|
|
313
|
+
program.name("meridian").description("Meridian project management framework CLI").version("0.1.0");
|
|
314
|
+
program.command("new [project-name]").description("Create a new Meridian project").action((projectName) => {
|
|
315
|
+
runNew(projectName).catch((err) => {
|
|
316
|
+
console.error(err);
|
|
317
|
+
process.exit(1);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
program.command("dev").description("Start the development server").action(() => {
|
|
321
|
+
runDev().catch((err) => {
|
|
322
|
+
console.error(err);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
program.command("build").description("Type-check the project").action(() => {
|
|
327
|
+
runBuild().catch((err) => {
|
|
328
|
+
console.error(err);
|
|
329
|
+
process.exit(1);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
program.command("db:migrate").description("Synchronize the database schema (runs updateSchema on all modules)").action(() => {
|
|
333
|
+
runDbMigrate().catch((err) => {
|
|
334
|
+
console.error(err);
|
|
335
|
+
process.exit(1);
|
|
336
|
+
});
|
|
337
|
+
});
|
|
338
|
+
program.command("db:generate <name>").description("Generate a new migration file").action((name) => {
|
|
339
|
+
runDbGenerate(name).catch((err) => {
|
|
340
|
+
console.error(err);
|
|
341
|
+
process.exit(1);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
var generateCmd = program.command("generate").alias("g").description("Generate boilerplate files");
|
|
345
|
+
generateCmd.command("module <name>").description("Scaffold a new module in src/modules/").action((name) => {
|
|
346
|
+
generateModule(name).catch((err) => {
|
|
347
|
+
console.error(err);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
generateCmd.command("workflow <name>").description("Scaffold a new workflow in src/workflows/").action((name) => {
|
|
352
|
+
generateWorkflow(name).catch((err) => {
|
|
353
|
+
console.error(err);
|
|
354
|
+
process.exit(1);
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
generateCmd.command("plugin <name>").description("Scaffold a local plugin in src/plugins/").action((name) => {
|
|
358
|
+
generatePlugin(name).catch((err) => {
|
|
359
|
+
console.error(err);
|
|
360
|
+
process.exit(1);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
program.parse(process.argv);
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
runNew
|
|
4
|
+
} from "./chunk-7IUTX6RW.js";
|
|
5
|
+
|
|
6
|
+
// src/index.ts
|
|
7
|
+
var projectName = process.argv[2];
|
|
8
|
+
var name = projectName && !projectName.startsWith("-") ? projectName : void 0;
|
|
9
|
+
runNew(name).catch((err) => {
|
|
10
|
+
console.error(err);
|
|
11
|
+
process.exit(1);
|
|
12
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-meridian-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a new Meridian project or manage an existing one",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"bin": {
|
|
11
|
+
"create-meridian-app": "./dist/index.js",
|
|
12
|
+
"meridian": "./dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"build": "tsup src/index.ts src/cli.ts --format esm --dts --clean",
|
|
16
|
+
"dev": "tsup src/index.ts src/cli.ts --format esm --dts --watch",
|
|
17
|
+
"typecheck": "tsc --noEmit",
|
|
18
|
+
"clean": "rm -rf dist",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"commander": "^12.1.0",
|
|
23
|
+
"ora": "^8.1.1",
|
|
24
|
+
"chalk": "^5.3.0",
|
|
25
|
+
"prompts": "^2.4.2",
|
|
26
|
+
"execa": "^9.5.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/prompts": "^2.4.9",
|
|
30
|
+
"tsup": "^8.3.5",
|
|
31
|
+
"tsx": "4.21.0",
|
|
32
|
+
"typescript": "*"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist"
|
|
36
|
+
],
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20.0.0"
|
|
39
|
+
}
|
|
40
|
+
}
|