create-routa-ts 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/cli.d.ts +2 -0
- package/dist/cli.js +649 -0
- package/dist/create-project.d.ts +18 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +651 -0
- package/dist/ui.d.ts +10 -0
- package/package.json +38 -0
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
6
|
+
import { dirname as dirname2, join as join2, relative, resolve as resolve2, sep } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
|
|
9
|
+
// src/create-project.ts
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { builtinModules } from "node:module";
|
|
13
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
14
|
+
function createProject(targetDir, cwd = process.cwd(), options = {}) {
|
|
15
|
+
const projectDir = resolve(cwd, targetDir);
|
|
16
|
+
const projectName = basename(projectDir);
|
|
17
|
+
if (existsSync(projectDir)) {
|
|
18
|
+
throw new Error(`Target directory already exists: ${targetDir}`);
|
|
19
|
+
}
|
|
20
|
+
if (!isValidPackageName(projectName)) {
|
|
21
|
+
throw new Error(`Invalid package name from target directory: ${projectName}`);
|
|
22
|
+
}
|
|
23
|
+
const statusRoute = statusRouteSource();
|
|
24
|
+
const statusSchemas = statusSchemasSource();
|
|
25
|
+
const openApi = openApiYaml();
|
|
26
|
+
const routesMetadata = routesMetadataSource();
|
|
27
|
+
const openApiBaseline = openApiBaselineJson();
|
|
28
|
+
const manifest = manifestJson({
|
|
29
|
+
route: statusRoute,
|
|
30
|
+
schema: statusSchemas,
|
|
31
|
+
routesMetadata,
|
|
32
|
+
baseline: openApiBaseline
|
|
33
|
+
});
|
|
34
|
+
const files = /* @__PURE__ */ new Map([
|
|
35
|
+
[".gitignore", gitignore()],
|
|
36
|
+
[".vscode/settings.json", vscodeSettings()],
|
|
37
|
+
["README.md", readme(projectName, options.openApi ?? true)],
|
|
38
|
+
["biome.json", biomeJson()],
|
|
39
|
+
[
|
|
40
|
+
"package.json",
|
|
41
|
+
packageJson(projectName, options.routaVersion ?? "latest", options.openApi ?? true)
|
|
42
|
+
],
|
|
43
|
+
["src/routa.ts", routaSource()],
|
|
44
|
+
["tsconfig.json", tsconfigJson()],
|
|
45
|
+
["src/routes/status/route.ts", statusRoute],
|
|
46
|
+
["src/routes/status/schemas.ts", statusSchemas]
|
|
47
|
+
]);
|
|
48
|
+
if (options.openApi ?? true) {
|
|
49
|
+
files.set("openapi.yaml", openApi);
|
|
50
|
+
files.set(".routa/openapi-baseline.json", openApiBaseline);
|
|
51
|
+
files.set(".routa/manifest.json", manifest);
|
|
52
|
+
files.set(".routa/routes.gen.ts", routesMetadata);
|
|
53
|
+
}
|
|
54
|
+
for (const [path, content] of files) {
|
|
55
|
+
const absolutePath = join(projectDir, path);
|
|
56
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
57
|
+
writeFileSync(absolutePath, content);
|
|
58
|
+
}
|
|
59
|
+
return { projectDir, files: Array.from(files.keys()) };
|
|
60
|
+
}
|
|
61
|
+
function isValidPackageName(name) {
|
|
62
|
+
const reservedNames = /* @__PURE__ */ new Set([
|
|
63
|
+
"node_modules",
|
|
64
|
+
"favicon.ico",
|
|
65
|
+
...builtinModules.map((moduleName) => moduleName.replace(/^node:/, ""))
|
|
66
|
+
]);
|
|
67
|
+
return name.length <= 214 && !reservedNames.has(name) && /^[a-z0-9][a-z0-9._-]*$/.test(name) && encodeURIComponent(name) === name;
|
|
68
|
+
}
|
|
69
|
+
function generatedHeader(source) {
|
|
70
|
+
return `// Generated by Routa.
|
|
71
|
+
// Safe to edit, but regeneration may update this file after preview.
|
|
72
|
+
// Source: ${source}
|
|
73
|
+
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
function routesMetadataSource() {
|
|
77
|
+
const routes = [
|
|
78
|
+
{
|
|
79
|
+
file: ["src", "routes", "status", "route.ts"].join("/"),
|
|
80
|
+
path: "/status",
|
|
81
|
+
methods: ["GET"],
|
|
82
|
+
responses: { get: [200] },
|
|
83
|
+
inputs: { get: { params: false, query: false, body: false } },
|
|
84
|
+
middleware: [],
|
|
85
|
+
methodMiddleware: { get: [] },
|
|
86
|
+
ctx: [],
|
|
87
|
+
groups: [],
|
|
88
|
+
segments: ["status"]
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
const emptyCtx = "{ readonly __empty?: never }";
|
|
92
|
+
const methodCtxMap = `{
|
|
93
|
+
${["get", "post", "put", "patch", "delete", "head", "options"].map((method) => ` ${method}: ${emptyCtx};`).join("\n")}
|
|
94
|
+
}`;
|
|
95
|
+
return `${generatedHeader("src/routes directory")}export const routaRoutes = ${JSON.stringify(routes, null, " ")} as const;
|
|
96
|
+
|
|
97
|
+
export type StatusCtx = {
|
|
98
|
+
readonly __empty?: never;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type RoutaRouteCtxByPath = {
|
|
102
|
+
"/status": ${methodCtxMap};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type RoutaCtxByKey = ${emptyCtx};
|
|
106
|
+
|
|
107
|
+
declare module "@routa-ts/core" {
|
|
108
|
+
export interface Register {
|
|
109
|
+
routeCtxByPath: RoutaRouteCtxByPath;
|
|
110
|
+
ctxByKey: RoutaCtxByKey;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
function openApiBaselineJson() {
|
|
116
|
+
return `${JSON.stringify(
|
|
117
|
+
{
|
|
118
|
+
openapi: "3.1.0",
|
|
119
|
+
info: {
|
|
120
|
+
title: "Routa API",
|
|
121
|
+
version: "0.0.0"
|
|
122
|
+
},
|
|
123
|
+
paths: {
|
|
124
|
+
"/status": {
|
|
125
|
+
get: {
|
|
126
|
+
operationId: "getStatus",
|
|
127
|
+
responses: {
|
|
128
|
+
"200": {
|
|
129
|
+
description: "Service status",
|
|
130
|
+
content: {
|
|
131
|
+
"application/json": {
|
|
132
|
+
schema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
required: ["ok"],
|
|
135
|
+
properties: {
|
|
136
|
+
ok: {
|
|
137
|
+
type: "boolean"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
null,
|
|
150
|
+
" "
|
|
151
|
+
)}
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
function manifestJson(content) {
|
|
155
|
+
return `${JSON.stringify(
|
|
156
|
+
{
|
|
157
|
+
version: 1,
|
|
158
|
+
openapi: {
|
|
159
|
+
baseline: ".routa/openapi-baseline.json"
|
|
160
|
+
},
|
|
161
|
+
generated: [
|
|
162
|
+
{
|
|
163
|
+
path: "src/routes/status/route.ts",
|
|
164
|
+
source: "openapi.yaml",
|
|
165
|
+
operationIds: ["getStatus"],
|
|
166
|
+
kind: "route",
|
|
167
|
+
sha256: sha256(content.route)
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
path: "src/routes/status/schemas.ts",
|
|
171
|
+
source: "openapi.yaml",
|
|
172
|
+
operationIds: ["getStatus"],
|
|
173
|
+
kind: "schema",
|
|
174
|
+
sha256: sha256(content.schema)
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
path: ".routa/routes.gen.ts",
|
|
178
|
+
source: "openapi.yaml",
|
|
179
|
+
kind: "route-metadata",
|
|
180
|
+
sha256: sha256(content.routesMetadata)
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
path: ".routa/openapi-baseline.json",
|
|
184
|
+
source: "openapi.yaml",
|
|
185
|
+
kind: "openapi-baseline",
|
|
186
|
+
sha256: sha256(content.baseline)
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
null,
|
|
191
|
+
" "
|
|
192
|
+
)}
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
function sha256(content) {
|
|
196
|
+
return createHash("sha256").update(content).digest("hex");
|
|
197
|
+
}
|
|
198
|
+
function packageJson(name, routaVersion, openApi) {
|
|
199
|
+
return `${JSON.stringify(
|
|
200
|
+
{
|
|
201
|
+
name,
|
|
202
|
+
version: "0.0.0",
|
|
203
|
+
private: true,
|
|
204
|
+
type: "module",
|
|
205
|
+
scripts: {
|
|
206
|
+
dev: "routa dev",
|
|
207
|
+
start: "routa start",
|
|
208
|
+
check: "routa check",
|
|
209
|
+
build: "routa build",
|
|
210
|
+
lint: "biome check .",
|
|
211
|
+
format: "biome check --write .",
|
|
212
|
+
...openApi ? {
|
|
213
|
+
scaffold: "routa scaffold openapi.yaml",
|
|
214
|
+
"openapi:check": "routa openapi check"
|
|
215
|
+
} : {}
|
|
216
|
+
},
|
|
217
|
+
dependencies: {
|
|
218
|
+
"@routa-ts/cli": routaVersion,
|
|
219
|
+
"@routa-ts/core": routaVersion,
|
|
220
|
+
hono: "^4.12.27",
|
|
221
|
+
tsx: "^4.22.4",
|
|
222
|
+
zod: "^4.4.3"
|
|
223
|
+
},
|
|
224
|
+
devDependencies: {
|
|
225
|
+
"@biomejs/biome": "^2.5.1",
|
|
226
|
+
"@types/node": "^24.0.4",
|
|
227
|
+
typescript: "^5.8.3"
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
null,
|
|
231
|
+
" "
|
|
232
|
+
)}
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
function tsconfigJson() {
|
|
236
|
+
return `{
|
|
237
|
+
"compilerOptions": {
|
|
238
|
+
"target": "ES2022",
|
|
239
|
+
"module": "NodeNext",
|
|
240
|
+
"moduleResolution": "NodeNext",
|
|
241
|
+
"strict": true,
|
|
242
|
+
"skipLibCheck": true,
|
|
243
|
+
"noEmitOnError": true,
|
|
244
|
+
"outDir": "dist",
|
|
245
|
+
"types": ["node"]
|
|
246
|
+
},
|
|
247
|
+
"include": ["src/**/*.ts", ".routa/**/*.ts"]
|
|
248
|
+
}
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
function gitignore() {
|
|
252
|
+
return `node_modules/
|
|
253
|
+
dist/
|
|
254
|
+
.env
|
|
255
|
+
.env.local
|
|
256
|
+
coverage/
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
function vscodeSettings() {
|
|
260
|
+
return `${JSON.stringify(
|
|
261
|
+
{
|
|
262
|
+
"files.watcherExclude": {
|
|
263
|
+
"**/.routa/routes.gen.ts": true
|
|
264
|
+
},
|
|
265
|
+
"search.exclude": {
|
|
266
|
+
"**/.routa/routes.gen.ts": true
|
|
267
|
+
},
|
|
268
|
+
"files.readonlyInclude": {
|
|
269
|
+
"**/.routa/routes.gen.ts": true
|
|
270
|
+
},
|
|
271
|
+
"[typescript]": {
|
|
272
|
+
"editor.defaultFormatter": "biomejs.biome"
|
|
273
|
+
},
|
|
274
|
+
"[json]": {
|
|
275
|
+
"editor.defaultFormatter": "biomejs.biome"
|
|
276
|
+
},
|
|
277
|
+
"[jsonc]": {
|
|
278
|
+
"editor.defaultFormatter": "biomejs.biome"
|
|
279
|
+
},
|
|
280
|
+
"editor.codeActionsOnSave": {
|
|
281
|
+
"source.organizeImports.biome": "explicit"
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
null,
|
|
285
|
+
" "
|
|
286
|
+
)}
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
function readme(name, openApi) {
|
|
290
|
+
return `# ${name}
|
|
291
|
+
|
|
292
|
+
Routa API generated with \`pnpm create routa-ts@latest\`.
|
|
293
|
+
|
|
294
|
+
## Development
|
|
295
|
+
|
|
296
|
+
\`\`\`sh
|
|
297
|
+
pnpm install
|
|
298
|
+
pnpm dev
|
|
299
|
+
\`\`\`
|
|
300
|
+
|
|
301
|
+
\`pnpm dev\` runs \`routa dev\`, which validates the route graph, generates Routa metadata, typechecks, and starts the internal development server.
|
|
302
|
+
|
|
303
|
+
## Scripts
|
|
304
|
+
|
|
305
|
+
\`\`\`sh
|
|
306
|
+
pnpm dev
|
|
307
|
+
pnpm start
|
|
308
|
+
pnpm check
|
|
309
|
+
pnpm build
|
|
310
|
+
pnpm lint
|
|
311
|
+
pnpm format
|
|
312
|
+
${openApi ? "pnpm openapi:check\n" : ""}\`\`\`
|
|
313
|
+
|
|
314
|
+
## Routes
|
|
315
|
+
|
|
316
|
+
\`src/routa.ts\` is the user-owned Routa entry point. Routes live in \`src/routes\`.
|
|
317
|
+
|
|
318
|
+
\`\`\`txt
|
|
319
|
+
src/routa.ts
|
|
320
|
+
src/routes/status/route.ts
|
|
321
|
+
src/routes/status/schemas.ts
|
|
322
|
+
\`\`\`
|
|
323
|
+
|
|
324
|
+
${openApi ? "Routa owns generated project metadata in `.routa/`. Commit those files so OpenAPI drift and regeneration safety work across machines." : "This starter was created without OpenAPI files. Add `openapi.yaml` later and run `routa scaffold openapi.yaml` if you want generated route metadata."}
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
function biomeJson() {
|
|
328
|
+
return `${JSON.stringify(
|
|
329
|
+
{
|
|
330
|
+
$schema: "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
331
|
+
root: false,
|
|
332
|
+
vcs: {
|
|
333
|
+
enabled: true,
|
|
334
|
+
clientKind: "git",
|
|
335
|
+
useIgnoreFile: true
|
|
336
|
+
},
|
|
337
|
+
files: {
|
|
338
|
+
ignoreUnknown: true,
|
|
339
|
+
includes: ["**", "!node_modules", "!dist", "!coverage", "!.routa"]
|
|
340
|
+
},
|
|
341
|
+
formatter: {
|
|
342
|
+
enabled: true,
|
|
343
|
+
indentStyle: "tab",
|
|
344
|
+
lineWidth: 100
|
|
345
|
+
},
|
|
346
|
+
linter: {
|
|
347
|
+
enabled: true,
|
|
348
|
+
rules: {
|
|
349
|
+
preset: "recommended",
|
|
350
|
+
correctness: {
|
|
351
|
+
useImportExtensions: "off"
|
|
352
|
+
},
|
|
353
|
+
suspicious: {
|
|
354
|
+
noConsole: "off",
|
|
355
|
+
noExplicitAny: "off"
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
assist: {
|
|
360
|
+
enabled: true,
|
|
361
|
+
actions: {
|
|
362
|
+
source: {
|
|
363
|
+
recommended: true
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
null,
|
|
369
|
+
" "
|
|
370
|
+
)}
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
function routaSource() {
|
|
374
|
+
return `import { createRouta } from "@routa-ts/core";
|
|
375
|
+
|
|
376
|
+
export default createRouta({
|
|
377
|
+
port: 3000,
|
|
378
|
+
});
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
function openApiYaml() {
|
|
382
|
+
return `openapi: 3.1.0
|
|
383
|
+
info:
|
|
384
|
+
title: Routa API
|
|
385
|
+
version: 0.0.0
|
|
386
|
+
paths:
|
|
387
|
+
/status:
|
|
388
|
+
get:
|
|
389
|
+
operationId: getStatus
|
|
390
|
+
responses:
|
|
391
|
+
"200":
|
|
392
|
+
description: Service status
|
|
393
|
+
content:
|
|
394
|
+
application/json:
|
|
395
|
+
schema:
|
|
396
|
+
type: object
|
|
397
|
+
required:
|
|
398
|
+
- ok
|
|
399
|
+
properties:
|
|
400
|
+
ok:
|
|
401
|
+
type: boolean
|
|
402
|
+
`;
|
|
403
|
+
}
|
|
404
|
+
function statusRouteSource() {
|
|
405
|
+
return `import { createRoute, defineRoute } from "@routa-ts/core";
|
|
406
|
+
import { GetStatusResponse } from "./schemas.js";
|
|
407
|
+
|
|
408
|
+
export default defineRoute({
|
|
409
|
+
get: createRoute({
|
|
410
|
+
responses: {
|
|
411
|
+
success: {
|
|
412
|
+
status: 200,
|
|
413
|
+
schema: GetStatusResponse,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
run: () => ({ type: "success", data: { ok: true } }),
|
|
417
|
+
}),
|
|
418
|
+
});
|
|
419
|
+
`;
|
|
420
|
+
}
|
|
421
|
+
function statusSchemasSource() {
|
|
422
|
+
return `import { z } from "zod";
|
|
423
|
+
|
|
424
|
+
export const GetStatusResponse = z.object({
|
|
425
|
+
ok: z.boolean(),
|
|
426
|
+
});
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/ui.ts
|
|
431
|
+
function shouldUseColor() {
|
|
432
|
+
if ("FORCE_COLOR" in process.env) {
|
|
433
|
+
return process.env.FORCE_COLOR !== "0";
|
|
434
|
+
}
|
|
435
|
+
if ("NO_COLOR" in process.env || process.env.CI) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
return Boolean(process.stdout.isTTY);
|
|
439
|
+
}
|
|
440
|
+
function createUi(color = false) {
|
|
441
|
+
return {
|
|
442
|
+
heading: style(color, "1;36"),
|
|
443
|
+
command: style(color, "36"),
|
|
444
|
+
success: style(color, "32"),
|
|
445
|
+
warn: style(color, "33"),
|
|
446
|
+
error: style(color, "31"),
|
|
447
|
+
muted: style(color, "2")
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function style(color, code) {
|
|
451
|
+
return (value) => color ? `\x1B[${code}m${value}\x1B[0m` : value;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/index.ts
|
|
455
|
+
async function runCreate(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
456
|
+
const config = await resolveCreateConfig(argv, cwd);
|
|
457
|
+
const ui = createUi(shouldUseColor());
|
|
458
|
+
try {
|
|
459
|
+
printSummary(config, ui);
|
|
460
|
+
if (config.interactive && config.prompted && !config.yes && !await confirm("Continue with these settings?", true)) {
|
|
461
|
+
process.stdout.write(`${ui.muted("Creation cancelled.")}
|
|
462
|
+
`);
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
const result = createProject(config.targetDir, cwd, {
|
|
466
|
+
openApi: config.openApi,
|
|
467
|
+
routaVersion: resolveRoutaVersion(config.cwd, config.targetDir)
|
|
468
|
+
});
|
|
469
|
+
if (config.git) {
|
|
470
|
+
const git = spawnSync("git", ["init"], {
|
|
471
|
+
cwd: result.projectDir,
|
|
472
|
+
encoding: "utf8"
|
|
473
|
+
});
|
|
474
|
+
if (git.status === 0) {
|
|
475
|
+
process.stdout.write(`${ui.success("Initialized git repository.")}
|
|
476
|
+
`);
|
|
477
|
+
} else {
|
|
478
|
+
process.stderr.write(`${ui.error("git init failed.")} ${git.stderr ?? ""}
|
|
479
|
+
`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
if (config.install) {
|
|
483
|
+
const install = spawnSync("pnpm", ["install"], {
|
|
484
|
+
cwd: result.projectDir,
|
|
485
|
+
encoding: "utf8",
|
|
486
|
+
stdio: "inherit"
|
|
487
|
+
});
|
|
488
|
+
if (install.status !== 0) {
|
|
489
|
+
process.stderr.write(
|
|
490
|
+
`${ui.error('Command "pnpm install" did not run successfully.')} Please run this manually in your project.
|
|
491
|
+
`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
process.stdout.write(
|
|
496
|
+
`
|
|
497
|
+
${ui.success(`Your Routa app is ready in '${config.targetDir}'.`)}
|
|
498
|
+
|
|
499
|
+
`
|
|
500
|
+
);
|
|
501
|
+
process.stdout.write("Use the following commands to start your app:\n");
|
|
502
|
+
process.stdout.write(`${ui.command(`cd ${config.targetDir}`)}
|
|
503
|
+
`);
|
|
504
|
+
if (!config.install) {
|
|
505
|
+
process.stdout.write(`${ui.command("pnpm install")}
|
|
506
|
+
`);
|
|
507
|
+
}
|
|
508
|
+
process.stdout.write(`${ui.command("pnpm dev")}
|
|
509
|
+
`);
|
|
510
|
+
return 0;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
process.stderr.write(
|
|
513
|
+
`${ui.error("Error:")} ${error instanceof Error ? error.message : String(error)}
|
|
514
|
+
`
|
|
515
|
+
);
|
|
516
|
+
return 1;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
function resolveRoutaVersion(cwd, targetDir) {
|
|
520
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
521
|
+
if (!workspaceRoot) {
|
|
522
|
+
return "latest";
|
|
523
|
+
}
|
|
524
|
+
const workspaceConfig = readFileSync(join2(workspaceRoot, "pnpm-workspace.yaml"), "utf8");
|
|
525
|
+
const projectPath = resolve2(cwd, targetDir);
|
|
526
|
+
const projectRelativePath = relative(workspaceRoot, projectPath).split(sep).join("/");
|
|
527
|
+
if (workspaceConfig.includes("examples/*") && projectRelativePath.startsWith("examples/")) {
|
|
528
|
+
return "workspace:*";
|
|
529
|
+
}
|
|
530
|
+
return "latest";
|
|
531
|
+
}
|
|
532
|
+
function findWorkspaceRoot(cwd) {
|
|
533
|
+
let current = resolve2(cwd);
|
|
534
|
+
while (true) {
|
|
535
|
+
if (existsSync2(join2(current, "pnpm-workspace.yaml"))) {
|
|
536
|
+
return current;
|
|
537
|
+
}
|
|
538
|
+
const parent = dirname2(current);
|
|
539
|
+
if (parent === current) {
|
|
540
|
+
return void 0;
|
|
541
|
+
}
|
|
542
|
+
current = parent;
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
async function resolveCreateConfig(argv, cwd) {
|
|
546
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
547
|
+
const targetArg = argv.find((item) => !item.startsWith("-"));
|
|
548
|
+
let targetDir = targetArg ?? "routa-app";
|
|
549
|
+
let prompted = false;
|
|
550
|
+
if (interactive && !targetArg) {
|
|
551
|
+
const answer = await question("Project name (leave empty to use routa-app)");
|
|
552
|
+
prompted = true;
|
|
553
|
+
targetDir = answer.trim() || targetDir;
|
|
554
|
+
}
|
|
555
|
+
const openApi = await flagOrPrompt(
|
|
556
|
+
argv,
|
|
557
|
+
"--openapi",
|
|
558
|
+
"--no-openapi",
|
|
559
|
+
"Include a starter OpenAPI file?",
|
|
560
|
+
true
|
|
561
|
+
);
|
|
562
|
+
const git = await flagOrPrompt(
|
|
563
|
+
argv,
|
|
564
|
+
"--git",
|
|
565
|
+
"--no-git",
|
|
566
|
+
"Initialize a new git repository?",
|
|
567
|
+
interactive
|
|
568
|
+
);
|
|
569
|
+
const install = await flagOrPrompt(
|
|
570
|
+
argv,
|
|
571
|
+
"--install",
|
|
572
|
+
"--no-install",
|
|
573
|
+
"Install dependencies?",
|
|
574
|
+
interactive
|
|
575
|
+
);
|
|
576
|
+
prompted = prompted || openApi.prompted || git.prompted || install.prompted;
|
|
577
|
+
return {
|
|
578
|
+
targetDir,
|
|
579
|
+
openApi: openApi.value,
|
|
580
|
+
git: git.value,
|
|
581
|
+
install: install.value,
|
|
582
|
+
interactive,
|
|
583
|
+
prompted,
|
|
584
|
+
yes: argv.includes("--yes") || argv.includes("-y"),
|
|
585
|
+
cwd
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async function flagOrPrompt(argv, enabledFlag, disabledFlag, label, defaultValue) {
|
|
589
|
+
if (argv.includes(enabledFlag)) {
|
|
590
|
+
return { value: true, prompted: false };
|
|
591
|
+
}
|
|
592
|
+
if (argv.includes(disabledFlag)) {
|
|
593
|
+
return { value: false, prompted: false };
|
|
594
|
+
}
|
|
595
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
596
|
+
return { value: defaultValue, prompted: false };
|
|
597
|
+
}
|
|
598
|
+
return { value: await confirm(label, defaultValue), prompted: true };
|
|
599
|
+
}
|
|
600
|
+
async function question(label) {
|
|
601
|
+
const prompt = createInterface({
|
|
602
|
+
input: process.stdin,
|
|
603
|
+
output: process.stdout
|
|
604
|
+
});
|
|
605
|
+
try {
|
|
606
|
+
return await prompt.question(`${label}
|
|
607
|
+
> `);
|
|
608
|
+
} finally {
|
|
609
|
+
prompt.close();
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
async function confirm(label, defaultValue) {
|
|
613
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
614
|
+
const answer = (await question(`${label} (${suffix})`)).trim().toLowerCase();
|
|
615
|
+
if (!answer) {
|
|
616
|
+
return defaultValue;
|
|
617
|
+
}
|
|
618
|
+
return ["y", "yes"].includes(answer);
|
|
619
|
+
}
|
|
620
|
+
function printSummary(config, ui) {
|
|
621
|
+
process.stdout.write(`${ui.heading("Let's configure your Routa API")}
|
|
622
|
+
|
|
623
|
+
`);
|
|
624
|
+
process.stdout.write(`${ui.muted("About to create:")}
|
|
625
|
+
|
|
626
|
+
`);
|
|
627
|
+
process.stdout.write(` Project: ${config.targetDir}
|
|
628
|
+
`);
|
|
629
|
+
process.stdout.write(` Location: ${config.cwd}/${config.targetDir}
|
|
630
|
+
`);
|
|
631
|
+
process.stdout.write(" Package manager: pnpm\n");
|
|
632
|
+
process.stdout.write(" Toolchain: TypeScript, Biome\n");
|
|
633
|
+
process.stdout.write(` OpenAPI starter: ${config.openApi ? "yes" : "no"}
|
|
634
|
+
`);
|
|
635
|
+
process.stdout.write(` Initialize git: ${config.git ? "yes" : "no"}
|
|
636
|
+
`);
|
|
637
|
+
process.stdout.write(` Install deps: ${config.install ? "yes" : "no"}
|
|
638
|
+
|
|
639
|
+
`);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// src/cli.ts
|
|
643
|
+
runCreate().then((code) => {
|
|
644
|
+
process.exitCode = code;
|
|
645
|
+
}).catch((error) => {
|
|
646
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}
|
|
647
|
+
`);
|
|
648
|
+
process.exitCode = 1;
|
|
649
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export type CreateProjectResult = {
|
|
2
|
+
projectDir: string;
|
|
3
|
+
files: string[];
|
|
4
|
+
};
|
|
5
|
+
export type CreateProjectOptions = {
|
|
6
|
+
openApi?: boolean;
|
|
7
|
+
routaVersion?: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new Routa project scaffold in the target directory.
|
|
11
|
+
*
|
|
12
|
+
* @param targetDir - Directory for the new project, resolved relative to `cwd`
|
|
13
|
+
* @param cwd - Base directory used to resolve `targetDir`
|
|
14
|
+
* @param options - Project generation options
|
|
15
|
+
* @returns The absolute project directory and the list of generated file paths
|
|
16
|
+
* @throws Error if the target directory already exists
|
|
17
|
+
*/
|
|
18
|
+
export declare function createProject(targetDir: string, cwd?: string, options?: CreateProjectOptions): CreateProjectResult;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export { createProject } from "./create-project.js";
|
|
3
|
+
export { createUi, shouldUseColor, type Ui } from "./ui.js";
|
|
4
|
+
/**
|
|
5
|
+
* Prepends the create subcommand to an argument list.
|
|
6
|
+
*
|
|
7
|
+
* @param argv - The original command-line arguments
|
|
8
|
+
* @returns The arguments with `"create"` inserted at the beginning
|
|
9
|
+
*/
|
|
10
|
+
export declare function createCommandArgs(argv: readonly string[]): string[];
|
|
11
|
+
/**
|
|
12
|
+
* Creates a new Routa app from the provided command-line arguments.
|
|
13
|
+
*
|
|
14
|
+
* @param argv - Command-line arguments to parse
|
|
15
|
+
* @param cwd - Current working directory used to resolve paths
|
|
16
|
+
* @returns `0` on success or cancellation, `1` if project creation fails
|
|
17
|
+
*/
|
|
18
|
+
export declare function runCreate(argv?: string[], cwd?: string): Promise<number>;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves the Routa package version for a new project.
|
|
21
|
+
*
|
|
22
|
+
* @param cwd - The current working directory used to locate a pnpm workspace
|
|
23
|
+
* @param targetDir - The target project directory
|
|
24
|
+
* @returns `"workspace:*"` for example projects in matching workspaces, `"latest"` otherwise
|
|
25
|
+
*/
|
|
26
|
+
export declare function resolveRoutaVersion(cwd: string, targetDir: string): "latest" | "workspace:*";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { existsSync as existsSync2, readFileSync } from "node:fs";
|
|
6
|
+
import { dirname as dirname2, join as join2, relative, resolve as resolve2, sep } from "node:path";
|
|
7
|
+
import { createInterface } from "node:readline/promises";
|
|
8
|
+
|
|
9
|
+
// src/create-project.ts
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { builtinModules } from "node:module";
|
|
13
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
14
|
+
function createProject(targetDir, cwd = process.cwd(), options = {}) {
|
|
15
|
+
const projectDir = resolve(cwd, targetDir);
|
|
16
|
+
const projectName = basename(projectDir);
|
|
17
|
+
if (existsSync(projectDir)) {
|
|
18
|
+
throw new Error(`Target directory already exists: ${targetDir}`);
|
|
19
|
+
}
|
|
20
|
+
if (!isValidPackageName(projectName)) {
|
|
21
|
+
throw new Error(`Invalid package name from target directory: ${projectName}`);
|
|
22
|
+
}
|
|
23
|
+
const statusRoute = statusRouteSource();
|
|
24
|
+
const statusSchemas = statusSchemasSource();
|
|
25
|
+
const openApi = openApiYaml();
|
|
26
|
+
const routesMetadata = routesMetadataSource();
|
|
27
|
+
const openApiBaseline = openApiBaselineJson();
|
|
28
|
+
const manifest = manifestJson({
|
|
29
|
+
route: statusRoute,
|
|
30
|
+
schema: statusSchemas,
|
|
31
|
+
routesMetadata,
|
|
32
|
+
baseline: openApiBaseline
|
|
33
|
+
});
|
|
34
|
+
const files = /* @__PURE__ */ new Map([
|
|
35
|
+
[".gitignore", gitignore()],
|
|
36
|
+
[".vscode/settings.json", vscodeSettings()],
|
|
37
|
+
["README.md", readme(projectName, options.openApi ?? true)],
|
|
38
|
+
["biome.json", biomeJson()],
|
|
39
|
+
[
|
|
40
|
+
"package.json",
|
|
41
|
+
packageJson(projectName, options.routaVersion ?? "latest", options.openApi ?? true)
|
|
42
|
+
],
|
|
43
|
+
["src/routa.ts", routaSource()],
|
|
44
|
+
["tsconfig.json", tsconfigJson()],
|
|
45
|
+
["src/routes/status/route.ts", statusRoute],
|
|
46
|
+
["src/routes/status/schemas.ts", statusSchemas]
|
|
47
|
+
]);
|
|
48
|
+
if (options.openApi ?? true) {
|
|
49
|
+
files.set("openapi.yaml", openApi);
|
|
50
|
+
files.set(".routa/openapi-baseline.json", openApiBaseline);
|
|
51
|
+
files.set(".routa/manifest.json", manifest);
|
|
52
|
+
files.set(".routa/routes.gen.ts", routesMetadata);
|
|
53
|
+
}
|
|
54
|
+
for (const [path, content] of files) {
|
|
55
|
+
const absolutePath = join(projectDir, path);
|
|
56
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
57
|
+
writeFileSync(absolutePath, content);
|
|
58
|
+
}
|
|
59
|
+
return { projectDir, files: Array.from(files.keys()) };
|
|
60
|
+
}
|
|
61
|
+
function isValidPackageName(name) {
|
|
62
|
+
const reservedNames = /* @__PURE__ */ new Set([
|
|
63
|
+
"node_modules",
|
|
64
|
+
"favicon.ico",
|
|
65
|
+
...builtinModules.map((moduleName) => moduleName.replace(/^node:/, ""))
|
|
66
|
+
]);
|
|
67
|
+
return name.length <= 214 && !reservedNames.has(name) && /^[a-z0-9][a-z0-9._-]*$/.test(name) && encodeURIComponent(name) === name;
|
|
68
|
+
}
|
|
69
|
+
function generatedHeader(source) {
|
|
70
|
+
return `// Generated by Routa.
|
|
71
|
+
// Safe to edit, but regeneration may update this file after preview.
|
|
72
|
+
// Source: ${source}
|
|
73
|
+
|
|
74
|
+
`;
|
|
75
|
+
}
|
|
76
|
+
function routesMetadataSource() {
|
|
77
|
+
const routes = [
|
|
78
|
+
{
|
|
79
|
+
file: ["src", "routes", "status", "route.ts"].join("/"),
|
|
80
|
+
path: "/status",
|
|
81
|
+
methods: ["GET"],
|
|
82
|
+
responses: { get: [200] },
|
|
83
|
+
inputs: { get: { params: false, query: false, body: false } },
|
|
84
|
+
middleware: [],
|
|
85
|
+
methodMiddleware: { get: [] },
|
|
86
|
+
ctx: [],
|
|
87
|
+
groups: [],
|
|
88
|
+
segments: ["status"]
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
const emptyCtx = "{ readonly __empty?: never }";
|
|
92
|
+
const methodCtxMap = `{
|
|
93
|
+
${["get", "post", "put", "patch", "delete", "head", "options"].map((method) => ` ${method}: ${emptyCtx};`).join("\n")}
|
|
94
|
+
}`;
|
|
95
|
+
return `${generatedHeader("src/routes directory")}export const routaRoutes = ${JSON.stringify(routes, null, " ")} as const;
|
|
96
|
+
|
|
97
|
+
export type StatusCtx = {
|
|
98
|
+
readonly __empty?: never;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export type RoutaRouteCtxByPath = {
|
|
102
|
+
"/status": ${methodCtxMap};
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export type RoutaCtxByKey = ${emptyCtx};
|
|
106
|
+
|
|
107
|
+
declare module "@routa-ts/core" {
|
|
108
|
+
export interface Register {
|
|
109
|
+
routeCtxByPath: RoutaRouteCtxByPath;
|
|
110
|
+
ctxByKey: RoutaCtxByKey;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
`;
|
|
114
|
+
}
|
|
115
|
+
function openApiBaselineJson() {
|
|
116
|
+
return `${JSON.stringify(
|
|
117
|
+
{
|
|
118
|
+
openapi: "3.1.0",
|
|
119
|
+
info: {
|
|
120
|
+
title: "Routa API",
|
|
121
|
+
version: "0.0.0"
|
|
122
|
+
},
|
|
123
|
+
paths: {
|
|
124
|
+
"/status": {
|
|
125
|
+
get: {
|
|
126
|
+
operationId: "getStatus",
|
|
127
|
+
responses: {
|
|
128
|
+
"200": {
|
|
129
|
+
description: "Service status",
|
|
130
|
+
content: {
|
|
131
|
+
"application/json": {
|
|
132
|
+
schema: {
|
|
133
|
+
type: "object",
|
|
134
|
+
required: ["ok"],
|
|
135
|
+
properties: {
|
|
136
|
+
ok: {
|
|
137
|
+
type: "boolean"
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
null,
|
|
150
|
+
" "
|
|
151
|
+
)}
|
|
152
|
+
`;
|
|
153
|
+
}
|
|
154
|
+
function manifestJson(content) {
|
|
155
|
+
return `${JSON.stringify(
|
|
156
|
+
{
|
|
157
|
+
version: 1,
|
|
158
|
+
openapi: {
|
|
159
|
+
baseline: ".routa/openapi-baseline.json"
|
|
160
|
+
},
|
|
161
|
+
generated: [
|
|
162
|
+
{
|
|
163
|
+
path: "src/routes/status/route.ts",
|
|
164
|
+
source: "openapi.yaml",
|
|
165
|
+
operationIds: ["getStatus"],
|
|
166
|
+
kind: "route",
|
|
167
|
+
sha256: sha256(content.route)
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
path: "src/routes/status/schemas.ts",
|
|
171
|
+
source: "openapi.yaml",
|
|
172
|
+
operationIds: ["getStatus"],
|
|
173
|
+
kind: "schema",
|
|
174
|
+
sha256: sha256(content.schema)
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
path: ".routa/routes.gen.ts",
|
|
178
|
+
source: "openapi.yaml",
|
|
179
|
+
kind: "route-metadata",
|
|
180
|
+
sha256: sha256(content.routesMetadata)
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
path: ".routa/openapi-baseline.json",
|
|
184
|
+
source: "openapi.yaml",
|
|
185
|
+
kind: "openapi-baseline",
|
|
186
|
+
sha256: sha256(content.baseline)
|
|
187
|
+
}
|
|
188
|
+
]
|
|
189
|
+
},
|
|
190
|
+
null,
|
|
191
|
+
" "
|
|
192
|
+
)}
|
|
193
|
+
`;
|
|
194
|
+
}
|
|
195
|
+
function sha256(content) {
|
|
196
|
+
return createHash("sha256").update(content).digest("hex");
|
|
197
|
+
}
|
|
198
|
+
function packageJson(name, routaVersion, openApi) {
|
|
199
|
+
return `${JSON.stringify(
|
|
200
|
+
{
|
|
201
|
+
name,
|
|
202
|
+
version: "0.0.0",
|
|
203
|
+
private: true,
|
|
204
|
+
type: "module",
|
|
205
|
+
scripts: {
|
|
206
|
+
dev: "routa dev",
|
|
207
|
+
start: "routa start",
|
|
208
|
+
check: "routa check",
|
|
209
|
+
build: "routa build",
|
|
210
|
+
lint: "biome check .",
|
|
211
|
+
format: "biome check --write .",
|
|
212
|
+
...openApi ? {
|
|
213
|
+
scaffold: "routa scaffold openapi.yaml",
|
|
214
|
+
"openapi:check": "routa openapi check"
|
|
215
|
+
} : {}
|
|
216
|
+
},
|
|
217
|
+
dependencies: {
|
|
218
|
+
"@routa-ts/cli": routaVersion,
|
|
219
|
+
"@routa-ts/core": routaVersion,
|
|
220
|
+
hono: "^4.12.27",
|
|
221
|
+
tsx: "^4.22.4",
|
|
222
|
+
zod: "^4.4.3"
|
|
223
|
+
},
|
|
224
|
+
devDependencies: {
|
|
225
|
+
"@biomejs/biome": "^2.5.1",
|
|
226
|
+
"@types/node": "^24.0.4",
|
|
227
|
+
typescript: "^5.8.3"
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
null,
|
|
231
|
+
" "
|
|
232
|
+
)}
|
|
233
|
+
`;
|
|
234
|
+
}
|
|
235
|
+
function tsconfigJson() {
|
|
236
|
+
return `{
|
|
237
|
+
"compilerOptions": {
|
|
238
|
+
"target": "ES2022",
|
|
239
|
+
"module": "NodeNext",
|
|
240
|
+
"moduleResolution": "NodeNext",
|
|
241
|
+
"strict": true,
|
|
242
|
+
"skipLibCheck": true,
|
|
243
|
+
"noEmitOnError": true,
|
|
244
|
+
"outDir": "dist",
|
|
245
|
+
"types": ["node"]
|
|
246
|
+
},
|
|
247
|
+
"include": ["src/**/*.ts", ".routa/**/*.ts"]
|
|
248
|
+
}
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
function gitignore() {
|
|
252
|
+
return `node_modules/
|
|
253
|
+
dist/
|
|
254
|
+
.env
|
|
255
|
+
.env.local
|
|
256
|
+
coverage/
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
function vscodeSettings() {
|
|
260
|
+
return `${JSON.stringify(
|
|
261
|
+
{
|
|
262
|
+
"files.watcherExclude": {
|
|
263
|
+
"**/.routa/routes.gen.ts": true
|
|
264
|
+
},
|
|
265
|
+
"search.exclude": {
|
|
266
|
+
"**/.routa/routes.gen.ts": true
|
|
267
|
+
},
|
|
268
|
+
"files.readonlyInclude": {
|
|
269
|
+
"**/.routa/routes.gen.ts": true
|
|
270
|
+
},
|
|
271
|
+
"[typescript]": {
|
|
272
|
+
"editor.defaultFormatter": "biomejs.biome"
|
|
273
|
+
},
|
|
274
|
+
"[json]": {
|
|
275
|
+
"editor.defaultFormatter": "biomejs.biome"
|
|
276
|
+
},
|
|
277
|
+
"[jsonc]": {
|
|
278
|
+
"editor.defaultFormatter": "biomejs.biome"
|
|
279
|
+
},
|
|
280
|
+
"editor.codeActionsOnSave": {
|
|
281
|
+
"source.organizeImports.biome": "explicit"
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
null,
|
|
285
|
+
" "
|
|
286
|
+
)}
|
|
287
|
+
`;
|
|
288
|
+
}
|
|
289
|
+
function readme(name, openApi) {
|
|
290
|
+
return `# ${name}
|
|
291
|
+
|
|
292
|
+
Routa API generated with \`pnpm create routa-ts@latest\`.
|
|
293
|
+
|
|
294
|
+
## Development
|
|
295
|
+
|
|
296
|
+
\`\`\`sh
|
|
297
|
+
pnpm install
|
|
298
|
+
pnpm dev
|
|
299
|
+
\`\`\`
|
|
300
|
+
|
|
301
|
+
\`pnpm dev\` runs \`routa dev\`, which validates the route graph, generates Routa metadata, typechecks, and starts the internal development server.
|
|
302
|
+
|
|
303
|
+
## Scripts
|
|
304
|
+
|
|
305
|
+
\`\`\`sh
|
|
306
|
+
pnpm dev
|
|
307
|
+
pnpm start
|
|
308
|
+
pnpm check
|
|
309
|
+
pnpm build
|
|
310
|
+
pnpm lint
|
|
311
|
+
pnpm format
|
|
312
|
+
${openApi ? "pnpm openapi:check\n" : ""}\`\`\`
|
|
313
|
+
|
|
314
|
+
## Routes
|
|
315
|
+
|
|
316
|
+
\`src/routa.ts\` is the user-owned Routa entry point. Routes live in \`src/routes\`.
|
|
317
|
+
|
|
318
|
+
\`\`\`txt
|
|
319
|
+
src/routa.ts
|
|
320
|
+
src/routes/status/route.ts
|
|
321
|
+
src/routes/status/schemas.ts
|
|
322
|
+
\`\`\`
|
|
323
|
+
|
|
324
|
+
${openApi ? "Routa owns generated project metadata in `.routa/`. Commit those files so OpenAPI drift and regeneration safety work across machines." : "This starter was created without OpenAPI files. Add `openapi.yaml` later and run `routa scaffold openapi.yaml` if you want generated route metadata."}
|
|
325
|
+
`;
|
|
326
|
+
}
|
|
327
|
+
function biomeJson() {
|
|
328
|
+
return `${JSON.stringify(
|
|
329
|
+
{
|
|
330
|
+
$schema: "./node_modules/@biomejs/biome/configuration_schema.json",
|
|
331
|
+
root: false,
|
|
332
|
+
vcs: {
|
|
333
|
+
enabled: true,
|
|
334
|
+
clientKind: "git",
|
|
335
|
+
useIgnoreFile: true
|
|
336
|
+
},
|
|
337
|
+
files: {
|
|
338
|
+
ignoreUnknown: true,
|
|
339
|
+
includes: ["**", "!node_modules", "!dist", "!coverage", "!.routa"]
|
|
340
|
+
},
|
|
341
|
+
formatter: {
|
|
342
|
+
enabled: true,
|
|
343
|
+
indentStyle: "tab",
|
|
344
|
+
lineWidth: 100
|
|
345
|
+
},
|
|
346
|
+
linter: {
|
|
347
|
+
enabled: true,
|
|
348
|
+
rules: {
|
|
349
|
+
preset: "recommended",
|
|
350
|
+
correctness: {
|
|
351
|
+
useImportExtensions: "off"
|
|
352
|
+
},
|
|
353
|
+
suspicious: {
|
|
354
|
+
noConsole: "off",
|
|
355
|
+
noExplicitAny: "off"
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
},
|
|
359
|
+
assist: {
|
|
360
|
+
enabled: true,
|
|
361
|
+
actions: {
|
|
362
|
+
source: {
|
|
363
|
+
recommended: true
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
null,
|
|
369
|
+
" "
|
|
370
|
+
)}
|
|
371
|
+
`;
|
|
372
|
+
}
|
|
373
|
+
function routaSource() {
|
|
374
|
+
return `import { createRouta } from "@routa-ts/core";
|
|
375
|
+
|
|
376
|
+
export default createRouta({
|
|
377
|
+
port: 3000,
|
|
378
|
+
});
|
|
379
|
+
`;
|
|
380
|
+
}
|
|
381
|
+
function openApiYaml() {
|
|
382
|
+
return `openapi: 3.1.0
|
|
383
|
+
info:
|
|
384
|
+
title: Routa API
|
|
385
|
+
version: 0.0.0
|
|
386
|
+
paths:
|
|
387
|
+
/status:
|
|
388
|
+
get:
|
|
389
|
+
operationId: getStatus
|
|
390
|
+
responses:
|
|
391
|
+
"200":
|
|
392
|
+
description: Service status
|
|
393
|
+
content:
|
|
394
|
+
application/json:
|
|
395
|
+
schema:
|
|
396
|
+
type: object
|
|
397
|
+
required:
|
|
398
|
+
- ok
|
|
399
|
+
properties:
|
|
400
|
+
ok:
|
|
401
|
+
type: boolean
|
|
402
|
+
`;
|
|
403
|
+
}
|
|
404
|
+
function statusRouteSource() {
|
|
405
|
+
return `import { createRoute, defineRoute } from "@routa-ts/core";
|
|
406
|
+
import { GetStatusResponse } from "./schemas.js";
|
|
407
|
+
|
|
408
|
+
export default defineRoute({
|
|
409
|
+
get: createRoute({
|
|
410
|
+
responses: {
|
|
411
|
+
success: {
|
|
412
|
+
status: 200,
|
|
413
|
+
schema: GetStatusResponse,
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
run: () => ({ type: "success", data: { ok: true } }),
|
|
417
|
+
}),
|
|
418
|
+
});
|
|
419
|
+
`;
|
|
420
|
+
}
|
|
421
|
+
function statusSchemasSource() {
|
|
422
|
+
return `import { z } from "zod";
|
|
423
|
+
|
|
424
|
+
export const GetStatusResponse = z.object({
|
|
425
|
+
ok: z.boolean(),
|
|
426
|
+
});
|
|
427
|
+
`;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/ui.ts
|
|
431
|
+
function shouldUseColor() {
|
|
432
|
+
if ("FORCE_COLOR" in process.env) {
|
|
433
|
+
return process.env.FORCE_COLOR !== "0";
|
|
434
|
+
}
|
|
435
|
+
if ("NO_COLOR" in process.env || process.env.CI) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
return Boolean(process.stdout.isTTY);
|
|
439
|
+
}
|
|
440
|
+
function createUi(color = false) {
|
|
441
|
+
return {
|
|
442
|
+
heading: style(color, "1;36"),
|
|
443
|
+
command: style(color, "36"),
|
|
444
|
+
success: style(color, "32"),
|
|
445
|
+
warn: style(color, "33"),
|
|
446
|
+
error: style(color, "31"),
|
|
447
|
+
muted: style(color, "2")
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function style(color, code) {
|
|
451
|
+
return (value) => color ? `\x1B[${code}m${value}\x1B[0m` : value;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// src/index.ts
|
|
455
|
+
function createCommandArgs(argv) {
|
|
456
|
+
return ["create", ...argv];
|
|
457
|
+
}
|
|
458
|
+
async function runCreate(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
459
|
+
const config = await resolveCreateConfig(argv, cwd);
|
|
460
|
+
const ui = createUi(shouldUseColor());
|
|
461
|
+
try {
|
|
462
|
+
printSummary(config, ui);
|
|
463
|
+
if (config.interactive && config.prompted && !config.yes && !await confirm("Continue with these settings?", true)) {
|
|
464
|
+
process.stdout.write(`${ui.muted("Creation cancelled.")}
|
|
465
|
+
`);
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
const result = createProject(config.targetDir, cwd, {
|
|
469
|
+
openApi: config.openApi,
|
|
470
|
+
routaVersion: resolveRoutaVersion(config.cwd, config.targetDir)
|
|
471
|
+
});
|
|
472
|
+
if (config.git) {
|
|
473
|
+
const git = spawnSync("git", ["init"], {
|
|
474
|
+
cwd: result.projectDir,
|
|
475
|
+
encoding: "utf8"
|
|
476
|
+
});
|
|
477
|
+
if (git.status === 0) {
|
|
478
|
+
process.stdout.write(`${ui.success("Initialized git repository.")}
|
|
479
|
+
`);
|
|
480
|
+
} else {
|
|
481
|
+
process.stderr.write(`${ui.error("git init failed.")} ${git.stderr ?? ""}
|
|
482
|
+
`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
if (config.install) {
|
|
486
|
+
const install = spawnSync("pnpm", ["install"], {
|
|
487
|
+
cwd: result.projectDir,
|
|
488
|
+
encoding: "utf8",
|
|
489
|
+
stdio: "inherit"
|
|
490
|
+
});
|
|
491
|
+
if (install.status !== 0) {
|
|
492
|
+
process.stderr.write(
|
|
493
|
+
`${ui.error('Command "pnpm install" did not run successfully.')} Please run this manually in your project.
|
|
494
|
+
`
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
process.stdout.write(
|
|
499
|
+
`
|
|
500
|
+
${ui.success(`Your Routa app is ready in '${config.targetDir}'.`)}
|
|
501
|
+
|
|
502
|
+
`
|
|
503
|
+
);
|
|
504
|
+
process.stdout.write("Use the following commands to start your app:\n");
|
|
505
|
+
process.stdout.write(`${ui.command(`cd ${config.targetDir}`)}
|
|
506
|
+
`);
|
|
507
|
+
if (!config.install) {
|
|
508
|
+
process.stdout.write(`${ui.command("pnpm install")}
|
|
509
|
+
`);
|
|
510
|
+
}
|
|
511
|
+
process.stdout.write(`${ui.command("pnpm dev")}
|
|
512
|
+
`);
|
|
513
|
+
return 0;
|
|
514
|
+
} catch (error) {
|
|
515
|
+
process.stderr.write(
|
|
516
|
+
`${ui.error("Error:")} ${error instanceof Error ? error.message : String(error)}
|
|
517
|
+
`
|
|
518
|
+
);
|
|
519
|
+
return 1;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function resolveRoutaVersion(cwd, targetDir) {
|
|
523
|
+
const workspaceRoot = findWorkspaceRoot(cwd);
|
|
524
|
+
if (!workspaceRoot) {
|
|
525
|
+
return "latest";
|
|
526
|
+
}
|
|
527
|
+
const workspaceConfig = readFileSync(join2(workspaceRoot, "pnpm-workspace.yaml"), "utf8");
|
|
528
|
+
const projectPath = resolve2(cwd, targetDir);
|
|
529
|
+
const projectRelativePath = relative(workspaceRoot, projectPath).split(sep).join("/");
|
|
530
|
+
if (workspaceConfig.includes("examples/*") && projectRelativePath.startsWith("examples/")) {
|
|
531
|
+
return "workspace:*";
|
|
532
|
+
}
|
|
533
|
+
return "latest";
|
|
534
|
+
}
|
|
535
|
+
function findWorkspaceRoot(cwd) {
|
|
536
|
+
let current = resolve2(cwd);
|
|
537
|
+
while (true) {
|
|
538
|
+
if (existsSync2(join2(current, "pnpm-workspace.yaml"))) {
|
|
539
|
+
return current;
|
|
540
|
+
}
|
|
541
|
+
const parent = dirname2(current);
|
|
542
|
+
if (parent === current) {
|
|
543
|
+
return void 0;
|
|
544
|
+
}
|
|
545
|
+
current = parent;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
async function resolveCreateConfig(argv, cwd) {
|
|
549
|
+
const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
550
|
+
const targetArg = argv.find((item) => !item.startsWith("-"));
|
|
551
|
+
let targetDir = targetArg ?? "routa-app";
|
|
552
|
+
let prompted = false;
|
|
553
|
+
if (interactive && !targetArg) {
|
|
554
|
+
const answer = await question("Project name (leave empty to use routa-app)");
|
|
555
|
+
prompted = true;
|
|
556
|
+
targetDir = answer.trim() || targetDir;
|
|
557
|
+
}
|
|
558
|
+
const openApi = await flagOrPrompt(
|
|
559
|
+
argv,
|
|
560
|
+
"--openapi",
|
|
561
|
+
"--no-openapi",
|
|
562
|
+
"Include a starter OpenAPI file?",
|
|
563
|
+
true
|
|
564
|
+
);
|
|
565
|
+
const git = await flagOrPrompt(
|
|
566
|
+
argv,
|
|
567
|
+
"--git",
|
|
568
|
+
"--no-git",
|
|
569
|
+
"Initialize a new git repository?",
|
|
570
|
+
interactive
|
|
571
|
+
);
|
|
572
|
+
const install = await flagOrPrompt(
|
|
573
|
+
argv,
|
|
574
|
+
"--install",
|
|
575
|
+
"--no-install",
|
|
576
|
+
"Install dependencies?",
|
|
577
|
+
interactive
|
|
578
|
+
);
|
|
579
|
+
prompted = prompted || openApi.prompted || git.prompted || install.prompted;
|
|
580
|
+
return {
|
|
581
|
+
targetDir,
|
|
582
|
+
openApi: openApi.value,
|
|
583
|
+
git: git.value,
|
|
584
|
+
install: install.value,
|
|
585
|
+
interactive,
|
|
586
|
+
prompted,
|
|
587
|
+
yes: argv.includes("--yes") || argv.includes("-y"),
|
|
588
|
+
cwd
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
async function flagOrPrompt(argv, enabledFlag, disabledFlag, label, defaultValue) {
|
|
592
|
+
if (argv.includes(enabledFlag)) {
|
|
593
|
+
return { value: true, prompted: false };
|
|
594
|
+
}
|
|
595
|
+
if (argv.includes(disabledFlag)) {
|
|
596
|
+
return { value: false, prompted: false };
|
|
597
|
+
}
|
|
598
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
599
|
+
return { value: defaultValue, prompted: false };
|
|
600
|
+
}
|
|
601
|
+
return { value: await confirm(label, defaultValue), prompted: true };
|
|
602
|
+
}
|
|
603
|
+
async function question(label) {
|
|
604
|
+
const prompt = createInterface({
|
|
605
|
+
input: process.stdin,
|
|
606
|
+
output: process.stdout
|
|
607
|
+
});
|
|
608
|
+
try {
|
|
609
|
+
return await prompt.question(`${label}
|
|
610
|
+
> `);
|
|
611
|
+
} finally {
|
|
612
|
+
prompt.close();
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
async function confirm(label, defaultValue) {
|
|
616
|
+
const suffix = defaultValue ? "Y/n" : "y/N";
|
|
617
|
+
const answer = (await question(`${label} (${suffix})`)).trim().toLowerCase();
|
|
618
|
+
if (!answer) {
|
|
619
|
+
return defaultValue;
|
|
620
|
+
}
|
|
621
|
+
return ["y", "yes"].includes(answer);
|
|
622
|
+
}
|
|
623
|
+
function printSummary(config, ui) {
|
|
624
|
+
process.stdout.write(`${ui.heading("Let's configure your Routa API")}
|
|
625
|
+
|
|
626
|
+
`);
|
|
627
|
+
process.stdout.write(`${ui.muted("About to create:")}
|
|
628
|
+
|
|
629
|
+
`);
|
|
630
|
+
process.stdout.write(` Project: ${config.targetDir}
|
|
631
|
+
`);
|
|
632
|
+
process.stdout.write(` Location: ${config.cwd}/${config.targetDir}
|
|
633
|
+
`);
|
|
634
|
+
process.stdout.write(" Package manager: pnpm\n");
|
|
635
|
+
process.stdout.write(" Toolchain: TypeScript, Biome\n");
|
|
636
|
+
process.stdout.write(` OpenAPI starter: ${config.openApi ? "yes" : "no"}
|
|
637
|
+
`);
|
|
638
|
+
process.stdout.write(` Initialize git: ${config.git ? "yes" : "no"}
|
|
639
|
+
`);
|
|
640
|
+
process.stdout.write(` Install deps: ${config.install ? "yes" : "no"}
|
|
641
|
+
|
|
642
|
+
`);
|
|
643
|
+
}
|
|
644
|
+
export {
|
|
645
|
+
createCommandArgs,
|
|
646
|
+
createProject,
|
|
647
|
+
createUi,
|
|
648
|
+
resolveRoutaVersion,
|
|
649
|
+
runCreate,
|
|
650
|
+
shouldUseColor
|
|
651
|
+
};
|
package/dist/ui.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Ui = {
|
|
2
|
+
heading: (value: string) => string;
|
|
3
|
+
command: (value: string) => string;
|
|
4
|
+
success: (value: string) => string;
|
|
5
|
+
warn: (value: string) => string;
|
|
6
|
+
error: (value: string) => string;
|
|
7
|
+
muted: (value: string) => string;
|
|
8
|
+
};
|
|
9
|
+
export declare function shouldUseColor(): boolean;
|
|
10
|
+
export declare function createUi(color?: boolean): Ui;
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-routa-ts",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a new Routa API project",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/joseAcevesG/Routa.git",
|
|
11
|
+
"directory": "packages/create-routa-ts"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"bin": {
|
|
20
|
+
"create-routa-ts": "./dist/cli.js"
|
|
21
|
+
},
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"import": "./dist/index.js",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "node ../../scripts/build-package.mjs create-routa-ts",
|
|
34
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
35
|
+
"lint": "biome lint src",
|
|
36
|
+
"test": "vitest run"
|
|
37
|
+
}
|
|
38
|
+
}
|