@pracht/cli 0.0.1 → 1.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/CHANGELOG.md +47 -0
- package/README.md +70 -3
- package/bin/pracht.js +23 -451
- package/lib/build-metadata.js +73 -0
- package/lib/build-shared.js +127 -0
- package/lib/cli.js +164 -0
- package/lib/commands/build.js +104 -0
- package/lib/commands/dev.js +16 -0
- package/lib/commands/doctor.js +21 -0
- package/lib/commands/generate.js +572 -0
- package/lib/commands/inspect.js +163 -0
- package/lib/commands/verify.js +21 -0
- package/lib/constants.js +22 -0
- package/lib/manifest.js +147 -0
- package/lib/project.js +143 -0
- package/lib/verification.js +641 -0
- package/package.json +6 -6
- package/test/pracht-cli.test.js +633 -0
|
@@ -0,0 +1,572 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ensureTrailingNewline,
|
|
5
|
+
parseApiMethods,
|
|
6
|
+
parseCommaList,
|
|
7
|
+
parseFlags,
|
|
8
|
+
printGenerateHelp,
|
|
9
|
+
quote,
|
|
10
|
+
requireEnumOption,
|
|
11
|
+
requireOptionalString,
|
|
12
|
+
requirePositiveIntegerOption,
|
|
13
|
+
requireStringOption,
|
|
14
|
+
} from "../cli.js";
|
|
15
|
+
import {
|
|
16
|
+
extractRegistryEntries,
|
|
17
|
+
insertArrayItem,
|
|
18
|
+
toManifestModulePath,
|
|
19
|
+
upsertObjectEntry,
|
|
20
|
+
ensureCoreNamedImport,
|
|
21
|
+
} from "../manifest.js";
|
|
22
|
+
import {
|
|
23
|
+
assertFileExists,
|
|
24
|
+
displayPath,
|
|
25
|
+
readProjectConfig,
|
|
26
|
+
resolveApiModulePath,
|
|
27
|
+
resolvePagesRouteModulePath,
|
|
28
|
+
resolveProjectPath,
|
|
29
|
+
resolveRouteModulePath,
|
|
30
|
+
resolveScopedFile,
|
|
31
|
+
writeGeneratedFile,
|
|
32
|
+
} from "../project.js";
|
|
33
|
+
|
|
34
|
+
export async function generateCommand(args) {
|
|
35
|
+
const [kind, ...rest] = args;
|
|
36
|
+
if (!kind || kind === "--help" || kind === "-h") {
|
|
37
|
+
printGenerateHelp();
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const options = parseFlags(rest);
|
|
42
|
+
const project = readProjectConfig(process.cwd());
|
|
43
|
+
const result = runGenerate(kind, options, project);
|
|
44
|
+
|
|
45
|
+
if (options.json) {
|
|
46
|
+
console.log(JSON.stringify({ ok: true, ...result }, null, 2));
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
console.log(`Created ${result.kind}:`);
|
|
51
|
+
for (const file of result.created) {
|
|
52
|
+
console.log(` ${file}`);
|
|
53
|
+
}
|
|
54
|
+
for (const file of result.updated) {
|
|
55
|
+
console.log(` updated ${file}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function runGenerate(kind, options, project) {
|
|
60
|
+
if (kind === "route") {
|
|
61
|
+
return generateRoute(options, project);
|
|
62
|
+
}
|
|
63
|
+
if (kind === "shell") {
|
|
64
|
+
return generateShell(options, project);
|
|
65
|
+
}
|
|
66
|
+
if (kind === "middleware") {
|
|
67
|
+
return generateMiddleware(options, project);
|
|
68
|
+
}
|
|
69
|
+
if (kind === "api") {
|
|
70
|
+
return generateApi(options, project);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
throw new Error(`Unknown generate kind: ${kind}`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function generateRoute(options, project) {
|
|
77
|
+
const routePath = normalizeRoutePathString(requireStringOption(options, "path"));
|
|
78
|
+
const render = requireEnumOption(options, "render", ["spa", "ssr", "ssg", "isg"], "ssr");
|
|
79
|
+
const includeLoader = Boolean(options.loader);
|
|
80
|
+
const includeErrorBoundary = Boolean(options["error-boundary"]);
|
|
81
|
+
const middleware = parseCommaList(options.middleware);
|
|
82
|
+
const includeStaticPaths =
|
|
83
|
+
Boolean(options["static-paths"]) ||
|
|
84
|
+
(hasDynamicSegments(routePath) && (render === "ssg" || render === "isg"));
|
|
85
|
+
const title = requireOptionalString(options, "title") ?? titleFromPath(routePath);
|
|
86
|
+
|
|
87
|
+
if (project.mode === "pages") {
|
|
88
|
+
if (options.shell) {
|
|
89
|
+
throw new Error("`pracht generate route --shell` is only available for manifest apps.");
|
|
90
|
+
}
|
|
91
|
+
if (middleware.length > 0) {
|
|
92
|
+
throw new Error("`pracht generate route --middleware` is only available for manifest apps.");
|
|
93
|
+
}
|
|
94
|
+
return generatePagesRoute({
|
|
95
|
+
includeErrorBoundary,
|
|
96
|
+
includeLoader,
|
|
97
|
+
includeStaticPaths,
|
|
98
|
+
project,
|
|
99
|
+
render,
|
|
100
|
+
routePath,
|
|
101
|
+
title,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const manifestPath = resolveProjectPath(project.root, project.appFile);
|
|
106
|
+
assertFileExists(manifestPath, `App manifest not found at ${project.appFile}.`);
|
|
107
|
+
|
|
108
|
+
const manifestSource = readFileSync(manifestPath, "utf-8");
|
|
109
|
+
const registeredShells = new Set(
|
|
110
|
+
extractRegistryEntries(manifestSource, "shells").map((entry) => entry.name),
|
|
111
|
+
);
|
|
112
|
+
const registeredMiddleware = new Set(
|
|
113
|
+
extractRegistryEntries(manifestSource, "middleware").map((entry) => entry.name),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const shellName = requireOptionalString(options, "shell");
|
|
117
|
+
if (shellName && !registeredShells.has(shellName)) {
|
|
118
|
+
throw new Error(`Shell "${shellName}" is not registered in ${project.appFile}.`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const name of middleware) {
|
|
122
|
+
if (!registeredMiddleware.has(name)) {
|
|
123
|
+
throw new Error(`Middleware "${name}" is not registered in ${project.appFile}.`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const routeFile = resolveRouteModulePath(project, routePath, ".tsx");
|
|
128
|
+
writeGeneratedFile(
|
|
129
|
+
routeFile.absolutePath,
|
|
130
|
+
buildManifestRouteModuleSource({
|
|
131
|
+
includeErrorBoundary,
|
|
132
|
+
includeLoader,
|
|
133
|
+
includeStaticPaths,
|
|
134
|
+
routePath,
|
|
135
|
+
title,
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
let nextManifestSource = ensureCoreNamedImport(manifestSource, "route");
|
|
140
|
+
if (render === "isg") {
|
|
141
|
+
nextManifestSource = ensureCoreNamedImport(nextManifestSource, "timeRevalidate");
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const routeModulePath = toManifestModulePath(manifestPath, routeFile.absolutePath);
|
|
145
|
+
const routeId = routeIdFromPath(routePath);
|
|
146
|
+
const meta = [`id: ${quote(routeId)}`, `render: ${quote(render)}`];
|
|
147
|
+
|
|
148
|
+
if (shellName) {
|
|
149
|
+
meta.push(`shell: ${quote(shellName)}`);
|
|
150
|
+
}
|
|
151
|
+
if (middleware.length > 0) {
|
|
152
|
+
meta.push(`middleware: [${middleware.map((item) => quote(item)).join(", ")}]`);
|
|
153
|
+
}
|
|
154
|
+
if (render === "isg") {
|
|
155
|
+
const seconds = requirePositiveIntegerOption(options, "revalidate", 3600);
|
|
156
|
+
meta.push(`revalidate: timeRevalidate(${seconds})`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
nextManifestSource = insertArrayItem(
|
|
160
|
+
nextManifestSource,
|
|
161
|
+
"routes",
|
|
162
|
+
[
|
|
163
|
+
`route(${quote(routePath)}, ${quote(routeModulePath)}, {`,
|
|
164
|
+
...meta.map((line) => ` ${line},`),
|
|
165
|
+
"})",
|
|
166
|
+
].join("\n"),
|
|
167
|
+
);
|
|
168
|
+
writeFileSync(manifestPath, ensureTrailingNewline(nextManifestSource), "utf-8");
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
created: [displayPath(project.root, routeFile.absolutePath)],
|
|
172
|
+
kind: "route",
|
|
173
|
+
updated: [displayPath(project.root, manifestPath)],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function generatePagesRoute({
|
|
178
|
+
includeErrorBoundary,
|
|
179
|
+
includeLoader,
|
|
180
|
+
includeStaticPaths,
|
|
181
|
+
project,
|
|
182
|
+
render,
|
|
183
|
+
routePath,
|
|
184
|
+
title,
|
|
185
|
+
}) {
|
|
186
|
+
const routeFile = resolvePagesRouteModulePath(project, routePath, ".tsx");
|
|
187
|
+
writeGeneratedFile(
|
|
188
|
+
routeFile.absolutePath,
|
|
189
|
+
buildPagesRouteModuleSource({
|
|
190
|
+
includeErrorBoundary,
|
|
191
|
+
includeLoader,
|
|
192
|
+
includeStaticPaths,
|
|
193
|
+
render,
|
|
194
|
+
routePath,
|
|
195
|
+
title,
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
created: [displayPath(project.root, routeFile.absolutePath)],
|
|
201
|
+
kind: "route",
|
|
202
|
+
updated: [],
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function generateShell(options, project) {
|
|
207
|
+
if (project.mode === "pages") {
|
|
208
|
+
throw new Error(
|
|
209
|
+
"Pages router apps use a single `_app` shell. `pracht generate shell` is only available for manifest apps.",
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const name = requireStringOption(options, "name");
|
|
214
|
+
const manifestPath = resolveProjectPath(project.root, project.appFile);
|
|
215
|
+
assertFileExists(manifestPath, `App manifest not found at ${project.appFile}.`);
|
|
216
|
+
|
|
217
|
+
const shellFile = resolveScopedFile(project.root, project.shellsDir, `${name}.tsx`);
|
|
218
|
+
writeGeneratedFile(shellFile, buildShellModuleSource(name));
|
|
219
|
+
|
|
220
|
+
const manifestSource = readFileSync(manifestPath, "utf-8");
|
|
221
|
+
const updatedSource = upsertObjectEntry(
|
|
222
|
+
manifestSource,
|
|
223
|
+
"shells",
|
|
224
|
+
`${name}: ${quote(toManifestModulePath(manifestPath, shellFile))}`,
|
|
225
|
+
);
|
|
226
|
+
writeFileSync(manifestPath, ensureTrailingNewline(updatedSource), "utf-8");
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
created: [displayPath(project.root, shellFile)],
|
|
230
|
+
kind: "shell",
|
|
231
|
+
updated: [displayPath(project.root, manifestPath)],
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function generateMiddleware(options, project) {
|
|
236
|
+
if (project.mode === "pages") {
|
|
237
|
+
throw new Error(
|
|
238
|
+
"Pages router apps do not use manifest middleware registration. `pracht generate middleware` is only available for manifest apps.",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const name = requireStringOption(options, "name");
|
|
243
|
+
const manifestPath = resolveProjectPath(project.root, project.appFile);
|
|
244
|
+
assertFileExists(manifestPath, `App manifest not found at ${project.appFile}.`);
|
|
245
|
+
|
|
246
|
+
const middlewareFile = resolveScopedFile(project.root, project.middlewareDir, `${name}.ts`);
|
|
247
|
+
writeGeneratedFile(middlewareFile, buildMiddlewareModuleSource());
|
|
248
|
+
|
|
249
|
+
const manifestSource = readFileSync(manifestPath, "utf-8");
|
|
250
|
+
const updatedSource = upsertObjectEntry(
|
|
251
|
+
manifestSource,
|
|
252
|
+
"middleware",
|
|
253
|
+
`${name}: ${quote(toManifestModulePath(manifestPath, middlewareFile))}`,
|
|
254
|
+
);
|
|
255
|
+
writeFileSync(manifestPath, ensureTrailingNewline(updatedSource), "utf-8");
|
|
256
|
+
|
|
257
|
+
return {
|
|
258
|
+
created: [displayPath(project.root, middlewareFile)],
|
|
259
|
+
kind: "middleware",
|
|
260
|
+
updated: [displayPath(project.root, manifestPath)],
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function generateApi(options, project) {
|
|
265
|
+
const endpointPath = normalizeApiPath(requireStringOption(options, "path"));
|
|
266
|
+
const methods = parseApiMethods(options.methods);
|
|
267
|
+
const apiFile = resolveApiModulePath(project, endpointPath);
|
|
268
|
+
writeGeneratedFile(apiFile.absolutePath, buildApiRouteSource({ endpointPath, methods }));
|
|
269
|
+
|
|
270
|
+
return {
|
|
271
|
+
created: [displayPath(project.root, apiFile.absolutePath)],
|
|
272
|
+
kind: "api",
|
|
273
|
+
updated: [],
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function buildManifestRouteModuleSource({
|
|
278
|
+
includeErrorBoundary,
|
|
279
|
+
includeLoader,
|
|
280
|
+
includeStaticPaths,
|
|
281
|
+
routePath,
|
|
282
|
+
title,
|
|
283
|
+
}) {
|
|
284
|
+
const params = dynamicParamNames(routePath);
|
|
285
|
+
const imports = [];
|
|
286
|
+
const sections = [];
|
|
287
|
+
|
|
288
|
+
if (includeLoader) {
|
|
289
|
+
imports.push("LoaderArgs", "RouteComponentProps");
|
|
290
|
+
}
|
|
291
|
+
if (includeErrorBoundary) {
|
|
292
|
+
imports.push("ErrorBoundaryProps");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (imports.length > 0) {
|
|
296
|
+
sections.push(`import type { ${imports.join(", ")} } from "@pracht/core";`);
|
|
297
|
+
sections.push("");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (includeLoader) {
|
|
301
|
+
sections.push(
|
|
302
|
+
"export async function loader(_args: LoaderArgs) {",
|
|
303
|
+
` return { message: ${quote(`Welcome to ${title}.`)} };`,
|
|
304
|
+
"}",
|
|
305
|
+
"",
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (includeStaticPaths) {
|
|
310
|
+
sections.push(
|
|
311
|
+
"export function getStaticPaths() {",
|
|
312
|
+
` return [${buildStaticPathsStub(params)}];`,
|
|
313
|
+
"}",
|
|
314
|
+
"",
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
sections.push("export function head() {", ` return { title: ${quote(title)} };`, "}", "");
|
|
319
|
+
|
|
320
|
+
if (includeLoader) {
|
|
321
|
+
sections.push(
|
|
322
|
+
"export function Component({ data }: RouteComponentProps<typeof loader>) {",
|
|
323
|
+
" return (",
|
|
324
|
+
" <section>",
|
|
325
|
+
` <h1>${escapeJsxText(title)}</h1>`,
|
|
326
|
+
" <p>{data.message}</p>",
|
|
327
|
+
" </section>",
|
|
328
|
+
" );",
|
|
329
|
+
"}",
|
|
330
|
+
);
|
|
331
|
+
} else {
|
|
332
|
+
sections.push(
|
|
333
|
+
"export function Component() {",
|
|
334
|
+
" return (",
|
|
335
|
+
" <section>",
|
|
336
|
+
` <h1>${escapeJsxText(title)}</h1>`,
|
|
337
|
+
" </section>",
|
|
338
|
+
" );",
|
|
339
|
+
"}",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (includeErrorBoundary) {
|
|
344
|
+
sections.push(
|
|
345
|
+
"",
|
|
346
|
+
"export function ErrorBoundary({ error }: ErrorBoundaryProps) {",
|
|
347
|
+
" return <p>{error.message}</p>;",
|
|
348
|
+
"}",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return `${sections.join("\n")}\n`;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
function buildPagesRouteModuleSource({
|
|
356
|
+
includeErrorBoundary,
|
|
357
|
+
includeLoader,
|
|
358
|
+
includeStaticPaths,
|
|
359
|
+
render,
|
|
360
|
+
routePath,
|
|
361
|
+
title,
|
|
362
|
+
}) {
|
|
363
|
+
const params = dynamicParamNames(routePath);
|
|
364
|
+
const imports = [];
|
|
365
|
+
const sections = [];
|
|
366
|
+
|
|
367
|
+
if (includeLoader) {
|
|
368
|
+
imports.push("LoaderArgs", "RouteComponentProps");
|
|
369
|
+
}
|
|
370
|
+
if (includeErrorBoundary) {
|
|
371
|
+
imports.push("ErrorBoundaryProps");
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (imports.length > 0) {
|
|
375
|
+
sections.push(`import type { ${imports.join(", ")} } from "@pracht/core";`);
|
|
376
|
+
sections.push("");
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
sections.push(`export const RENDER_MODE = ${quote(render)};`, "");
|
|
380
|
+
|
|
381
|
+
if (includeLoader) {
|
|
382
|
+
sections.push(
|
|
383
|
+
"export async function loader(_args: LoaderArgs) {",
|
|
384
|
+
` return { message: ${quote(`Welcome to ${title}.`)} };`,
|
|
385
|
+
"}",
|
|
386
|
+
"",
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (includeStaticPaths) {
|
|
391
|
+
sections.push(
|
|
392
|
+
"export function getStaticPaths() {",
|
|
393
|
+
` return [${buildStaticPathsStub(params)}];`,
|
|
394
|
+
"}",
|
|
395
|
+
"",
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (includeLoader) {
|
|
400
|
+
sections.push(
|
|
401
|
+
"export function Component({ data }: RouteComponentProps<typeof loader>) {",
|
|
402
|
+
" return (",
|
|
403
|
+
" <section>",
|
|
404
|
+
` <h1>${escapeJsxText(title)}</h1>`,
|
|
405
|
+
" <p>{data.message}</p>",
|
|
406
|
+
" </section>",
|
|
407
|
+
" );",
|
|
408
|
+
"}",
|
|
409
|
+
);
|
|
410
|
+
} else {
|
|
411
|
+
sections.push(
|
|
412
|
+
"export function Component() {",
|
|
413
|
+
" return (",
|
|
414
|
+
" <section>",
|
|
415
|
+
` <h1>${escapeJsxText(title)}</h1>`,
|
|
416
|
+
" </section>",
|
|
417
|
+
" );",
|
|
418
|
+
"}",
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (includeErrorBoundary) {
|
|
423
|
+
sections.push(
|
|
424
|
+
"",
|
|
425
|
+
"export function ErrorBoundary({ error }: ErrorBoundaryProps) {",
|
|
426
|
+
" return <p>{error.message}</p>;",
|
|
427
|
+
"}",
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return `${sections.join("\n")}\n`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function buildShellModuleSource(name) {
|
|
435
|
+
const title = titleCase(name);
|
|
436
|
+
return [
|
|
437
|
+
'import type { ShellProps } from "@pracht/core";',
|
|
438
|
+
"",
|
|
439
|
+
"export function Shell({ children }: ShellProps) {",
|
|
440
|
+
" return (",
|
|
441
|
+
` <div class=${quote(`${name}-shell`)}>`,
|
|
442
|
+
" <main>{children}</main>",
|
|
443
|
+
" </div>",
|
|
444
|
+
" );",
|
|
445
|
+
"}",
|
|
446
|
+
"",
|
|
447
|
+
"export function head() {",
|
|
448
|
+
` return { title: ${quote(title)} };`,
|
|
449
|
+
"}",
|
|
450
|
+
"",
|
|
451
|
+
].join("\n");
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function buildMiddlewareModuleSource() {
|
|
455
|
+
return [
|
|
456
|
+
'import type { MiddlewareFn } from "@pracht/core";',
|
|
457
|
+
"",
|
|
458
|
+
"export const middleware: MiddlewareFn = async (_args) => {",
|
|
459
|
+
" return;",
|
|
460
|
+
"};",
|
|
461
|
+
"",
|
|
462
|
+
].join("\n");
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function buildApiRouteSource({ endpointPath, methods }) {
|
|
466
|
+
const methodLines = methods.flatMap((method, index) => {
|
|
467
|
+
const lines = buildApiMethodSource(method, methods, endpointPath);
|
|
468
|
+
if (index === methods.length - 1) return lines;
|
|
469
|
+
return [...lines, ""];
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
return ['import type { BaseRouteArgs } from "@pracht/core";', "", ...methodLines, ""].join("\n");
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function buildApiMethodSource(method, methods, endpointPath) {
|
|
476
|
+
if (method === "DELETE" || method === "HEAD") {
|
|
477
|
+
return [
|
|
478
|
+
`export function ${method}(_args: BaseRouteArgs) {`,
|
|
479
|
+
" return new Response(null, { status: 204 });",
|
|
480
|
+
"}",
|
|
481
|
+
];
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (method === "OPTIONS") {
|
|
485
|
+
return [
|
|
486
|
+
`export function ${method}(_args: BaseRouteArgs) {`,
|
|
487
|
+
" return new Response(null, {",
|
|
488
|
+
` headers: { allow: ${quote(methods.join(", "))} },`,
|
|
489
|
+
" status: 204,",
|
|
490
|
+
" });",
|
|
491
|
+
"}",
|
|
492
|
+
];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (method === "GET") {
|
|
496
|
+
return [
|
|
497
|
+
`export function ${method}(_args: BaseRouteArgs) {`,
|
|
498
|
+
` return Response.json({ endpoint: ${quote(`/api${endpointPath}`)}, ok: true });`,
|
|
499
|
+
"}",
|
|
500
|
+
];
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const status = method === "POST" ? 201 : 200;
|
|
504
|
+
return [
|
|
505
|
+
`export async function ${method}({ request }: BaseRouteArgs) {`,
|
|
506
|
+
" const body = await request.json();",
|
|
507
|
+
` return Response.json({ body, ok: true }, { status: ${status} });`,
|
|
508
|
+
"}",
|
|
509
|
+
];
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function normalizeRoutePathString(value) {
|
|
513
|
+
if (!value || value === "/") return "/";
|
|
514
|
+
const normalized = `/${value}`.replace(/\/+/g, "/");
|
|
515
|
+
return normalized !== "/" && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function normalizeApiPath(value) {
|
|
519
|
+
const normalized = normalizeRoutePathString(value).replace(/^\/api(?=\/|$)/, "");
|
|
520
|
+
return normalized || "/";
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function hasDynamicSegments(routePath) {
|
|
524
|
+
return routePath.split("/").some((segment) => segment.startsWith(":") || segment === "*");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function dynamicParamNames(routePath) {
|
|
528
|
+
return routePath
|
|
529
|
+
.split("/")
|
|
530
|
+
.filter(Boolean)
|
|
531
|
+
.map((segment) => {
|
|
532
|
+
if (segment.startsWith(":")) return segment.slice(1);
|
|
533
|
+
if (segment === "*") return "slug";
|
|
534
|
+
return null;
|
|
535
|
+
})
|
|
536
|
+
.filter(Boolean);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function routeIdFromPath(routePath) {
|
|
540
|
+
if (routePath === "/") return "index";
|
|
541
|
+
return routePath
|
|
542
|
+
.split("/")
|
|
543
|
+
.filter(Boolean)
|
|
544
|
+
.map((segment) => segment.replace(/^:/, "").replace(/\*/g, "splat"))
|
|
545
|
+
.join("-");
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function titleFromPath(routePath) {
|
|
549
|
+
if (routePath === "/") return "Home";
|
|
550
|
+
const lastSegment = routePath.split("/").filter(Boolean).at(-1) ?? "Page";
|
|
551
|
+
return titleCase(lastSegment.replace(/^:/, "").replace(/\*/g, "slug"));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function titleCase(value) {
|
|
555
|
+
return value
|
|
556
|
+
.split(/[-_/]/)
|
|
557
|
+
.filter(Boolean)
|
|
558
|
+
.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))
|
|
559
|
+
.join(" ");
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function buildStaticPathsStub(params) {
|
|
563
|
+
if (params.length === 0) {
|
|
564
|
+
return "{}";
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return `{ ${params.map((name) => `${name}: ${quote(`example-${name}`)}`).join(", ")} }`;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function escapeJsxText(value) {
|
|
571
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
572
|
+
}
|