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