@pracht/cli 1.2.0 → 1.2.2
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 +85 -0
- package/dist/{build-BzQAQjuy.mjs → build-DkP2ffu9.mjs} +11 -1
- package/dist/{build-metadata-IABPFFKk.mjs → build-metadata-BOChHO8g.mjs} +11 -2
- package/dist/{doctor-BcoqyBk-.mjs → doctor-CwQJjJ83.mjs} +1 -1
- package/dist/{generate-BXkePCIx.mjs → generate-C9AMSGsv.mjs} +141 -142
- package/dist/index.mjs +5 -5
- package/dist/{inspect-DIjjCfs3.mjs → inspect-MseHPU9s.mjs} +4 -9
- package/dist/{project-DydjqBoS.mjs → project-BdMiN3s7.mjs} +5 -12
- package/dist/{verification-Dfl3X4Zo.mjs → verification-Dwf1ZuNn.mjs} +116 -108
- package/dist/{verify-CObGxWtW.mjs → verify-o0G5Xuff.mjs} +1 -1
- package/package.json +9 -2
- package/src/build-metadata.ts +0 -86
- package/src/build-shared.ts +0 -179
- package/src/commands/build.ts +0 -132
- package/src/commands/dev.ts +0 -27
- package/src/commands/doctor.ts +0 -33
- package/src/commands/generate.ts +0 -678
- package/src/commands/inspect.ts +0 -214
- package/src/commands/verify.ts +0 -37
- package/src/constants.ts +0 -32
- package/src/index.ts +0 -21
- package/src/manifest.ts +0 -166
- package/src/project.ts +0 -171
- package/src/utils.ts +0 -72
- package/src/verification.ts +0 -710
- package/test/pracht-cli.test.js +0 -633
- package/tsdown.config.ts +0 -8
- /package/dist/{manifest-DGq1n5LT.mjs → manifest-Bs5hp3gA.mjs} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,90 @@
|
|
|
1
1
|
# @pracht/cli
|
|
2
2
|
|
|
3
|
+
## 1.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#124](https://github.com/JoviDeCroock/pracht/pull/124) [`8f662c0`](https://github.com/JoviDeCroock/pracht/commit/8f662c0b78b1911a7534ffd7aa4e919cf22a3a42) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Internal refactor: split several large modules into smaller, focused files to improve maintainability. Public APIs are unchanged.
|
|
8
|
+
|
|
9
|
+
- [#132](https://github.com/JoviDeCroock/pracht/pull/132) [`30d867f`](https://github.com/JoviDeCroock/pracht/commit/30d867f4a4cd41107a1ed60c607afe0d51848c3b) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Follow-up security hardening after the main audit fixes.
|
|
10
|
+
|
|
11
|
+
- `@pracht/adapter-node` now supports `canonicalOrigin` so apps can pin
|
|
12
|
+
`request.url` to a known public origin instead of depending on untrusted
|
|
13
|
+
`Host` values. The adapter also treats both `x-pracht-route-state-request`
|
|
14
|
+
and `?_data=1` as route-state transports before any static/ISG HTML serving,
|
|
15
|
+
and ISG regeneration now uses a clean HTML request instead of replaying the
|
|
16
|
+
triggering user's cookies or authorization headers.
|
|
17
|
+
- `@pracht/adapter-cloudflare` now bypasses static asset serving for both
|
|
18
|
+
route-state transports (`x-pracht-route-state-request` and `?_data=1`).
|
|
19
|
+
- `@pracht/cli` now emits a Vercel Build Output rule that sends `?_data=1`
|
|
20
|
+
requests to the render function before static rewrites can serve prerendered
|
|
21
|
+
HTML.
|
|
22
|
+
|
|
23
|
+
- [#131](https://github.com/JoviDeCroock/pracht/pull/131) [`015e987`](https://github.com/JoviDeCroock/pracht/commit/015e987a2de471980fab557e3dbf3d52937ad0ac) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Security hardening across request handling, redirects, and build output.
|
|
24
|
+
|
|
25
|
+
**Framework (`@pracht/core`)**
|
|
26
|
+
|
|
27
|
+
- **Middleware/loader redirects are now validated.** `javascript:`, `data:`,
|
|
28
|
+
`vbscript:`, `blob:`, and `file:` targets are refused server-side (they
|
|
29
|
+
were already refused on the client) and CR/LF in the `Location` value
|
|
30
|
+
throws instead of producing a split response. Non-safe-method redirects
|
|
31
|
+
now default to **303 See Other** rather than 302 so browsers don't
|
|
32
|
+
resend the POST body to the redirect target. `MiddlewareResult`'s
|
|
33
|
+
`redirect` form now accepts an optional `status` override.
|
|
34
|
+
- **CSRF protection for mutating API routes.** Non-GET API requests are
|
|
35
|
+
rejected with 403 unless the browser signals a same-origin/same-site
|
|
36
|
+
fetch (`Sec-Fetch-Site`) or the `Origin` header matches the request
|
|
37
|
+
URL's origin. Opt out per-app via `defineApp({ api: { requireSameOrigin: false } })`.
|
|
38
|
+
- **`_data=1` route-state bypass is now gated.** The query-param form of
|
|
39
|
+
the route-state endpoint now requires `Sec-Fetch-Site: same-origin`/
|
|
40
|
+
`same-site` (or a matching `Origin`). The explicit
|
|
41
|
+
`x-pracht-route-state-request` header is still accepted unconditionally
|
|
42
|
+
(CORS-protected).
|
|
43
|
+
- **Catch-all path traversal at build time is closed.**
|
|
44
|
+
`buildPathFromSegments` now percent-encodes catch-all components
|
|
45
|
+
individually and explicitly neutralises `.` / `..` segments, so a
|
|
46
|
+
`getStaticPaths` returning `{ "*": "../../etc/passwd" }` can no longer
|
|
47
|
+
escape `dist/client/` at SSG/ISG write time.
|
|
48
|
+
- **`headers()` values are validated for CR/LF.** `applyHeaders` now
|
|
49
|
+
throws a consistent framework error on response-splitting attempts,
|
|
50
|
+
regardless of adapter-specific Headers implementation behaviour.
|
|
51
|
+
- **`debugErrors` is ignored in production.** When `NODE_ENV=production`,
|
|
52
|
+
`debugErrors: true` is refused (with a one-shot console warning) so a
|
|
53
|
+
misconfigured deploy cannot leak stack traces and module paths.
|
|
54
|
+
|
|
55
|
+
**Adapter (`@pracht/adapter-node`)**
|
|
56
|
+
|
|
57
|
+
- **Symlinks are no longer followed by the static server.** `resolveStaticFile`
|
|
58
|
+
now uses `lstat` and rejects files whose inode is a symlink, preventing
|
|
59
|
+
a malicious build artifact from exposing files outside `dist/client/`.
|
|
60
|
+
- **ISG cache is path-contained.** The on-disk write path is now
|
|
61
|
+
`resolve()`-checked against the static root, rejecting any URL path
|
|
62
|
+
that would escape via `..`, encoded separators, or NUL bytes.
|
|
63
|
+
- **ISG skips the on-disk cache when the response is user-specific.**
|
|
64
|
+
Responses that set `Cache-Control: no-store`/`private`, `Set-Cookie`,
|
|
65
|
+
or a `Vary` covering `cookie`/`authorization`/`*` are served through
|
|
66
|
+
but not written to disk, closing a per-user cache-poisoning window.
|
|
67
|
+
|
|
68
|
+
**Packaging**
|
|
69
|
+
|
|
70
|
+
- `@pracht/cli` now has an explicit `files` allowlist so future
|
|
71
|
+
workdir additions can't accidentally ship in the npm tarball.
|
|
72
|
+
- `create-pracht`'s bin entry is now executable in the repository.
|
|
73
|
+
|
|
74
|
+
- Updated dependencies [[`caae3cb`](https://github.com/JoviDeCroock/pracht/commit/caae3cb53e0b6136ef78c3ac189a0d0ab82e4df7), [`8f662c0`](https://github.com/JoviDeCroock/pracht/commit/8f662c0b78b1911a7534ffd7aa4e919cf22a3a42), [`901ef5b`](https://github.com/JoviDeCroock/pracht/commit/901ef5b7958e4066d5382f836d098bded8bfe320), [`015e987`](https://github.com/JoviDeCroock/pracht/commit/015e987a2de471980fab557e3dbf3d52937ad0ac)]:
|
|
75
|
+
- @pracht/core@0.3.0
|
|
76
|
+
|
|
77
|
+
## 1.2.1
|
|
78
|
+
|
|
79
|
+
### Patch Changes
|
|
80
|
+
|
|
81
|
+
- [#116](https://github.com/JoviDeCroock/pracht/pull/116) [`411da18`](https://github.com/JoviDeCroock/pracht/commit/411da18d0fa8bbc20270729584c6677376be7f24) Thanks [@kinngh](https://github.com/kinngh)! - Strip server-only route and shell exports from client module imports so inline loaders can statically import server-only dependencies without evaluating them in browser bundles.
|
|
82
|
+
|
|
83
|
+
- [#117](https://github.com/JoviDeCroock/pracht/pull/117) [`39a226d`](https://github.com/JoviDeCroock/pracht/commit/39a226d1023317c357df8b72e020034a2c68d896) Thanks [@JoviDeCroock](https://github.com/JoviDeCroock)! - Copy public/ folder contents to dist/client/ during build so that static assets like favicons and robots.txt are available for deployment platforms
|
|
84
|
+
|
|
85
|
+
- Updated dependencies [[`f0ed41e`](https://github.com/JoviDeCroock/pracht/commit/f0ed41e4b886e751fbdfd29ae10f880c3aa364d4), [`49732fc`](https://github.com/JoviDeCroock/pracht/commit/49732fc78a776cbaabe9579e5a7f2fb154497479), [`d88c9e4`](https://github.com/JoviDeCroock/pracht/commit/d88c9e4b8347c4d3ecacdbc5f7674ee38af0092e), [`7ee2a93`](https://github.com/JoviDeCroock/pracht/commit/7ee2a936357a0f0b4ff7f5a7f6f3206b070f3890), [`00c4014`](https://github.com/JoviDeCroock/pracht/commit/00c401410b13c2d904c0beafc4da62dfb8f0f91e), [`f0ed41e`](https://github.com/JoviDeCroock/pracht/commit/f0ed41e4b886e751fbdfd29ae10f880c3aa364d4)]:
|
|
86
|
+
- @pracht/core@0.2.7
|
|
87
|
+
|
|
3
88
|
## 1.2.0
|
|
4
89
|
|
|
5
90
|
### Minor Changes
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { r as VERSION } from "./index.mjs";
|
|
2
|
-
import { t as readClientBuildAssets } from "./build-metadata-
|
|
2
|
+
import { t as readClientBuildAssets } from "./build-metadata-BOChHO8g.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
@@ -36,6 +36,14 @@ function createVercelOutputConfig({ functionName, headersManifest, staticRoutes,
|
|
|
36
36
|
value: "1"
|
|
37
37
|
}],
|
|
38
38
|
src: "/(.*)"
|
|
39
|
+
}, {
|
|
40
|
+
dest: target,
|
|
41
|
+
has: [{
|
|
42
|
+
type: "query",
|
|
43
|
+
key: "_data",
|
|
44
|
+
value: "1"
|
|
45
|
+
}],
|
|
46
|
+
src: "/(.*)"
|
|
39
47
|
}];
|
|
40
48
|
for (const route of sortStaticRoutes(staticRoutes)) routes.push({
|
|
41
49
|
dest: routeToStaticHtmlPath(route),
|
|
@@ -157,6 +165,8 @@ var build_default = defineCommand({
|
|
|
157
165
|
});
|
|
158
166
|
}
|
|
159
167
|
}
|
|
168
|
+
const publicDir = resolve(root, "public");
|
|
169
|
+
if (existsSync(publicDir)) cpSync(publicDir, clientDir, { recursive: true });
|
|
160
170
|
if (existsSync(serverEntry)) {
|
|
161
171
|
const serverMod = await import(serverEntry);
|
|
162
172
|
const { prerenderApp } = serverMod;
|
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from "node:fs";
|
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
//#region src/build-metadata.ts
|
|
4
4
|
const MANIFEST_PATHS = ["dist/client/.vite/manifest.json", "dist/.vite/manifest.json"];
|
|
5
|
+
const PRACHT_CLIENT_MODULE_QUERY = "pracht-client";
|
|
5
6
|
function readClientBuildAssets(root = process.cwd()) {
|
|
6
7
|
const manifestPath = MANIFEST_PATHS.map((candidate) => resolve(root, candidate)).find((path) => existsSync(path));
|
|
7
8
|
if (!manifestPath) return {
|
|
@@ -36,8 +37,9 @@ function readClientBuildAssets(root = process.cwd()) {
|
|
|
36
37
|
for (const [key, entry] of Object.entries(manifest)) {
|
|
37
38
|
if (!entry.src) continue;
|
|
38
39
|
const deps = collectTransitiveDeps(key);
|
|
39
|
-
|
|
40
|
-
if (deps.
|
|
40
|
+
const manifestKey = stripPrachtClientModuleQuery(entry.src);
|
|
41
|
+
if (deps.css.length > 0) cssManifest[manifestKey] = deps.css.map((file) => `/${file}`);
|
|
42
|
+
if (deps.js.length > 0) jsManifest[manifestKey] = deps.js.map((file) => `/${file}`);
|
|
41
43
|
}
|
|
42
44
|
return {
|
|
43
45
|
clientEntryUrl: clientEntry ? `/${clientEntry.file}` : null,
|
|
@@ -45,5 +47,12 @@ function readClientBuildAssets(root = process.cwd()) {
|
|
|
45
47
|
jsManifest
|
|
46
48
|
};
|
|
47
49
|
}
|
|
50
|
+
function stripPrachtClientModuleQuery(id) {
|
|
51
|
+
const queryStart = id.indexOf("?");
|
|
52
|
+
if (queryStart === -1) return id;
|
|
53
|
+
const path = id.slice(0, queryStart);
|
|
54
|
+
const query = id.slice(queryStart + 1).split("&").filter((part) => part !== PRACHT_CLIENT_MODULE_QUERY);
|
|
55
|
+
return query.length > 0 ? `${path}?${query.join("&")}` : path;
|
|
56
|
+
}
|
|
48
57
|
//#endregion
|
|
49
58
|
export { readClientBuildAssets as t };
|
|
@@ -1,7 +1,146 @@
|
|
|
1
|
-
import { a as
|
|
2
|
-
import {
|
|
1
|
+
import { _ as requireEnum, a as readProjectConfig, c as resolveProjectPath, d as writeGeneratedFile, f as ensureTrailingNewline, g as quote, h as parseCommaList, l as resolveRouteModulePath, m as parseApiMethods, n as displayPath, o as resolveApiModulePath, s as resolvePagesRouteModulePath, t as assertFileExists, u as resolveScopedFile, v as requirePositiveInteger } from "./project-BdMiN3s7.mjs";
|
|
2
|
+
import { a as toManifestModulePath, i as insertArrayItem, n as extractRegistryEntries, o as upsertObjectEntry, t as ensureCoreNamedImport } from "./manifest-Bs5hp3gA.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
//#region src/commands/generate-paths.ts
|
|
6
|
+
function normalizeRoutePathString(value) {
|
|
7
|
+
if (!value || value === "/") return "/";
|
|
8
|
+
const normalized = `/${value}`.replace(/\/+/g, "/");
|
|
9
|
+
return normalized !== "/" && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
10
|
+
}
|
|
11
|
+
function normalizeApiPath(value) {
|
|
12
|
+
return normalizeRoutePathString(value).replace(/^\/api(?=\/|$)/, "") || "/";
|
|
13
|
+
}
|
|
14
|
+
function hasDynamicSegments(routePath) {
|
|
15
|
+
return routePath.split("/").some((segment) => segment.startsWith(":") || segment === "*");
|
|
16
|
+
}
|
|
17
|
+
function dynamicParamNames(routePath) {
|
|
18
|
+
return routePath.split("/").filter(Boolean).map((segment) => {
|
|
19
|
+
if (segment.startsWith(":")) return segment.slice(1);
|
|
20
|
+
if (segment === "*") return "slug";
|
|
21
|
+
return null;
|
|
22
|
+
}).filter((s) => s !== null);
|
|
23
|
+
}
|
|
24
|
+
function routeIdFromPath(routePath) {
|
|
25
|
+
if (routePath === "/") return "index";
|
|
26
|
+
return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\*/g, "splat")).join("-");
|
|
27
|
+
}
|
|
28
|
+
function titleFromPath(routePath) {
|
|
29
|
+
if (routePath === "/") return "Home";
|
|
30
|
+
return titleCase((routePath.split("/").filter(Boolean).at(-1) ?? "Page").replace(/^:/, "").replace(/\*/g, "slug"));
|
|
31
|
+
}
|
|
32
|
+
function titleCase(value) {
|
|
33
|
+
return value.split(/[-_/]/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region src/commands/generate-source.ts
|
|
37
|
+
function buildManifestRouteModuleSource(opts) {
|
|
38
|
+
const sections = buildRouteModuleSections(opts);
|
|
39
|
+
const componentIdx = sections.findIndex((s) => s.startsWith("export function Component"));
|
|
40
|
+
const insertAt = componentIdx === -1 ? sections.length : componentIdx;
|
|
41
|
+
sections.splice(insertAt, 0, "export function head() {", ` return { title: ${quote(opts.title)} };`, "}", "");
|
|
42
|
+
return `${sections.join("\n")}\n`;
|
|
43
|
+
}
|
|
44
|
+
function buildPagesRouteModuleSource(opts) {
|
|
45
|
+
const sections = buildRouteModuleSections(opts);
|
|
46
|
+
const firstExportIdx = sections.findIndex((s) => s.startsWith("export"));
|
|
47
|
+
const insertAt = firstExportIdx === -1 ? sections.length : firstExportIdx;
|
|
48
|
+
sections.splice(insertAt, 0, `export const RENDER_MODE = ${quote(opts.render)};`, "");
|
|
49
|
+
return `${sections.join("\n")}\n`;
|
|
50
|
+
}
|
|
51
|
+
function buildShellModuleSource(name) {
|
|
52
|
+
const title = titleCase(name);
|
|
53
|
+
return [
|
|
54
|
+
"import type { ShellProps } from \"@pracht/core\";",
|
|
55
|
+
"",
|
|
56
|
+
"export function Shell({ children }: ShellProps) {",
|
|
57
|
+
" return (",
|
|
58
|
+
` <div class=${quote(`${name}-shell`)}>`,
|
|
59
|
+
" <main>{children}</main>",
|
|
60
|
+
" </div>",
|
|
61
|
+
" );",
|
|
62
|
+
"}",
|
|
63
|
+
"",
|
|
64
|
+
"export function head() {",
|
|
65
|
+
` return { title: ${quote(title)} };`,
|
|
66
|
+
"}",
|
|
67
|
+
""
|
|
68
|
+
].join("\n");
|
|
69
|
+
}
|
|
70
|
+
function buildMiddlewareModuleSource() {
|
|
71
|
+
return [
|
|
72
|
+
"import type { MiddlewareFn } from \"@pracht/core\";",
|
|
73
|
+
"",
|
|
74
|
+
"export const middleware: MiddlewareFn = async (_args) => {",
|
|
75
|
+
" return;",
|
|
76
|
+
"};",
|
|
77
|
+
""
|
|
78
|
+
].join("\n");
|
|
79
|
+
}
|
|
80
|
+
function buildApiRouteSource({ endpointPath, methods }) {
|
|
81
|
+
return [
|
|
82
|
+
"import type { BaseRouteArgs } from \"@pracht/core\";",
|
|
83
|
+
"",
|
|
84
|
+
...methods.flatMap((method, index) => {
|
|
85
|
+
const lines = buildApiMethodSource(method, methods, endpointPath);
|
|
86
|
+
if (index === methods.length - 1) return lines;
|
|
87
|
+
return [...lines, ""];
|
|
88
|
+
}),
|
|
89
|
+
""
|
|
90
|
+
].join("\n");
|
|
91
|
+
}
|
|
92
|
+
function buildRouteModuleSections(opts) {
|
|
93
|
+
const { includeErrorBoundary, includeLoader, includeStaticPaths, routePath, title } = opts;
|
|
94
|
+
const params = dynamicParamNames(routePath);
|
|
95
|
+
const imports = [];
|
|
96
|
+
const sections = [];
|
|
97
|
+
if (includeLoader) imports.push("LoaderArgs", "RouteComponentProps");
|
|
98
|
+
if (includeErrorBoundary) imports.push("ErrorBoundaryProps");
|
|
99
|
+
if (imports.length > 0) {
|
|
100
|
+
sections.push(`import type { ${imports.join(", ")} } from "@pracht/core";`);
|
|
101
|
+
sections.push("");
|
|
102
|
+
}
|
|
103
|
+
if (includeLoader) sections.push("export async function loader(_args: LoaderArgs) {", ` return { message: ${quote(`Welcome to ${title}.`)} };`, "}", "");
|
|
104
|
+
if (includeStaticPaths) sections.push("export function getStaticPaths() {", ` return [${buildStaticPathsStub(params)}];`, "}", "");
|
|
105
|
+
if (includeLoader) sections.push("export function Component({ data }: RouteComponentProps<typeof loader>) {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " <p>{data.message}</p>", " </section>", " );", "}");
|
|
106
|
+
else sections.push("export function Component() {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " </section>", " );", "}");
|
|
107
|
+
if (includeErrorBoundary) sections.push("", "export function ErrorBoundary({ error }: ErrorBoundaryProps) {", " return <p>{error.message}</p>;", "}");
|
|
108
|
+
return sections;
|
|
109
|
+
}
|
|
110
|
+
function buildApiMethodSource(method, methods, endpointPath) {
|
|
111
|
+
if (method === "DELETE" || method === "HEAD") return [
|
|
112
|
+
`export function ${method}(_args: BaseRouteArgs) {`,
|
|
113
|
+
" return new Response(null, { status: 204 });",
|
|
114
|
+
"}"
|
|
115
|
+
];
|
|
116
|
+
if (method === "OPTIONS") return [
|
|
117
|
+
`export function ${method}(_args: BaseRouteArgs) {`,
|
|
118
|
+
" return new Response(null, {",
|
|
119
|
+
` headers: { allow: ${quote(methods.join(", "))} },`,
|
|
120
|
+
" status: 204,",
|
|
121
|
+
" });",
|
|
122
|
+
"}"
|
|
123
|
+
];
|
|
124
|
+
if (method === "GET") return [
|
|
125
|
+
`export function ${method}(_args: BaseRouteArgs) {`,
|
|
126
|
+
` return Response.json({ endpoint: ${quote(`/api${endpointPath}`)}, ok: true });`,
|
|
127
|
+
"}"
|
|
128
|
+
];
|
|
129
|
+
const status = method === "POST" ? 201 : 200;
|
|
130
|
+
return [
|
|
131
|
+
`export async function ${method}({ request }: BaseRouteArgs) {`,
|
|
132
|
+
" const body = await request.json();",
|
|
133
|
+
` return Response.json({ body, ok: true }, { status: ${status} });`,
|
|
134
|
+
"}"
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
function buildStaticPathsStub(params) {
|
|
138
|
+
if (params.length === 0) return "{}";
|
|
139
|
+
return `{ ${params.map((name) => `${name}: ${quote(`example-${name}`)}`).join(", ")} }`;
|
|
140
|
+
}
|
|
141
|
+
function escapeJsxText(value) {
|
|
142
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
143
|
+
}
|
|
5
144
|
var generate_default = defineCommand({
|
|
6
145
|
meta: {
|
|
7
146
|
name: "generate",
|
|
@@ -260,145 +399,5 @@ function generateApi(args, project) {
|
|
|
260
399
|
updated: []
|
|
261
400
|
};
|
|
262
401
|
}
|
|
263
|
-
function buildManifestRouteModuleSource({ includeErrorBoundary, includeLoader, includeStaticPaths, routePath, title }) {
|
|
264
|
-
const params = dynamicParamNames(routePath);
|
|
265
|
-
const imports = [];
|
|
266
|
-
const sections = [];
|
|
267
|
-
if (includeLoader) imports.push("LoaderArgs", "RouteComponentProps");
|
|
268
|
-
if (includeErrorBoundary) imports.push("ErrorBoundaryProps");
|
|
269
|
-
if (imports.length > 0) {
|
|
270
|
-
sections.push(`import type { ${imports.join(", ")} } from "@pracht/core";`);
|
|
271
|
-
sections.push("");
|
|
272
|
-
}
|
|
273
|
-
if (includeLoader) sections.push("export async function loader(_args: LoaderArgs) {", ` return { message: ${quote(`Welcome to ${title}.`)} };`, "}", "");
|
|
274
|
-
if (includeStaticPaths) sections.push("export function getStaticPaths() {", ` return [${buildStaticPathsStub(params)}];`, "}", "");
|
|
275
|
-
sections.push("export function head() {", ` return { title: ${quote(title)} };`, "}", "");
|
|
276
|
-
if (includeLoader) sections.push("export function Component({ data }: RouteComponentProps<typeof loader>) {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " <p>{data.message}</p>", " </section>", " );", "}");
|
|
277
|
-
else sections.push("export function Component() {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " </section>", " );", "}");
|
|
278
|
-
if (includeErrorBoundary) sections.push("", "export function ErrorBoundary({ error }: ErrorBoundaryProps) {", " return <p>{error.message}</p>;", "}");
|
|
279
|
-
return `${sections.join("\n")}\n`;
|
|
280
|
-
}
|
|
281
|
-
function buildPagesRouteModuleSource({ includeErrorBoundary, includeLoader, includeStaticPaths, render, routePath, title }) {
|
|
282
|
-
const params = dynamicParamNames(routePath);
|
|
283
|
-
const imports = [];
|
|
284
|
-
const sections = [];
|
|
285
|
-
if (includeLoader) imports.push("LoaderArgs", "RouteComponentProps");
|
|
286
|
-
if (includeErrorBoundary) imports.push("ErrorBoundaryProps");
|
|
287
|
-
if (imports.length > 0) {
|
|
288
|
-
sections.push(`import type { ${imports.join(", ")} } from "@pracht/core";`);
|
|
289
|
-
sections.push("");
|
|
290
|
-
}
|
|
291
|
-
sections.push(`export const RENDER_MODE = ${quote(render)};`, "");
|
|
292
|
-
if (includeLoader) sections.push("export async function loader(_args: LoaderArgs) {", ` return { message: ${quote(`Welcome to ${title}.`)} };`, "}", "");
|
|
293
|
-
if (includeStaticPaths) sections.push("export function getStaticPaths() {", ` return [${buildStaticPathsStub(params)}];`, "}", "");
|
|
294
|
-
if (includeLoader) sections.push("export function Component({ data }: RouteComponentProps<typeof loader>) {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " <p>{data.message}</p>", " </section>", " );", "}");
|
|
295
|
-
else sections.push("export function Component() {", " return (", " <section>", ` <h1>${escapeJsxText(title)}</h1>`, " </section>", " );", "}");
|
|
296
|
-
if (includeErrorBoundary) sections.push("", "export function ErrorBoundary({ error }: ErrorBoundaryProps) {", " return <p>{error.message}</p>;", "}");
|
|
297
|
-
return `${sections.join("\n")}\n`;
|
|
298
|
-
}
|
|
299
|
-
function buildShellModuleSource(name) {
|
|
300
|
-
const title = titleCase(name);
|
|
301
|
-
return [
|
|
302
|
-
"import type { ShellProps } from \"@pracht/core\";",
|
|
303
|
-
"",
|
|
304
|
-
"export function Shell({ children }: ShellProps) {",
|
|
305
|
-
" return (",
|
|
306
|
-
` <div class=${quote(`${name}-shell`)}>`,
|
|
307
|
-
" <main>{children}</main>",
|
|
308
|
-
" </div>",
|
|
309
|
-
" );",
|
|
310
|
-
"}",
|
|
311
|
-
"",
|
|
312
|
-
"export function head() {",
|
|
313
|
-
` return { title: ${quote(title)} };`,
|
|
314
|
-
"}",
|
|
315
|
-
""
|
|
316
|
-
].join("\n");
|
|
317
|
-
}
|
|
318
|
-
function buildMiddlewareModuleSource() {
|
|
319
|
-
return [
|
|
320
|
-
"import type { MiddlewareFn } from \"@pracht/core\";",
|
|
321
|
-
"",
|
|
322
|
-
"export const middleware: MiddlewareFn = async (_args) => {",
|
|
323
|
-
" return;",
|
|
324
|
-
"};",
|
|
325
|
-
""
|
|
326
|
-
].join("\n");
|
|
327
|
-
}
|
|
328
|
-
function buildApiRouteSource({ endpointPath, methods }) {
|
|
329
|
-
return [
|
|
330
|
-
"import type { BaseRouteArgs } from \"@pracht/core\";",
|
|
331
|
-
"",
|
|
332
|
-
...methods.flatMap((method, index) => {
|
|
333
|
-
const lines = buildApiMethodSource(method, methods, endpointPath);
|
|
334
|
-
if (index === methods.length - 1) return lines;
|
|
335
|
-
return [...lines, ""];
|
|
336
|
-
}),
|
|
337
|
-
""
|
|
338
|
-
].join("\n");
|
|
339
|
-
}
|
|
340
|
-
function buildApiMethodSource(method, methods, endpointPath) {
|
|
341
|
-
if (method === "DELETE" || method === "HEAD") return [
|
|
342
|
-
`export function ${method}(_args: BaseRouteArgs) {`,
|
|
343
|
-
" return new Response(null, { status: 204 });",
|
|
344
|
-
"}"
|
|
345
|
-
];
|
|
346
|
-
if (method === "OPTIONS") return [
|
|
347
|
-
`export function ${method}(_args: BaseRouteArgs) {`,
|
|
348
|
-
" return new Response(null, {",
|
|
349
|
-
` headers: { allow: ${quote(methods.join(", "))} },`,
|
|
350
|
-
" status: 204,",
|
|
351
|
-
" });",
|
|
352
|
-
"}"
|
|
353
|
-
];
|
|
354
|
-
if (method === "GET") return [
|
|
355
|
-
`export function ${method}(_args: BaseRouteArgs) {`,
|
|
356
|
-
` return Response.json({ endpoint: ${quote(`/api${endpointPath}`)}, ok: true });`,
|
|
357
|
-
"}"
|
|
358
|
-
];
|
|
359
|
-
const status = method === "POST" ? 201 : 200;
|
|
360
|
-
return [
|
|
361
|
-
`export async function ${method}({ request }: BaseRouteArgs) {`,
|
|
362
|
-
" const body = await request.json();",
|
|
363
|
-
` return Response.json({ body, ok: true }, { status: ${status} });`,
|
|
364
|
-
"}"
|
|
365
|
-
];
|
|
366
|
-
}
|
|
367
|
-
function normalizeRoutePathString(value) {
|
|
368
|
-
if (!value || value === "/") return "/";
|
|
369
|
-
const normalized = `/${value}`.replace(/\/+/g, "/");
|
|
370
|
-
return normalized !== "/" && normalized.endsWith("/") ? normalized.slice(0, -1) : normalized;
|
|
371
|
-
}
|
|
372
|
-
function normalizeApiPath(value) {
|
|
373
|
-
return normalizeRoutePathString(value).replace(/^\/api(?=\/|$)/, "") || "/";
|
|
374
|
-
}
|
|
375
|
-
function hasDynamicSegments(routePath) {
|
|
376
|
-
return routePath.split("/").some((segment) => segment.startsWith(":") || segment === "*");
|
|
377
|
-
}
|
|
378
|
-
function dynamicParamNames(routePath) {
|
|
379
|
-
return routePath.split("/").filter(Boolean).map((segment) => {
|
|
380
|
-
if (segment.startsWith(":")) return segment.slice(1);
|
|
381
|
-
if (segment === "*") return "slug";
|
|
382
|
-
return null;
|
|
383
|
-
}).filter((s) => s !== null);
|
|
384
|
-
}
|
|
385
|
-
function routeIdFromPath(routePath) {
|
|
386
|
-
if (routePath === "/") return "index";
|
|
387
|
-
return routePath.split("/").filter(Boolean).map((segment) => segment.replace(/^:/, "").replace(/\*/g, "splat")).join("-");
|
|
388
|
-
}
|
|
389
|
-
function titleFromPath(routePath) {
|
|
390
|
-
if (routePath === "/") return "Home";
|
|
391
|
-
return titleCase((routePath.split("/").filter(Boolean).at(-1) ?? "Page").replace(/^:/, "").replace(/\*/g, "slug"));
|
|
392
|
-
}
|
|
393
|
-
function titleCase(value) {
|
|
394
|
-
return value.split(/[-_/]/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join(" ");
|
|
395
|
-
}
|
|
396
|
-
function buildStaticPathsStub(params) {
|
|
397
|
-
if (params.length === 0) return "{}";
|
|
398
|
-
return `{ ${params.map((name) => `${name}: ${quote(`example-${name}`)}`).join(", ")} }`;
|
|
399
|
-
}
|
|
400
|
-
function escapeJsxText(value) {
|
|
401
|
-
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
402
|
-
}
|
|
403
402
|
//#endregion
|
|
404
403
|
export { generate_default as default };
|
package/dist/index.mjs
CHANGED
|
@@ -29,12 +29,12 @@ runMain(defineCommand({
|
|
|
29
29
|
description: "The pracht CLI"
|
|
30
30
|
},
|
|
31
31
|
subCommands: {
|
|
32
|
-
build: () => import("./build-
|
|
32
|
+
build: () => import("./build-DkP2ffu9.mjs").then((m) => m.default),
|
|
33
33
|
dev: () => import("./dev-BCFe3g38.mjs").then((m) => m.default),
|
|
34
|
-
doctor: () => import("./doctor-
|
|
35
|
-
generate: () => import("./generate-
|
|
36
|
-
inspect: () => import("./inspect-
|
|
37
|
-
verify: () => import("./verify-
|
|
34
|
+
doctor: () => import("./doctor-CwQJjJ83.mjs").then((m) => m.default),
|
|
35
|
+
generate: () => import("./generate-C9AMSGsv.mjs").then((m) => m.default),
|
|
36
|
+
inspect: () => import("./inspect-MseHPU9s.mjs").then((m) => m.default),
|
|
37
|
+
verify: () => import("./verify-o0G5Xuff.mjs").then((m) => m.default)
|
|
38
38
|
}
|
|
39
39
|
}));
|
|
40
40
|
//#endregion
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { t as HTTP_METHODS } from "./index.mjs";
|
|
2
|
-
import { t as readClientBuildAssets } from "./build-metadata-
|
|
3
|
-
import { a as readProjectConfig, c as resolveProjectPath, p as handleCliError } from "./project-
|
|
2
|
+
import { t as readClientBuildAssets } from "./build-metadata-BOChHO8g.mjs";
|
|
3
|
+
import { a as readProjectConfig, c as resolveProjectPath, p as handleCliError } from "./project-BdMiN3s7.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import { resolve } from "node:path";
|
|
7
7
|
import { createServer } from "vite";
|
|
8
8
|
//#region src/commands/inspect.ts
|
|
@@ -45,12 +45,7 @@ async function runInspect(root, { target = "all" } = {}) {
|
|
|
45
45
|
if (!project.configFile) throw new Error("Missing vite config. `pracht inspect` requires a project with pracht configured.");
|
|
46
46
|
if (!project.hasPrachtPlugin) throw new Error("vite.config does not appear to register the pracht plugin.");
|
|
47
47
|
if (project.mode === "manifest") {
|
|
48
|
-
|
|
49
|
-
try {
|
|
50
|
-
readFileSync(manifestPath, "utf-8");
|
|
51
|
-
} catch {
|
|
52
|
-
throw new Error(`App manifest is missing at ${project.appFile}.`);
|
|
53
|
-
}
|
|
48
|
+
if (!existsSync(resolveProjectPath(project.root, project.appFile))) throw new Error(`App manifest is missing at ${project.appFile}.`);
|
|
54
49
|
}
|
|
55
50
|
const server = await createServer({
|
|
56
51
|
root,
|
|
@@ -67,7 +67,7 @@ function resolveScopedFile(root, configDir, fileName) {
|
|
|
67
67
|
return resolve(resolveProjectPath(root, configDir), fileName);
|
|
68
68
|
}
|
|
69
69
|
function resolveRouteModulePath(project, routePath, extension) {
|
|
70
|
-
const segments =
|
|
70
|
+
const segments = segmentsFromPath(routePath);
|
|
71
71
|
const relativePath = segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
|
|
72
72
|
return {
|
|
73
73
|
absolutePath: resolve(resolveProjectPath(project.root, project.routesDir), relativePath),
|
|
@@ -75,7 +75,7 @@ function resolveRouteModulePath(project, routePath, extension) {
|
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
77
|
function resolvePagesRouteModulePath(project, routePath, extension) {
|
|
78
|
-
const segments =
|
|
78
|
+
const segments = segmentsFromPath(routePath);
|
|
79
79
|
const relativePath = segments.length === 0 ? `index${extension}` : `${segments.join("/")}${extension}`;
|
|
80
80
|
return {
|
|
81
81
|
absolutePath: resolve(resolveProjectPath(project.root, project.pagesDir), relativePath),
|
|
@@ -83,7 +83,7 @@ function resolvePagesRouteModulePath(project, routePath, extension) {
|
|
|
83
83
|
};
|
|
84
84
|
}
|
|
85
85
|
function resolveApiModulePath(project, endpointPath) {
|
|
86
|
-
const segments =
|
|
86
|
+
const segments = segmentsFromPath(endpointPath);
|
|
87
87
|
const relativePath = segments.length === 0 ? "index.ts" : `${segments.join("/")}.ts`;
|
|
88
88
|
return {
|
|
89
89
|
absolutePath: resolve(resolveProjectPath(project.root, project.apiDir), relativePath),
|
|
@@ -137,15 +137,8 @@ function normalizeConfigPath(value) {
|
|
|
137
137
|
if (!value) return value;
|
|
138
138
|
return value.startsWith("/") ? value : `/${value}`;
|
|
139
139
|
}
|
|
140
|
-
function
|
|
141
|
-
return
|
|
142
|
-
if (segment.startsWith(":")) return `[${segment.slice(1)}]`;
|
|
143
|
-
if (segment === "*") return "[...slug]";
|
|
144
|
-
return segment;
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
function segmentsFromApiPath(endpointPath) {
|
|
148
|
-
return endpointPath.split("/").filter(Boolean).map((segment) => {
|
|
140
|
+
function segmentsFromPath(path) {
|
|
141
|
+
return path.split("/").filter(Boolean).map((segment) => {
|
|
149
142
|
if (segment.startsWith(":")) return `[${segment.slice(1)}]`;
|
|
150
143
|
if (segment === "*") return "[...slug]";
|
|
151
144
|
return segment;
|