@nifrajs/cli 2.1.0 → 2.3.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/README.md +25 -25
- package/dist/assure.d.ts +2 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +17 -10
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts +7 -0
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +68 -16
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check.d.ts +64 -22
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +400 -83
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +37 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +291 -53
- package/dist/cli.js.map +1 -1
- package/dist/diff-tool.d.ts +3 -3
- package/dist/diff-tool.js +7 -7
- package/dist/docs-search.d.ts +3 -3
- package/dist/docs-search.js +6 -6
- package/dist/doctor.d.ts +8 -4
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +74 -14
- package/dist/doctor.js.map +1 -1
- package/dist/examples.d.ts +5 -5
- package/dist/examples.js +5 -5
- package/dist/init-agents.d.ts +14 -14
- package/dist/init-agents.js +14 -14
- package/dist/internal/typescript-import.d.ts +24 -0
- package/dist/internal/typescript-import.d.ts.map +1 -0
- package/dist/internal/typescript-import.js +32 -0
- package/dist/internal/typescript-import.js.map +1 -0
- package/dist/introspect.d.ts +12 -12
- package/dist/introspect.js +24 -24
- package/dist/levels-tool.d.ts +8 -8
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +33 -30
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +17 -6
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +45 -7
- package/dist/load.js.map +1 -1
- package/dist/manifest-tool.d.ts +1 -1
- package/dist/manifest-tool.d.ts.map +1 -1
- package/dist/manifest-tool.js +4 -2
- package/dist/manifest-tool.js.map +1 -1
- package/dist/mcp-docs-tools.d.ts +1 -1
- package/dist/mcp-docs-tools.d.ts.map +1 -1
- package/dist/mcp-docs-tools.js +14 -10
- package/dist/mcp-docs-tools.js.map +1 -1
- package/dist/mcp-http.d.ts +5 -5
- package/dist/mcp-http.js +7 -7
- package/dist/mcp-protocol.d.ts +2 -2
- package/dist/mcp-protocol.js +2 -2
- package/dist/mcp-render.d.ts +3 -3
- package/dist/mcp-render.js +15 -7
- package/dist/mcp-render.js.map +1 -1
- package/dist/mcp-run.d.ts +2 -2
- package/dist/mcp-run.js +2 -2
- package/dist/mcp.d.ts +14 -14
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +119 -47
- package/dist/mcp.js.map +1 -1
- package/dist/openapi-tool.d.ts +1 -1
- package/dist/openapi-tool.js +2 -2
- package/dist/pipeline-guard.d.ts +83 -0
- package/dist/pipeline-guard.d.ts.map +1 -0
- package/dist/pipeline-guard.js +136 -0
- package/dist/pipeline-guard.js.map +1 -0
- package/dist/port.d.ts +9 -9
- package/dist/port.js +27 -27
- package/dist/recipes/1.8.0.d.ts +2 -2
- package/dist/recipes/1.8.0.js +5 -5
- package/dist/recipes/index.d.ts +2 -2
- package/dist/recipes/index.js +1 -1
- package/dist/scaffold.d.ts +3 -3
- package/dist/scaffold.js +8 -8
- package/dist/sync-routes.d.ts +28 -0
- package/dist/sync-routes.d.ts.map +1 -0
- package/dist/sync-routes.js +55 -0
- package/dist/sync-routes.js.map +1 -0
- package/dist/types-search.d.ts +6 -6
- package/dist/types-search.d.ts.map +1 -1
- package/dist/types-search.js +53 -8
- package/dist/types-search.js.map +1 -1
- package/dist/upgrade.d.ts +7 -7
- package/dist/upgrade.js +12 -12
- package/dist/verification.d.ts +51 -0
- package/dist/verification.d.ts.map +1 -0
- package/dist/verification.js +80 -0
- package/dist/verification.js.map +1 -0
- package/docs/examples.json +178 -88
- package/docs/llms-full.txt +1766 -766
- package/docs/types.json +2171 -2807
- package/package.json +22 -11
- package/src/assure.ts +17 -18
- package/src/capabilities-tool.ts +82 -16
- package/src/check.ts +493 -100
- package/src/cli.ts +306 -54
- package/src/diff-tool.ts +8 -8
- package/src/docs-search.ts +6 -6
- package/src/doctor.ts +81 -16
- package/src/examples.ts +7 -7
- package/src/init-agents.ts +17 -17
- package/src/internal/typescript-import.ts +33 -0
- package/src/introspect.ts +28 -28
- package/src/levels-tool.ts +37 -31
- package/src/load.ts +65 -12
- package/src/manifest-tool.ts +4 -2
- package/src/mcp-docs-tools.ts +20 -10
- package/src/mcp-http.ts +7 -7
- package/src/mcp-protocol.ts +2 -2
- package/src/mcp-render.ts +15 -7
- package/src/mcp-run.ts +2 -2
- package/src/mcp.ts +127 -47
- package/src/openapi-tool.ts +2 -2
- package/src/pipeline-guard.ts +186 -0
- package/src/port.ts +31 -31
- package/src/recipes/1.8.0.ts +5 -5
- package/src/recipes/index.ts +2 -2
- package/src/scaffold.ts +8 -8
- package/src/sync-routes.ts +69 -0
- package/src/types-search.ts +57 -8
- package/src/upgrade.ts +12 -12
- package/src/verification.ts +123 -0
package/dist/cli.js
CHANGED
|
@@ -8,27 +8,32 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
|
|
|
8
8
|
return path;
|
|
9
9
|
};
|
|
10
10
|
/**
|
|
11
|
-
* `nifra`
|
|
11
|
+
* `nifra` - the zero-config CLI for a nifra app. Reads `framework.ts` + `backend.ts` + `routes/` from
|
|
12
12
|
* the project root (see {@link loadApp}) and wires the right `@nifrajs/web` entrypoint:
|
|
13
13
|
*
|
|
14
|
-
* nifra dev true-HMR dev server (Vite middleware + nifra SSR)
|
|
15
|
-
* nifra build emit a complete target-specific deploy directory
|
|
16
|
-
* nifra start run the default Bun build
|
|
14
|
+
* nifra dev true-HMR dev server (Vite middleware + nifra SSR) - @nifrajs/web/vite
|
|
15
|
+
* nifra build emit a complete target-specific deploy directory - @nifrajs/web/build
|
|
16
|
+
* nifra start run the default Bun build - dist/server.js
|
|
17
17
|
*
|
|
18
18
|
* Bun-only (it runs the framework's TS + Bun plugins directly). The *output* runs anywhere.
|
|
19
19
|
*/
|
|
20
20
|
import { existsSync } from "node:fs";
|
|
21
21
|
import { resolve } from "node:path";
|
|
22
22
|
import { inProcessClient } from "@nifrajs/client";
|
|
23
|
-
import { createWebApp, DEFAULT_DEV_PORT } from "@nifrajs/web";
|
|
23
|
+
import { createWebApp, DEFAULT_DEV_PORT, SERVER_FN_MODULE, SERVER_ONLY_MODULE, } from "@nifrajs/web";
|
|
24
24
|
import { discoverRoutes } from "@nifrajs/web/fs";
|
|
25
25
|
import { describeProject, describeRoutes } from "./introspect.js";
|
|
26
|
-
import { loadApp
|
|
27
|
-
|
|
26
|
+
import { loadApp } from "./load.js";
|
|
27
|
+
import { chooseBuildPipeline } from "./pipeline-guard.js";
|
|
28
|
+
const HELP = `nifra - zero-config dev/build/start for a nifra app
|
|
28
29
|
|
|
29
30
|
Usage:
|
|
30
31
|
nifra dev [--port <n>] [--poll] Start the true-HMR dev server (Vite). Default port ${DEFAULT_DEV_PORT}.
|
|
31
|
-
|
|
32
|
+
[--bun] Run the BUN pipeline instead: Bun.serve native HMR (React Fast
|
|
33
|
+
Refresh included, state preserved), no Vite in the process - one
|
|
34
|
+
bundler across dev and prod. Plain CSS/Tailwind work; refuses an
|
|
35
|
+
app using *.module.css (Bun's dev bundler can't transform it).
|
|
36
|
+
nifra build [--out <dir>] [--report] Emit a complete deploy directory.
|
|
32
37
|
[--target <t>] Target a FULL deploy dir for <t>:
|
|
33
38
|
bun | node | deno | cf-pages | vercel | static. Packages
|
|
34
39
|
buildClient + buildServer (+ prerender for static) so an app
|
|
@@ -36,6 +41,11 @@ Usage:
|
|
|
36
41
|
_routes.json. The server entry is generated from your
|
|
37
42
|
framework.ts (adapter) + backend.ts + routes/. --report prints
|
|
38
43
|
a per-chunk size + gzip table (biggest first).
|
|
44
|
+
[--vite | --bun] Force the bundler. Without a flag it follows your config: Bun
|
|
45
|
+
(faster, Bun-native) unless your ONLY transforms are
|
|
46
|
+
\`vitePlugins\`, which the Bun build cannot run - then Vite,
|
|
47
|
+
and it says so. --bun is refused in exactly that case rather
|
|
48
|
+
than dropping the transforms silently.
|
|
39
49
|
nifra start [--port <n>] [--out <dir>] Run the Bun build at <out>/server.js. Default port ${DEFAULT_DEV_PORT}.
|
|
40
50
|
nifra context Print this project's route INDEX (API + page routes) + conventions
|
|
41
51
|
for an AI agent's prompt. Per-route schemas: nifra mcp's
|
|
@@ -44,6 +54,11 @@ Usage:
|
|
|
44
54
|
routes (routes/) + the in-process backend's API routes,
|
|
45
55
|
marking which API routes are auto-mounted under apiPrefix.
|
|
46
56
|
--json for agents (see POST /api/x is/isn't served, not via 405).
|
|
57
|
+
nifra routes --modes [--target <t>] Show each page route's RENDER MODE (static | isr | ssr),
|
|
58
|
+
hydration, and cache policy in one table. With --target, gate
|
|
59
|
+
against it: a route the target can't honour (an ssr route on a
|
|
60
|
+
static build, ISR where there's no revalidation) exits nonzero
|
|
61
|
+
with the consequence - run in CI so it fails the build, not prod.
|
|
47
62
|
nifra init-agents [--force] [--json] Retrofit an EXISTING app with the agent-discovery files a new
|
|
48
63
|
app ships: .mcp.json + .cursor/mcp.json (register this project's
|
|
49
64
|
nifra MCP), CLAUDE.md (MCP-first preamble + @AGENTS.md import),
|
|
@@ -56,7 +71,7 @@ Usage:
|
|
|
56
71
|
nifra_example (verified snippets), nifra_scaffold (route→file),
|
|
57
72
|
nifra_check (drift gate + fixes), nifra_levels (verification
|
|
58
73
|
ladder), nifra_doctor (deps + duplicate installs).
|
|
59
|
-
nifra docs-mcp [--port <n>] Serve the PUBLIC docs MCP over HTTP (nifra_docs + nifra_example)
|
|
74
|
+
nifra docs-mcp [--port <n>] Serve the PUBLIC docs MCP over HTTP (nifra_docs + nifra_example) -
|
|
60
75
|
self-host on a VPS so any remote agent can learn nifra. Default :8787.
|
|
61
76
|
nifra check [--json] [--lints-only] Gate: typecheck + lints (hand-rolled fetch(), untyped client("…"),
|
|
62
77
|
server-only imports in routes/). Run as "done"; --json for agents;
|
|
@@ -64,13 +79,16 @@ Usage:
|
|
|
64
79
|
nifra sync-manifest Regenerate a committed web server-manifest.ts from routes/ WITHOUT
|
|
65
80
|
a full build - clears server-manifest drift after a route add/rename
|
|
66
81
|
(a new hydrating component still needs a full build).
|
|
82
|
+
nifra sync-routes Regenerate nifra-routes.d.ts from routes/ so navigate({ to, search })
|
|
83
|
+
is typed against each static route's searchSchema (a stale shape is a
|
|
84
|
+
tsc error). Include the file in your tsconfig. Pure file write.
|
|
67
85
|
nifra snapshot [--out <file>] Write the backend's API contract (routes + schemas) as plain
|
|
68
|
-
JSON
|
|
86
|
+
JSON - the baseline for \`nifra diff\`. Default api-snapshot.json.
|
|
69
87
|
nifra diff [<baseline>] [--json] Breaking-change gate: re-snapshot the contract and compare
|
|
70
88
|
against the committed baseline. Direction-aware (a new required
|
|
71
89
|
request field or a removed response field breaks; widening a
|
|
72
90
|
request enum or adding a response field doesn't) and fails
|
|
73
|
-
closed. Exits non-zero on any breaking change
|
|
91
|
+
closed. Exits non-zero on any breaking change - run it in CI.
|
|
74
92
|
nifra assure [--config <file>] [--json] Route-assurance gate: load nifra.assurance.ts, classify every
|
|
75
93
|
reflected backend route, and fail when required enforcement
|
|
76
94
|
evidence is missing/forbidden or a route is unclassified.
|
|
@@ -103,7 +121,7 @@ Usage:
|
|
|
103
121
|
[--ci] [--strict] matrix (in-memory stores, in-process cron/WebSocket, Bun/Deno
|
|
104
122
|
globals, node: builtins) with file:line evidence. --target auto-
|
|
105
123
|
detected from build/deploy scripts, wrangler.toml, or vercel config;
|
|
106
|
-
--ci (or any --target) exits
|
|
124
|
+
--ci (or any --target) exits nonzero when a used feature is
|
|
107
125
|
unsupported on the target; --strict also fails on caveats.
|
|
108
126
|
|
|
109
127
|
Reads nifra.config.ts (adapter + clientModule + plugins; or framework.ts), backend.ts (optional), and
|
|
@@ -112,12 +130,155 @@ routes/ from the current directory. Run from your project root.
|
|
|
112
130
|
Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with \`--port <n>\` (alias \`-p\`) or the
|
|
113
131
|
\`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`;
|
|
114
132
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
115
|
-
const CLI_VERSION = "2.
|
|
133
|
+
const CLI_VERSION = "2.3.0";
|
|
116
134
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
117
135
|
const asAdapter = (v) => v;
|
|
118
136
|
const asBunPlugins = (v) => v;
|
|
119
137
|
const apiOf = (backend) => backend === undefined ? {} : { api: inProcessClient(backend) };
|
|
138
|
+
/**
|
|
139
|
+
* Render an error for the CLI, unwrapping the detail a bare `.message` drops.
|
|
140
|
+
*
|
|
141
|
+
* `Bun.build` reports a bundling failure as an `AggregateError` whose `.message` is a generic
|
|
142
|
+
* "Bundle failed" and whose `.errors` hold the real causes - the unresolved import, the plugin that
|
|
143
|
+
* threw, each with a file and line. `buildClient`/`buildServer` catch a RETURNED `{ success: false }`
|
|
144
|
+
* and print `result.logs`, but a `Bun.build` that THROWS (a plugin `onLoad`/`onResolve` that throws, a
|
|
145
|
+
* resolution failure) rejects before that check runs, so the AggregateError reaches the CLI whole and a
|
|
146
|
+
* catch printing only `.message` throws the causes away. That is the difference between "Bundle failed"
|
|
147
|
+
* and "Could not resolve ./db from routes/x.tsx". So unwrap an AggregateError - directly, or one carried
|
|
148
|
+
* as a `.cause` - into a line per underlying error.
|
|
149
|
+
*/
|
|
150
|
+
export function formatCliError(err) {
|
|
151
|
+
const aggregate = err instanceof AggregateError
|
|
152
|
+
? err
|
|
153
|
+
: err instanceof Error && err.cause instanceof AggregateError
|
|
154
|
+
? err.cause
|
|
155
|
+
: undefined;
|
|
156
|
+
if (aggregate !== undefined && aggregate.errors.length > 0) {
|
|
157
|
+
const outer = err instanceof Error ? err.message : "";
|
|
158
|
+
const head = outer !== "" && outer !== "Bundle failed" ? outer : "build failed";
|
|
159
|
+
// `String(e)` (not `e.message`) so a Bun BuildMessage/ResolveMessage renders with its file + line,
|
|
160
|
+
// not just the bare text. Multi-line entries are indented to stay under the head. Deduplicated
|
|
161
|
+
// because Bun can repeat one cause across `.errors`.
|
|
162
|
+
const details = [...new Set(aggregate.errors.map((e) => String(e).trim()))].map((line) => ` - ${line.replace(/\n/g, "\n ")}`);
|
|
163
|
+
return [head, ...details].join("\n");
|
|
164
|
+
}
|
|
165
|
+
if (err instanceof Error)
|
|
166
|
+
return err.message;
|
|
167
|
+
return String(err);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Refuse `nifra dev --bun` for an app Bun's DEV-server bundler cannot compile.
|
|
171
|
+
*
|
|
172
|
+
* Bun's dev server and `Bun.build` are not the same bundler. `Bun.build` transforms `*.module.css` into a
|
|
173
|
+
* scoped class map (the Bun production build of a CSS-Modules app works, verified), but the dev server's
|
|
174
|
+
* bundler does not: the import compiles to a dangling reference and the browser throws
|
|
175
|
+
* `ReferenceError: import_X_module is not defined` from inside the component - a message that names
|
|
176
|
+
* neither CSS Modules nor the dev server, so the cause is invisible.
|
|
177
|
+
*
|
|
178
|
+
* A silently-broken client is the one outcome worth failing the command over, so this checks up front and
|
|
179
|
+
* names the trade plus both ways out. Narrow by design: only the transform proven missing is refused, so
|
|
180
|
+
* an app without CSS Modules still gets the Bun dev loop.
|
|
181
|
+
*/
|
|
182
|
+
export async function assertBunDevSupportsApp(app) {
|
|
183
|
+
const { Glob } = await import("bun");
|
|
184
|
+
const offenders = [];
|
|
185
|
+
for await (const file of new Glob("**/*.module.css").scan({ cwd: app.cwd, dot: false })) {
|
|
186
|
+
if (/(^|\/)(node_modules|dist|\.nifra|\.git)\//.test(file))
|
|
187
|
+
continue;
|
|
188
|
+
offenders.push(file);
|
|
189
|
+
if (offenders.length >= 5)
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
// Server functions are the same trade with a worse consequence. The client build replaces a `*.fn`
|
|
193
|
+
// module with stubs so its body never reaches a browser; Bun's dev-server bundler takes no plugins, so
|
|
194
|
+
// there it would be bundled WHOLE - the function bodies and every secret they import. Measured, not
|
|
195
|
+
// assumed: a runtime `Bun.plugin` onLoad does not reach `HTMLBundle`, so the module ships raw.
|
|
196
|
+
//
|
|
197
|
+
// `*.server` modules are the same trade again: the client build EMPTIES them, and with no plugins
|
|
198
|
+
// here they ship whole.
|
|
199
|
+
//
|
|
200
|
+
// Both are matched with the SAME regexes the transforms use, imported rather than re-encoded. A
|
|
201
|
+
// hand-written glob here is how these two gates drifted from the pipelines they guard: the old
|
|
202
|
+
// `**/*.fn.{ts,tsx,js,jsx}` missed `.fn.mts`, `.fn.cts`, `.fn.mjs` and `.fn.cjs`, every one of which
|
|
203
|
+
// both build pipelines stub - so a module the framework calls a server function was waved straight
|
|
204
|
+
// into the browser by the check that exists to stop exactly that. A gate written in a second dialect
|
|
205
|
+
// is a gate that eventually disagrees with the thing it guards.
|
|
206
|
+
const leaking = [];
|
|
207
|
+
const serverOnly = [];
|
|
208
|
+
for await (const file of new Glob("**/*.{server,fn,ts,tsx,js,jsx,mts,cts,mjs,cjs}").scan({
|
|
209
|
+
cwd: app.cwd,
|
|
210
|
+
dot: false,
|
|
211
|
+
})) {
|
|
212
|
+
if (/(^|\/)(node_modules|dist|\.nifra|\.git)\//.test(file))
|
|
213
|
+
continue;
|
|
214
|
+
// Strip the source extension once so the conventions' regexes see `src/todos.fn` / `src/db.server`,
|
|
215
|
+
// which is the shape they match. The glob also includes the extensionless `.fn` / `.server` forms;
|
|
216
|
+
// those already have the matcher-ready shape and pass through unchanged.
|
|
217
|
+
const stem = file.replace(/\.[cm]?[jt]sx?$/, "");
|
|
218
|
+
if (SERVER_FN_MODULE.test(stem)) {
|
|
219
|
+
if (leaking.length < 5)
|
|
220
|
+
leaking.push(file);
|
|
221
|
+
}
|
|
222
|
+
else if (SERVER_ONLY_MODULE.test(stem)) {
|
|
223
|
+
if (serverOnly.length < 5)
|
|
224
|
+
serverOnly.push(file);
|
|
225
|
+
}
|
|
226
|
+
if (leaking.length >= 5 && serverOnly.length >= 5)
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
if (serverOnly.length > 0) {
|
|
230
|
+
throw new Error("[nifra] `nifra dev --bun` can't empty `*.server` modules: Bun's DEV-server bundler takes no " +
|
|
231
|
+
"plugins, so a server-only module would be bundled WHOLE into the browser - along with whatever " +
|
|
232
|
+
"it imports and any secret it holds. `nifra build` and `nifra dev` (Vite) both replace it with " +
|
|
233
|
+
"an empty module, so only this pipeline is affected.\n" +
|
|
234
|
+
` Server-only modules found: ${serverOnly.join(", ")}\n` +
|
|
235
|
+
" Use `nifra dev` (the Vite pipeline), or reach these from a route/loader for the Bun dev loop.");
|
|
236
|
+
}
|
|
237
|
+
if (leaking.length > 0) {
|
|
238
|
+
throw new Error("[nifra] `nifra dev --bun` can't transform server functions: Bun's DEV-server bundler takes no " +
|
|
239
|
+
"plugins, so a `*.fn` module would be bundled WHOLE into the browser - its function bodies and " +
|
|
240
|
+
"anything they import, secrets included. `nifra build` and `nifra dev` (Vite) both replace it " +
|
|
241
|
+
"with client stubs, so only this pipeline is affected.\n" +
|
|
242
|
+
` Server-function modules found: ${leaking.join(", ")}\n` +
|
|
243
|
+
" Use `nifra dev` (the Vite pipeline), or move these calls behind a route for the Bun dev loop.");
|
|
244
|
+
}
|
|
245
|
+
if (offenders.length === 0)
|
|
246
|
+
return;
|
|
247
|
+
throw new Error("[nifra] `nifra dev --bun` can't compile CSS Modules: Bun's DEV-server bundler has no `*.module.css` " +
|
|
248
|
+
"transform (its production `Bun.build` does, so `nifra build` is unaffected). The client would load " +
|
|
249
|
+
"with a bare `ReferenceError: import_… is not defined` instead of your component.\n" +
|
|
250
|
+
` CSS Modules found: ${offenders.join(", ")}\n` +
|
|
251
|
+
" Use `nifra dev` (the Vite pipeline handles them), or drop the CSS Modules to use the Bun dev loop.");
|
|
252
|
+
}
|
|
120
253
|
async function dev(app, flags) {
|
|
254
|
+
// `--bun`: the Bun-pipeline dev server - Bun.serve's native HMR bundles + hot-reloads the client while
|
|
255
|
+
// Bun's runtime resolves SSR, with no Vite in the process at all. The mirror of `nifra build --vite`:
|
|
256
|
+
// each pipeline is selectable in BOTH phases, and neither ever runs inside the other.
|
|
257
|
+
if (flags.bun) {
|
|
258
|
+
await assertBunDevSupportsApp(app);
|
|
259
|
+
const { createDevServer } = await import("@nifrajs/web/dev");
|
|
260
|
+
const { framework: fw, routesDir, outDir, backend } = app;
|
|
261
|
+
const server = await createDevServer({
|
|
262
|
+
routesDir,
|
|
263
|
+
outDir,
|
|
264
|
+
clientModule: fw.clientModule,
|
|
265
|
+
port: flags.port,
|
|
266
|
+
// The Bun pipeline's own plugins (SFC compilers etc.) - never `vitePlugins`, which belong to the
|
|
267
|
+
// other pipeline; `assertPipelineSeparation` already refuses a plugin sitting in the wrong slot.
|
|
268
|
+
plugins: asBunPlugins(app.resolvedPlugins.clientPlugins),
|
|
269
|
+
...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
|
|
270
|
+
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
271
|
+
...(fw.define ? { define: fw.define } : {}),
|
|
272
|
+
createApp: (clientEntry, importQuery) => createWebApp({
|
|
273
|
+
adapter: asAdapter(fw.adapter),
|
|
274
|
+
manifest: discoverRoutes(routesDir, { importQuery }),
|
|
275
|
+
clientEntry,
|
|
276
|
+
...apiOf(backend),
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
console.log(`nifra dev (bun) → http://localhost:${server.port}`);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
121
282
|
// Preflight: `nifra dev` needs `vite` resolvable from the project. Run via `bunx @nifrajs/cli dev` the CLI
|
|
122
283
|
// sits in an isolated install where the project's peer deps don't resolve, so the vite import below fails
|
|
123
284
|
// with an opaque ERR_MODULE_NOT_FOUND. Surface the real fix instead.
|
|
@@ -128,26 +289,33 @@ async function dev(app, flags) {
|
|
|
128
289
|
throw new Error("[nifra] `nifra dev` needs `vite` installed in this project. Run it via your workspace-local " +
|
|
129
290
|
"script (`bun run dev`), not `bunx @nifrajs/cli dev` (which can't resolve the project's peers).");
|
|
130
291
|
}
|
|
131
|
-
const { plugin } = await import("bun");
|
|
132
292
|
const { createViteDevServer } = await import("@nifrajs/web/vite");
|
|
133
293
|
const { framework: fw, routesDir, cwd, backend } = app;
|
|
134
|
-
//
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
|
|
294
|
+
// No Bun SSR plugins here. `nifra dev` runs the VITE pipeline, and Vite now resolves and compiles
|
|
295
|
+
// route modules for SSR too (via `ssrLoadModule`), so `.vue`/`.svelte`/Solid are handled by the
|
|
296
|
+
// app's `vitePlugins` on both halves.
|
|
297
|
+
//
|
|
298
|
+
// Registering Bun's SFC plugins alongside was the structural intermix: two toolchains compiling the
|
|
299
|
+
// same file in one process, with only one of them governed by Vite's `resolve.dedupe`. That is what
|
|
300
|
+
// made an app's hand-written React alias fail to reach SSR, and it is the condition being removed -
|
|
301
|
+
// not a redundancy worth keeping "just in case".
|
|
139
302
|
const server = await createViteDevServer({
|
|
140
303
|
root: cwd,
|
|
141
304
|
routesDir,
|
|
142
305
|
clientModule: fw.clientModule,
|
|
143
|
-
plugins:
|
|
306
|
+
plugins: app.resolvedPlugins.vitePlugins,
|
|
307
|
+
...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
|
|
308
|
+
...(fw.publicEnvPrefix !== undefined ? { publicEnvPrefix: fw.publicEnvPrefix } : {}),
|
|
144
309
|
poll: flags.poll,
|
|
145
310
|
port: flags.port,
|
|
146
311
|
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
147
312
|
...(fw.define ? { define: fw.define } : {}),
|
|
148
|
-
|
|
313
|
+
// `load` resolves route modules through VITE, not through Bun. That is what makes the Vite
|
|
314
|
+
// pipeline own the whole phase: the client and the server now agree on every specifier, so
|
|
315
|
+
// `resolve.dedupe` finally governs SSR and the dual-React crash cannot occur.
|
|
316
|
+
createApp: (clientEntry, load) => createWebApp({
|
|
149
317
|
adapter: asAdapter(fw.adapter),
|
|
150
|
-
manifest: discoverRoutes(routesDir, {
|
|
318
|
+
manifest: discoverRoutes(routesDir, { load }),
|
|
151
319
|
clientEntry,
|
|
152
320
|
...apiOf(backend),
|
|
153
321
|
}),
|
|
@@ -155,17 +323,30 @@ async function dev(app, flags) {
|
|
|
155
323
|
console.log(`nifra dev → http://localhost:${server.port}`);
|
|
156
324
|
}
|
|
157
325
|
/**
|
|
158
|
-
* `nifra build --target <t>`
|
|
326
|
+
* `nifra build --target <t>` - package the engine (buildClient + buildServer + prerender) into one
|
|
159
327
|
* command that emits a full deploy dir, so an app no longer hand-writes build-bun.ts + _worker.ts +
|
|
160
|
-
* _routes.json per target. The adapter is imported from `framework.ts` (the edge-bundlable file
|
|
328
|
+
* _routes.json per target. The adapter is imported from `framework.ts` (the edge-bundlable file - never
|
|
161
329
|
* `nifra.config.ts`, which pulls in Vite plugins), and the backend from `backend.ts` when present;
|
|
162
330
|
* `buildTarget` generates the per-target server entry from those + the app's `routes/`.
|
|
163
331
|
*/
|
|
164
|
-
async function buildForTarget(app, target,
|
|
165
|
-
const {
|
|
332
|
+
async function buildForTarget(app, target, flags) {
|
|
333
|
+
const { isBuildTarget, renderSizeReport, BUILD_TARGETS } = await import("@nifrajs/web/build");
|
|
166
334
|
if (!isBuildTarget(target)) {
|
|
167
335
|
throw new Error(`[nifra] unknown --target "${target}". Valid: ${BUILD_TARGETS.join(", ")}.`);
|
|
168
336
|
}
|
|
337
|
+
if (flags.vite && flags.bun) {
|
|
338
|
+
throw new Error("[nifra] `nifra build` takes `--vite` or `--bun`, not both.");
|
|
339
|
+
}
|
|
340
|
+
// Which bundler builds this app. Default is Bun; an app whose ONLY transforms are `vitePlugins` gets
|
|
341
|
+
// Vite instead, because the Bun build cannot run them and would silently omit the work. `--vite`/`--bun`
|
|
342
|
+
// force it, and the one forced combination that would drop transforms throws.
|
|
343
|
+
const decision = chooseBuildPipeline(app.resolvedPlugins, flags.vite ? "vite" : flags.bun ? "bun" : undefined);
|
|
344
|
+
const useVite = decision.pipeline === "vite";
|
|
345
|
+
// Same deploy-dir output - buildTargetVite delegates to the same orchestrator as buildTarget - so only
|
|
346
|
+
// the bundler and the plugin FORMAT (Vite plugins, not Bun) differ.
|
|
347
|
+
const buildTarget = useVite
|
|
348
|
+
? (await import("@nifrajs/web/build-vite")).buildTargetVite
|
|
349
|
+
: (await import("@nifrajs/web/build")).buildTarget;
|
|
169
350
|
const { framework: fw, routesDir, outDir, cwd, backend } = app;
|
|
170
351
|
// The server entry must import the adapter from `framework.ts` (edge-safe), not the loaded config.
|
|
171
352
|
// `loadApp` guarantees one of them exists; prefer framework.ts so a multi-target app's Vite-plugin
|
|
@@ -176,6 +357,18 @@ async function buildForTarget(app, target, report) {
|
|
|
176
357
|
? resolve(cwd, "nifra.config.ts")
|
|
177
358
|
: resolve(cwd, "framework.ts");
|
|
178
359
|
const backendFile = resolve(cwd, "backend.ts");
|
|
360
|
+
// Plugin FORMAT differs by pipeline: the Bun build takes Bun plugins (clientPlugins/serverPlugins); the
|
|
361
|
+
// Vite build takes the app's Vite plugins (fw.vitePlugins) for BOTH halves. buildTargetWith forwards
|
|
362
|
+
// whatever it's given straight to the chosen bundler, which casts to its own plugin type.
|
|
363
|
+
const plugins = useVite
|
|
364
|
+
? {
|
|
365
|
+
clientPlugins: app.resolvedPlugins.vitePlugins,
|
|
366
|
+
serverPlugins: app.resolvedPlugins.vitePlugins,
|
|
367
|
+
}
|
|
368
|
+
: {
|
|
369
|
+
clientPlugins: asBunPlugins(app.resolvedPlugins.clientPlugins),
|
|
370
|
+
serverPlugins: asBunPlugins(app.resolvedPlugins.serverPlugins),
|
|
371
|
+
};
|
|
179
372
|
const result = await buildTarget(target, {
|
|
180
373
|
routesDir,
|
|
181
374
|
outDir,
|
|
@@ -183,27 +376,32 @@ async function buildForTarget(app, target, report) {
|
|
|
183
376
|
clientModule: fw.clientModule,
|
|
184
377
|
adapterImport: frameworkFile,
|
|
185
378
|
...(backend !== undefined && existsSync(backendFile) ? { backendImport: backendFile } : {}),
|
|
186
|
-
|
|
187
|
-
serverPlugins: asBunPlugins(await resolvePlugins(fw.serverPlugins)),
|
|
379
|
+
...plugins,
|
|
188
380
|
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
189
381
|
...(fw.define ? { define: fw.define } : {}),
|
|
190
|
-
|
|
382
|
+
...(fw.publicDir !== undefined ? { publicDir: fw.publicDir } : {}),
|
|
383
|
+
...(fw.publicEnvPrefix !== undefined ? { publicEnvPrefix: fw.publicEnvPrefix } : {}),
|
|
384
|
+
// The static target needs a built app to drive prerendering - only build it when targeting static.
|
|
191
385
|
...(target === "static" ? { prerenderApp: await buildPrerenderApp(app) } : {}),
|
|
192
386
|
});
|
|
193
|
-
console.log(`nifra build (${target}) → ${result.run}`);
|
|
194
|
-
|
|
387
|
+
console.log(`nifra build (${target}${useVite ? ", vite" : ""}) → ${result.run}`);
|
|
388
|
+
// An auto-selected pipeline is stated, never silent: the user asked for the default and got something
|
|
389
|
+
// else, and the reason is the thing that makes that legible instead of surprising.
|
|
390
|
+
if (decision.reason !== undefined)
|
|
391
|
+
console.log(` ${decision.reason}`);
|
|
392
|
+
if (flags.report)
|
|
195
393
|
console.log(`\n${renderSizeReport(result.size)}`);
|
|
196
394
|
}
|
|
197
395
|
/** Build the app FACTORY for the `static` target's prerender pass. Registers the
|
|
198
396
|
* framework's SSR Bun plugins (so `.vue`/`.svelte`/Solid routes import) once, then return a factory that
|
|
199
397
|
* `createWebApp`s the app for the client build's manifest. The client entry MUST be the real content-hashed
|
|
200
|
-
* bundle (`client.entry`)
|
|
398
|
+
* bundle (`client.entry`) - it's the hydration `<script src>` the prerendered HTML emits, so a placeholder
|
|
201
399
|
* would 404 and the pages would render but never hydrate (inert controls). Styles/route-preload are wired
|
|
202
400
|
* from the same manifest so the static HTML matches the deployed app. */
|
|
203
401
|
async function buildPrerenderApp(app) {
|
|
204
402
|
const { plugin } = await import("bun");
|
|
205
403
|
const { framework: fw, routesDir, backend } = app;
|
|
206
|
-
for (const p of asBunPlugins(
|
|
404
|
+
for (const p of asBunPlugins(app.resolvedPlugins.serverPlugins))
|
|
207
405
|
plugin(p);
|
|
208
406
|
return (client) => createWebApp({
|
|
209
407
|
adapter: asAdapter(fw.adapter),
|
|
@@ -225,7 +423,7 @@ async function start(app, flags) {
|
|
|
225
423
|
throw new Error(`[nifra] ${app.outDir} holds a Cloudflare Pages worker bundle (_worker.js), not a self-hosting ` +
|
|
226
424
|
"server. Run `nifra build --target bun` then `nifra start`, or serve this dir with `wrangler pages`.");
|
|
227
425
|
}
|
|
228
|
-
throw new Error(`[nifra] no ${serverFile}
|
|
426
|
+
throw new Error(`[nifra] no ${serverFile} - run \`nifra build\` or \`nifra build --target bun\` first.`);
|
|
229
427
|
}
|
|
230
428
|
Bun.env.PORT = String(flags.port);
|
|
231
429
|
await import(__rewriteRelativeImportExtension(serverFile));
|
|
@@ -239,6 +437,8 @@ export function parseFlags(args) {
|
|
|
239
437
|
let poll = Bun.env.CHOKIDAR_USEPOLLING === "1";
|
|
240
438
|
let target = "bun";
|
|
241
439
|
let report = false;
|
|
440
|
+
let vite = false;
|
|
441
|
+
let bun = false;
|
|
242
442
|
for (let i = 0; i < args.length; i++) {
|
|
243
443
|
const a = args[i];
|
|
244
444
|
if ((a === "--port" || a === "-p") && args[i + 1])
|
|
@@ -251,11 +451,15 @@ export function parseFlags(args) {
|
|
|
251
451
|
target = args[++i];
|
|
252
452
|
else if (a === "--report")
|
|
253
453
|
report = true;
|
|
454
|
+
else if (a === "--vite")
|
|
455
|
+
vite = true;
|
|
456
|
+
else if (a === "--bun")
|
|
457
|
+
bun = true;
|
|
254
458
|
}
|
|
255
459
|
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
|
256
460
|
throw new Error(`[nifra] invalid --port: ${port}`);
|
|
257
461
|
}
|
|
258
|
-
return { port, out, poll, target, report };
|
|
462
|
+
return { port, out, poll, target, report, vite, bun };
|
|
259
463
|
}
|
|
260
464
|
async function main() {
|
|
261
465
|
const argv = Bun.argv.slice(2);
|
|
@@ -268,14 +472,14 @@ async function main() {
|
|
|
268
472
|
console.log(`nifra (@nifrajs/cli) ${CLI_VERSION}`);
|
|
269
473
|
return;
|
|
270
474
|
}
|
|
271
|
-
// `mcp` runs a long-lived stdio server and loads the project lazily per-tool
|
|
475
|
+
// `mcp` runs a long-lived stdio server and loads the project lazily per-tool - it must not go through
|
|
272
476
|
// the eager `loadApp` below (which would fail fast on a project that's API-only / not yet built).
|
|
273
477
|
if (command === "mcp") {
|
|
274
478
|
const { runMcpServer } = await import("./mcp.js");
|
|
275
479
|
await runMcpServer(process.cwd(), CLI_VERSION);
|
|
276
480
|
return;
|
|
277
481
|
}
|
|
278
|
-
// `docs-mcp` runs the PUBLIC docs MCP over HTTP
|
|
482
|
+
// `docs-mcp` runs the PUBLIC docs MCP over HTTP - project-independent (serves the bundled corpus), so
|
|
279
483
|
// it self-hosts anywhere Bun runs (a VPS behind a reverse proxy, a container). Long-lived; no loadApp.
|
|
280
484
|
if (command === "docs-mcp") {
|
|
281
485
|
const { handleMcpHttp } = await import("./mcp-http.js");
|
|
@@ -288,7 +492,7 @@ async function main() {
|
|
|
288
492
|
console.log(`nifra docs MCP (HTTP) → ${server.url}`);
|
|
289
493
|
return;
|
|
290
494
|
}
|
|
291
|
-
// `check` is a pure cwd-based gate (typecheck + lint)
|
|
495
|
+
// `check` is a pure cwd-based gate (typecheck + lint) - it must run even when the project doesn't
|
|
292
496
|
// load (API-only, not built yet), so it dispatches before the eager `loadApp` below.
|
|
293
497
|
if (command === "check") {
|
|
294
498
|
const { runCheck } = await import("./check.js");
|
|
@@ -319,7 +523,15 @@ async function main() {
|
|
|
319
523
|
process.exitCode = 1;
|
|
320
524
|
return;
|
|
321
525
|
}
|
|
322
|
-
// `
|
|
526
|
+
// `sync-routes` regenerates the route-search types `.d.ts` (typed `navigate({ to, search })`) from
|
|
527
|
+
// `routes/`. Pure fs/text (needs only the route tree, not `framework.ts`), so dispatch before `loadApp`.
|
|
528
|
+
if (command === "sync-routes") {
|
|
529
|
+
const { runSyncRoutes } = await import("./sync-routes.js");
|
|
530
|
+
if (!(await runSyncRoutes(process.cwd())))
|
|
531
|
+
process.exitCode = 1;
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
// `doctor` is a pure cwd check (imports vs declared deps) - like `check`, dispatch before `loadApp`
|
|
323
535
|
// so it runs on an API-only / not-yet-built project.
|
|
324
536
|
if (command === "doctor") {
|
|
325
537
|
const { runDoctor } = await import("./doctor.js");
|
|
@@ -330,7 +542,7 @@ async function main() {
|
|
|
330
542
|
process.exitCode = 1;
|
|
331
543
|
return;
|
|
332
544
|
}
|
|
333
|
-
// `snapshot` / `diff` load ONLY backend.ts (the API contract)
|
|
545
|
+
// `snapshot` / `diff` load ONLY backend.ts (the API contract) - like `check`, they must run on an
|
|
334
546
|
// API-only project, so they dispatch before the eager `loadApp`.
|
|
335
547
|
if (command === "snapshot") {
|
|
336
548
|
const { runSnapshot } = await import("./diff-tool.js");
|
|
@@ -345,7 +557,7 @@ async function main() {
|
|
|
345
557
|
await runSnapshot(process.cwd(), out !== undefined ? { out } : {});
|
|
346
558
|
}
|
|
347
559
|
catch (err) {
|
|
348
|
-
console.error(
|
|
560
|
+
console.error(formatCliError(err));
|
|
349
561
|
process.exitCode = 1;
|
|
350
562
|
}
|
|
351
563
|
return;
|
|
@@ -359,7 +571,7 @@ async function main() {
|
|
|
359
571
|
}
|
|
360
572
|
}
|
|
361
573
|
catch (err) {
|
|
362
|
-
console.error(
|
|
574
|
+
console.error(formatCliError(err));
|
|
363
575
|
process.exitCode = 1;
|
|
364
576
|
}
|
|
365
577
|
return;
|
|
@@ -383,7 +595,7 @@ async function main() {
|
|
|
383
595
|
process.exitCode = 1;
|
|
384
596
|
}
|
|
385
597
|
catch (err) {
|
|
386
|
-
console.error(
|
|
598
|
+
console.error(formatCliError(err));
|
|
387
599
|
process.exitCode = 1;
|
|
388
600
|
}
|
|
389
601
|
return;
|
|
@@ -423,7 +635,7 @@ async function main() {
|
|
|
423
635
|
}
|
|
424
636
|
}
|
|
425
637
|
catch (err) {
|
|
426
|
-
console.error(
|
|
638
|
+
console.error(formatCliError(err));
|
|
427
639
|
process.exitCode = 1;
|
|
428
640
|
}
|
|
429
641
|
return;
|
|
@@ -465,7 +677,7 @@ async function main() {
|
|
|
465
677
|
}
|
|
466
678
|
}
|
|
467
679
|
catch (err) {
|
|
468
|
-
console.error(
|
|
680
|
+
console.error(formatCliError(err));
|
|
469
681
|
process.exitCode = 1;
|
|
470
682
|
}
|
|
471
683
|
return;
|
|
@@ -495,13 +707,13 @@ async function main() {
|
|
|
495
707
|
process.exitCode = 1;
|
|
496
708
|
}
|
|
497
709
|
catch (err) {
|
|
498
|
-
console.error(
|
|
710
|
+
console.error(formatCliError(err));
|
|
499
711
|
process.exitCode = 1;
|
|
500
712
|
}
|
|
501
713
|
return;
|
|
502
714
|
}
|
|
503
715
|
// `upgrade` is a pure cwd file-transformer (package.json pins + import moves) driven by a per-release
|
|
504
|
-
// recipe, then verified with `nifra check`. Dispatch before the eager `loadApp`
|
|
716
|
+
// recipe, then verified with `nifra check`. Dispatch before the eager `loadApp` - it must run on any
|
|
505
717
|
// repo (API-only, not built, or mid-upgrade with edits that don't yet typecheck under dry-run).
|
|
506
718
|
if (command === "upgrade") {
|
|
507
719
|
const { runUpgrade } = await import("./upgrade.js");
|
|
@@ -518,12 +730,12 @@ async function main() {
|
|
|
518
730
|
process.exitCode = 1;
|
|
519
731
|
}
|
|
520
732
|
catch (err) {
|
|
521
|
-
console.error(
|
|
733
|
+
console.error(formatCliError(err));
|
|
522
734
|
process.exitCode = 1;
|
|
523
735
|
}
|
|
524
736
|
return;
|
|
525
737
|
}
|
|
526
|
-
// `port` is a pure cwd-based portability linter (scans source, doesn't run the app)
|
|
738
|
+
// `port` is a pure cwd-based portability linter (scans source, doesn't run the app) - like `check`/
|
|
527
739
|
// `doctor`, dispatch before the eager `loadApp` so it runs on an API-only / not-yet-built project.
|
|
528
740
|
if (command === "port") {
|
|
529
741
|
const { runPort } = await import("./port.js");
|
|
@@ -545,7 +757,7 @@ async function main() {
|
|
|
545
757
|
process.exitCode = 1;
|
|
546
758
|
}
|
|
547
759
|
catch (err) {
|
|
548
|
-
console.error(
|
|
760
|
+
console.error(formatCliError(err));
|
|
549
761
|
process.exitCode = 1;
|
|
550
762
|
}
|
|
551
763
|
return;
|
|
@@ -565,11 +777,37 @@ async function main() {
|
|
|
565
777
|
if (command === "dev")
|
|
566
778
|
await dev(app, flags);
|
|
567
779
|
else if (command === "build")
|
|
568
|
-
await buildForTarget(app, flags.target, flags
|
|
780
|
+
await buildForTarget(app, flags.target, flags);
|
|
569
781
|
else if (command === "context")
|
|
570
782
|
console.log(describeProject(app));
|
|
571
783
|
else if (command === "routes") {
|
|
572
|
-
|
|
784
|
+
if (argv.includes("--modes")) {
|
|
785
|
+
// `--modes` answers a different question than the default table: not "what URLs exist" but "how
|
|
786
|
+
// does each render, and can the target I'm about to deploy to actually honour that". Resolves
|
|
787
|
+
// against `--target` when given, so a `static` build's server-needing route fails here, in one
|
|
788
|
+
// place, instead of 404ing in production.
|
|
789
|
+
const { buildRouteManifest, renderRouteManifest } = await import("@nifrajs/web/route-manifest");
|
|
790
|
+
const manifest = discoverRoutes(app.routesDir);
|
|
791
|
+
// `flags.target` defaults to "bun" for `build`; here an EXPLICIT `--target` is what opts into
|
|
792
|
+
// gating, so read it from argv rather than inheriting the build default (which would silently gate
|
|
793
|
+
// every `nifra routes --modes` against bun).
|
|
794
|
+
const targetIdx = argv.indexOf("--target");
|
|
795
|
+
const target = targetIdx !== -1 ? argv[targetIdx + 1] : undefined;
|
|
796
|
+
const routeManifest = await buildRouteManifest(manifest, {
|
|
797
|
+
...(target !== undefined ? { target } : {}),
|
|
798
|
+
});
|
|
799
|
+
if (argv.includes("--json"))
|
|
800
|
+
console.log(JSON.stringify(routeManifest, null, 2));
|
|
801
|
+
else
|
|
802
|
+
console.log(renderRouteManifest(routeManifest));
|
|
803
|
+
// A target that cannot honour a route is a build-breaking fact, not a note: exit non-zero so the
|
|
804
|
+
// same command in CI gates the deploy it is warning about.
|
|
805
|
+
if (routeManifest.conflicts.length > 0)
|
|
806
|
+
process.exitCode = 1;
|
|
807
|
+
}
|
|
808
|
+
else {
|
|
809
|
+
console.log(await describeRoutes(app, { json: argv.includes("--json") }));
|
|
810
|
+
}
|
|
573
811
|
}
|
|
574
812
|
else
|
|
575
813
|
await start(app, flags);
|
|
@@ -578,7 +816,7 @@ async function main() {
|
|
|
578
816
|
// exported `parseFlags`. `import.meta.main` is true only for the process's entry module.
|
|
579
817
|
if (import.meta.main) {
|
|
580
818
|
main().catch((err) => {
|
|
581
|
-
console.error(
|
|
819
|
+
console.error(formatCliError(err));
|
|
582
820
|
process.exitCode = 1;
|
|
583
821
|
});
|
|
584
822
|
}
|