@nifrajs/cli 4.0.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +9 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts +12 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +59 -8
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +36 -9
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +22 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +83 -24
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1516 -318
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +203 -70
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +215 -50
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +20 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +73 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +24 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +194 -45
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +18 -1
- package/dist/dev-feed-tool.d.ts.map +1 -1
- package/dist/dev-feed-tool.js +93 -0
- package/dist/dev-feed-tool.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +102 -2
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +18 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +20 -13
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/config-paths.d.ts +16 -0
- package/dist/internal/config-paths.d.ts.map +1 -0
- package/dist/internal/config-paths.js +124 -0
- package/dist/internal/config-paths.js.map +1 -0
- package/dist/internal/installed-package.d.ts +4 -0
- package/dist/internal/installed-package.d.ts.map +1 -0
- package/dist/internal/installed-package.js +17 -0
- package/dist/internal/installed-package.js.map +1 -0
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +29 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +435 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +8 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +22 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +10 -6
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +15 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +87 -25
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +98 -23
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +33 -10
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +60 -10
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +8 -23
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +2 -2
- package/dist/mcp-delegate.d.ts.map +1 -1
- package/dist/mcp-delegate.js +19 -7
- package/dist/mcp-delegate.js.map +1 -1
- package/dist/mcp-exec.d.ts +13 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +56 -14
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +30 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +63 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-path.d.ts +2 -1
- package/dist/mcp-path.d.ts.map +1 -1
- package/dist/mcp-path.js +9 -24
- package/dist/mcp-path.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +47 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1972 -170
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +128 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +975 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.d.ts.map +1 -1
- package/dist/project-path.js +48 -14
- package/dist/project-path.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +5 -0
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +14 -0
- package/dist/release-verification.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +43 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +12 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +12 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +13 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +356 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +178 -44
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +50 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +436 -4
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +13 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +313 -69
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +218 -44
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +290 -24
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +6 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +84 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +498 -78
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +16 -2
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +94 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +55 -7
- package/docs/llms-full.txt +2043 -222
- package/docs/types.json +1573 -246
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +18 -2
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +47 -9
- package/src/check-diagnostics.ts +75 -9
- package/src/check-scan.ts +1816 -377
- package/src/check.ts +258 -81
- package/src/cli.ts +242 -37
- package/src/client-module.ts +97 -0
- package/src/command-catalog.ts +237 -52
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/dev-feed-tool.ts +112 -1
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +116 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +21 -14
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +477 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +48 -5
- package/src/internal/typescript-import.ts +15 -6
- package/src/introspect.ts +104 -28
- package/src/levels-tool.ts +124 -29
- package/src/load.ts +107 -17
- package/src/mcp-context.ts +12 -25
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +68 -18
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +74 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-path.ts +8 -22
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +56 -22
- package/src/migrate-layout.ts +2257 -176
- package/src/output-schemas.ts +1240 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/project-path.ts +44 -13
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/release-verification.ts +20 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +44 -10
- package/src/rules/codes.ts +12 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +391 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +206 -52
- package/src/rules/pages.ts +481 -4
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +391 -72
- package/src/rules/zones.ts +232 -42
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +251 -24
- package/src/test-tool.ts +12 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +576 -72
- package/src/verification-plan.ts +33 -2
- package/src/version-drift.ts +118 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/cli.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { formatShadowedPages, shadowedPages } from "@nifrajs/web/route-manifest"
|
|
|
24
24
|
import type { BunPlugin } from "bun"
|
|
25
25
|
import { adapterFile, BACKEND_APP_FILE } from "./app-files.ts"
|
|
26
26
|
import { bindCommandArgv, findCommandSpec, renderCommandCatalogHelp } from "./command-catalog.ts"
|
|
27
|
-
import { applyEnvFiles, forgetAutoLoadedEnv, takeEnvFileFlags } from "./env-file.ts"
|
|
27
|
+
import { applyEnvFiles, forgetAutoLoadedEnv, REFLECT_ENV, takeEnvFileFlags } from "./env-file.ts"
|
|
28
28
|
import { FRAMEWORK_WEB_OPTIONS, type LoadedApp, loadApp, type NifraFramework } from "./load.ts"
|
|
29
29
|
import { chooseBuildPipeline, describePipeline } from "./pipeline-guard.ts"
|
|
30
30
|
import {
|
|
@@ -126,6 +126,9 @@ Usage:
|
|
|
126
126
|
one left. Exits 1 while current-code errors are open.
|
|
127
127
|
nifra logs [--since <n>] [--level <l>] [--grep <s>] The dev server's console, server and
|
|
128
128
|
browser, each line tagged with its request.
|
|
129
|
+
nifra explain [<message>] [--stack <s>] [--name <n>] A structured diagnostic for an error: its
|
|
130
|
+
code, the frame in your source, and the cause and fix when
|
|
131
|
+
nifra knows them. No message: the dev server's latest error.
|
|
129
132
|
nifra cdn-check <url> Check a deployed page behind a CDN: served from cache, no
|
|
130
133
|
internal or CDN-only headers reaching the visitor, and soft
|
|
131
134
|
navigations get page data, not the cached document. Exits 1
|
|
@@ -165,6 +168,10 @@ Usage:
|
|
|
165
168
|
against it: a route the target can't honour (an ssr route on a
|
|
166
169
|
static build, ISR where there's no revalidation) exits nonzero
|
|
167
170
|
with the consequence - run in CI so it fails the build, not prod.
|
|
171
|
+
nifra routes --hooks [--json] List the hooks each backend route and each prefix's unmatched
|
|
172
|
+
[--since <file>] paths actually run: kind, plugin name, and where it was
|
|
173
|
+
registered (app, group, merge, mount). --since diffs against a
|
|
174
|
+
saved --json report.
|
|
168
175
|
nifra init-agents [--force] [--json] Retrofit an EXISTING app with the agent files a new app ships:
|
|
169
176
|
.mcp.json + .cursor/mcp.json (register this project's nifra MCP),
|
|
170
177
|
each agent's pointer to AGENTS.md (CLAUDE.md, GEMINI.md,
|
|
@@ -201,6 +208,8 @@ Usage:
|
|
|
201
208
|
backend code in browser code). Run as "done"; --json for agents;
|
|
202
209
|
--sarif emits the same stable diagnostics as SARIF 2.1.0 for external
|
|
203
210
|
review tools; --lints-only skips tsc for a near-instant inner-loop pass.
|
|
211
|
+
With a contracts.lock.json, both modes import backend/app.ts
|
|
212
|
+
(running its top-level code) to compare the routes it serves.
|
|
204
213
|
nifra verify [--release] [--json] Run the shared repository verification gate. --release runs the
|
|
205
214
|
full build, test, coverage, corpus, consumer, and cross-runtime gate.
|
|
206
215
|
nifra fix [--code <NF-code>] Apply a registered diagnostic recipe, then print the remaining
|
|
@@ -262,20 +271,28 @@ Usage:
|
|
|
262
271
|
[--list] [--json] apply exact imports, pin every matching dependency (keeping
|
|
263
272
|
[--exact] ^/~, or exact with --exact), then verify with nifra check.
|
|
264
273
|
[--allow-downgrade] Dry-run by default; --write applies then verifies (--no-verify
|
|
265
|
-
|
|
274
|
+
[--no-hooks] to skip), then prints how hook scope changed (--no-hooks to
|
|
275
|
+
skip). --list shows available targets. A version newer than
|
|
266
276
|
this CLI prints the command for that release's CLI. Fail-closed
|
|
267
277
|
on an unknown version or a rollback (--allow-downgrade
|
|
268
278
|
overrides). Deterministic + idempotent.
|
|
269
|
-
nifra migrate layout [--write] [--json]
|
|
279
|
+
nifra migrate layout [--write] [--zones] [--json]
|
|
270
280
|
Move an app onto the frontend/backend split: split each route
|
|
271
281
|
into x.tsx + x.backend.ts, fold _middleware.ts into
|
|
272
282
|
_layout.backend.ts, move backend.ts/framework.ts under backend/,
|
|
273
|
-
and zone the other modules into frontend/, backend/ or shared
|
|
274
|
-
|
|
283
|
+
and zone the other modules into frontend/, backend/ or shared/
|
|
284
|
+
(--zones keeps them in place and lists them in package.json
|
|
285
|
+
"nifra.zones"). Dry-run by default; --write applies.
|
|
275
286
|
nifra types [--check] [--json] Generate each route's ./+types module under .nifra/types: its
|
|
276
287
|
params, the data its output schemas let through, and api typed
|
|
277
288
|
by backend/app.ts. dev, build and check refresh them too;
|
|
278
289
|
--check fails when one is stale.
|
|
290
|
+
nifra schemas [--write] [--check] [--json]
|
|
291
|
+
Draft loaderOutput/actionOutput from each loader's and
|
|
292
|
+
action's return type for review; --write adds the missing
|
|
293
|
+
ones (marked nifra:generated) and refreshes generated ones.
|
|
294
|
+
Lists the returned fields a hand-written schema drops;
|
|
295
|
+
--check fails on a missing, stale or any-typed one.
|
|
279
296
|
nifra port [--target <t>] [--json] Portability linter: print a feature × deploy-target capability
|
|
280
297
|
[--ci] [--strict] matrix (in-memory stores, in-process cron/WebSocket, Bun/Deno
|
|
281
298
|
globals, node: builtins) with file:line evidence. --target auto-
|
|
@@ -304,12 +321,23 @@ set in the process environment is never overwritten by a file.
|
|
|
304
321
|
${renderCommandCatalogHelp()}`
|
|
305
322
|
|
|
306
323
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
307
|
-
const CLI_VERSION = "
|
|
324
|
+
const CLI_VERSION = "5.1.0"
|
|
308
325
|
|
|
309
326
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
310
327
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
|
311
328
|
const asBunPlugins = (v: readonly unknown[]): BunPlugin[] => v as BunPlugin[]
|
|
312
329
|
|
|
330
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Trusted application configuration supplies native Vite plugins; the pinned pipeline validates their hooks.
|
|
331
|
+
const asComponentPlugins = (
|
|
332
|
+
value: readonly unknown[],
|
|
333
|
+
): NonNullable<
|
|
334
|
+
import("@nifrajs/web/server-components/build").ComponentPipelineOptions["plugins"]
|
|
335
|
+
> =>
|
|
336
|
+
// biome-ignore lint/plugin/requireSafetyCommentForTypeAssertion: Trusted config forwards the pinned Vite plugin ABI; this does not accept request data.
|
|
337
|
+
value as NonNullable<
|
|
338
|
+
import("@nifrajs/web/server-components/build").ComponentPipelineOptions["plugins"]
|
|
339
|
+
>
|
|
340
|
+
|
|
313
341
|
/**
|
|
314
342
|
* Render an error for the CLI, unwrapping the detail a bare `.message` drops.
|
|
315
343
|
*
|
|
@@ -344,7 +372,36 @@ export function formatCliError(err: unknown): string {
|
|
|
344
372
|
return String(err)
|
|
345
373
|
}
|
|
346
374
|
|
|
375
|
+
/** A failed command's message on stderr and, for a `--json` caller parsing stdout, as JSON there. */
|
|
376
|
+
function reportFailure(err: unknown, argv: readonly string[]): void {
|
|
377
|
+
const message = formatCliError(err)
|
|
378
|
+
console.error(message)
|
|
379
|
+
if (argv.includes("--json") || argv.includes("--json=true"))
|
|
380
|
+
console.log(JSON.stringify({ ok: false, error: message }, null, 2))
|
|
381
|
+
}
|
|
382
|
+
|
|
347
383
|
async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
384
|
+
if (app.framework.componentPipeline !== undefined) {
|
|
385
|
+
if (flags.bun)
|
|
386
|
+
throw new Error(
|
|
387
|
+
"[nifra/components] Components require the Vite 8 pipeline; Bun remains a server runtime",
|
|
388
|
+
)
|
|
389
|
+
const { devServerComponents } = await import("@nifrajs/web/server-components/build")
|
|
390
|
+
if (
|
|
391
|
+
app.resolvedPlugins.clientPlugins.length > 0 ||
|
|
392
|
+
app.resolvedPlugins.serverPlugins.length > 0
|
|
393
|
+
)
|
|
394
|
+
throw new Error(
|
|
395
|
+
"[nifra/components] Use Vite equivalents for client/server Bun plugins in the component pipeline",
|
|
396
|
+
)
|
|
397
|
+
await devServerComponents({
|
|
398
|
+
...app.framework.componentPipeline,
|
|
399
|
+
root: app.cwd,
|
|
400
|
+
port: flags.port,
|
|
401
|
+
plugins: asComponentPlugins(app.resolvedPlugins.vitePlugins),
|
|
402
|
+
})
|
|
403
|
+
return
|
|
404
|
+
}
|
|
348
405
|
if (flags.vite && flags.bun) {
|
|
349
406
|
throw new Error("[nifra] `nifra dev` takes `--vite` or `--bun`, not both.")
|
|
350
407
|
}
|
|
@@ -511,6 +568,7 @@ async function dev(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
511
568
|
...(fw.conditions ? { conditions: fw.conditions } : {}),
|
|
512
569
|
...(fw.define ? { define: fw.define } : {}),
|
|
513
570
|
indicator: showsIndicator(flags, fw),
|
|
571
|
+
allowedHosts: fw.dev?.allowedHosts ?? [],
|
|
514
572
|
createApp: (clientEntry, importQuery, dev) =>
|
|
515
573
|
createWebApp({
|
|
516
574
|
adapter: asAdapter(fw.adapter),
|
|
@@ -693,7 +751,7 @@ async function assertFreshWorkspaceDists(cwd: string): Promise<void> {
|
|
|
693
751
|
let rootPackage: Record<string, unknown>
|
|
694
752
|
try {
|
|
695
753
|
rootPackage = JSON.parse(readFileSync(pkgPath, "utf8")) as Record<string, unknown>
|
|
696
|
-
//
|
|
754
|
+
// nifra-expect NF-S001: not a security gate. An unreadable/non-JSON package.json is a separate
|
|
697
755
|
// build problem; returning here skips the freshness check rather than masking a denial.
|
|
698
756
|
} catch {
|
|
699
757
|
return // an unreadable/!JSON package.json is a separate problem; don't mask it as a stale-dist failure
|
|
@@ -727,6 +785,40 @@ async function buildForTarget(
|
|
|
727
785
|
throw new Error("[nifra] `nifra build` takes `--vite` or `--bun`, not both.")
|
|
728
786
|
}
|
|
729
787
|
const { framework: fw } = app
|
|
788
|
+
if (fw.componentPipeline !== undefined) {
|
|
789
|
+
if (flags.bun)
|
|
790
|
+
throw new Error(
|
|
791
|
+
"[nifra/components] Components require the Vite 8 pipeline; Bun remains a server runtime",
|
|
792
|
+
)
|
|
793
|
+
if (target === "static")
|
|
794
|
+
throw new Error(
|
|
795
|
+
"[nifra/components] Component owners require private per-request execution; choose bun, node, deno, cloudflare, or vercel",
|
|
796
|
+
)
|
|
797
|
+
if (
|
|
798
|
+
app.resolvedPlugins.clientPlugins.length > 0 ||
|
|
799
|
+
app.resolvedPlugins.serverPlugins.length > 0
|
|
800
|
+
)
|
|
801
|
+
throw new Error(
|
|
802
|
+
"[nifra/components] Use Vite equivalents for client/server Bun plugins in the component pipeline",
|
|
803
|
+
)
|
|
804
|
+
const { buildServerComponents } = await import("@nifrajs/web/server-components/build")
|
|
805
|
+
const outDir =
|
|
806
|
+
target === "vercel" && flags.out === undefined
|
|
807
|
+
? resolve(app.cwd, ".vercel/output")
|
|
808
|
+
: app.outDir
|
|
809
|
+
const result = await buildServerComponents({
|
|
810
|
+
...fw.componentPipeline,
|
|
811
|
+
target,
|
|
812
|
+
root: app.cwd,
|
|
813
|
+
outDir,
|
|
814
|
+
plugins: asComponentPlugins(app.resolvedPlugins.vitePlugins),
|
|
815
|
+
})
|
|
816
|
+
await assertFreshWorkspaceDists(app.cwd)
|
|
817
|
+
console.log(
|
|
818
|
+
`[nifra/components] Built ${fw.componentPipeline.adapter} for ${target} (${result.build}) in ${outDir}`,
|
|
819
|
+
)
|
|
820
|
+
return
|
|
821
|
+
}
|
|
730
822
|
const cssCodeSplitRequested = fw.cssCodeSplit === false
|
|
731
823
|
const hasBunTransforms =
|
|
732
824
|
app.resolvedPlugins.clientPlugins.length > 0 || app.resolvedPlugins.serverPlugins.length > 0
|
|
@@ -876,35 +968,73 @@ async function start(app: LoadedApp, flags: Flags): Promise<void> {
|
|
|
876
968
|
await import(serverFile)
|
|
877
969
|
}
|
|
878
970
|
|
|
971
|
+
const VALUE_FLAGS: Readonly<Record<string, "port" | "out" | "target">> = {
|
|
972
|
+
"--port": "port",
|
|
973
|
+
"-p": "port",
|
|
974
|
+
"--out": "out",
|
|
975
|
+
"--target": "target",
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
type SwitchField = "poll" | "report" | "vite" | "bun" | "allowDuplicateIdentity" | "noIndicator"
|
|
979
|
+
|
|
980
|
+
const SWITCH_FLAGS: Readonly<Record<string, SwitchField>> = {
|
|
981
|
+
"--poll": "poll",
|
|
982
|
+
"--report": "report",
|
|
983
|
+
"--vite": "vite",
|
|
984
|
+
"--bun": "bun",
|
|
985
|
+
"--allow-duplicate-identity": "allowDuplicateIdentity",
|
|
986
|
+
"--no-indicator": "noIndicator",
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
/** The flags of `nifra dev`, `build` and `start`, as `--flag value` or `--flag=value`. Anything else is
|
|
990
|
+
* refused: an ignored `--target=vercel` would build the default target and exit 0. */
|
|
879
991
|
export function parseFlags(args: readonly string[]): Flags {
|
|
880
992
|
// Port precedence: `--port`/`-p` (most specific) > `PORT` env > the framework default. The default is
|
|
881
993
|
// the SAME uncommon port for `nifra dev` and `nifra start` (DEFAULT_DEV_PORT) so a project's URL is
|
|
882
994
|
// stable across commands and doesn't collide with the usual 3000/5173/8080 crowd.
|
|
883
995
|
let port = Number(Bun.env.PORT ?? DEFAULT_DEV_PORT)
|
|
884
996
|
let out: string | undefined
|
|
885
|
-
let poll = Bun.env.CHOKIDAR_USEPOLLING === "1"
|
|
886
997
|
let target: string | undefined
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
998
|
+
const switches: Record<SwitchField, boolean> = {
|
|
999
|
+
poll: Bun.env.CHOKIDAR_USEPOLLING === "1",
|
|
1000
|
+
report: false,
|
|
1001
|
+
vite: false,
|
|
1002
|
+
bun: false,
|
|
1003
|
+
allowDuplicateIdentity: false,
|
|
1004
|
+
noIndicator: false,
|
|
1005
|
+
}
|
|
892
1006
|
for (let i = 0; i < args.length; i++) {
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
1007
|
+
const arg = args[i] as string
|
|
1008
|
+
const equals = arg.startsWith("--") ? arg.indexOf("=") : -1
|
|
1009
|
+
const name = equals === -1 ? arg : arg.slice(0, equals)
|
|
1010
|
+
const valueField = Object.hasOwn(VALUE_FLAGS, name) ? VALUE_FLAGS[name] : undefined
|
|
1011
|
+
if (valueField !== undefined) {
|
|
1012
|
+
const value = equals === -1 ? args[++i] : arg.slice(equals + 1)
|
|
1013
|
+
if (value === undefined || value === "" || (equals === -1 && value.startsWith("-")))
|
|
1014
|
+
throw new Error(`[nifra] ${name} needs a value`)
|
|
1015
|
+
if (valueField === "port") port = Number(value)
|
|
1016
|
+
else if (valueField === "out") out = value
|
|
1017
|
+
else target = value
|
|
1018
|
+
continue
|
|
1019
|
+
}
|
|
1020
|
+
const switchField = Object.hasOwn(SWITCH_FLAGS, name) ? SWITCH_FLAGS[name] : undefined
|
|
1021
|
+
if (switchField !== undefined) {
|
|
1022
|
+
const value = equals === -1 ? "true" : arg.slice(equals + 1)
|
|
1023
|
+
if (value !== "true" && value !== "false")
|
|
1024
|
+
throw new Error(`[nifra] ${name} takes no value (or =true / =false)`)
|
|
1025
|
+
switches[switchField] = value === "true"
|
|
1026
|
+
continue
|
|
1027
|
+
}
|
|
1028
|
+
throw new Error(
|
|
1029
|
+
arg.startsWith("-")
|
|
1030
|
+
? `[nifra] unknown option: ${arg} - see \`nifra help\``
|
|
1031
|
+
: `[nifra] unexpected argument: ${arg} - see \`nifra help\``,
|
|
1032
|
+
)
|
|
903
1033
|
}
|
|
904
1034
|
if (!Number.isFinite(port) || port < 0 || port > 65535) {
|
|
905
1035
|
throw new Error(`[nifra] invalid --port: ${port}`)
|
|
906
1036
|
}
|
|
907
|
-
return { port, out,
|
|
1037
|
+
return { port, out, target, ...switches }
|
|
908
1038
|
}
|
|
909
1039
|
|
|
910
1040
|
/**
|
|
@@ -945,6 +1075,42 @@ function installReflectionExitHint(): (command: string | undefined) => void {
|
|
|
945
1075
|
}
|
|
946
1076
|
}
|
|
947
1077
|
|
|
1078
|
+
/** Commands that import the app only to read what it declares, never sending it a request, so the
|
|
1079
|
+
* app may skip connecting to its services. `assure` qualifies without its bundle lane, which renders. */
|
|
1080
|
+
const READ_ONLY_COMMANDS: ReadonlySet<string> = new Set([
|
|
1081
|
+
"capabilities",
|
|
1082
|
+
"check",
|
|
1083
|
+
"context",
|
|
1084
|
+
"contracts",
|
|
1085
|
+
"diff",
|
|
1086
|
+
"doctor",
|
|
1087
|
+
"manifest",
|
|
1088
|
+
"openapi",
|
|
1089
|
+
"routes",
|
|
1090
|
+
"sync-manifest",
|
|
1091
|
+
"sync-routes",
|
|
1092
|
+
])
|
|
1093
|
+
|
|
1094
|
+
const HELD_OPEN_GRACE_MS = 2_000
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* A command that imports the app for its answer has already answered when the app's own work - a
|
|
1098
|
+
* database or Redis client reconnecting, an interval - would keep the process alive indefinitely.
|
|
1099
|
+
* A process that exits on its own never reaches this unref'd timer; a held one is ended with a note.
|
|
1100
|
+
*/
|
|
1101
|
+
function exitIfHeldOpen(command: string): void {
|
|
1102
|
+
setTimeout(() => {
|
|
1103
|
+
process.stderr.write(
|
|
1104
|
+
`[nifra] \`nifra ${command}\` has finished, but something the app started when it was imported ` +
|
|
1105
|
+
`(an open connection or a timer) is keeping the process alive, so nifra is exiting.` +
|
|
1106
|
+
(process.env[REFLECT_ENV] === command
|
|
1107
|
+
? ` To skip that work while nifra only reads the app, check process.env.${REFLECT_ENV}, which is set to "${command}".\n`
|
|
1108
|
+
: "\n"),
|
|
1109
|
+
)
|
|
1110
|
+
process.exit(typeof process.exitCode === "number" ? process.exitCode : 0)
|
|
1111
|
+
}, HELD_OPEN_GRACE_MS).unref()
|
|
1112
|
+
}
|
|
1113
|
+
|
|
948
1114
|
/** Set on the copy of `nifra mcp` that serves without `.env` files, so it does not restart again. */
|
|
949
1115
|
const MCP_WITHOUT_ENV_FILES = "NIFRA_MCP_WITHOUT_ENV_FILES"
|
|
950
1116
|
|
|
@@ -1051,6 +1217,9 @@ async function main(): Promise<void> {
|
|
|
1051
1217
|
const catalogSpec = findCommandSpec(dbSub === undefined ? command : `db-${dbSub}`)
|
|
1052
1218
|
if (catalogSpec?.transports.includes("cli")) {
|
|
1053
1219
|
const markReflecting = installReflectionExitHint()
|
|
1220
|
+
if (READ_ONLY_COMMANDS.has(command) || (command === "assure" && !assureBundleRequested(argv))) {
|
|
1221
|
+
process.env[REFLECT_ENV] = command
|
|
1222
|
+
}
|
|
1054
1223
|
try {
|
|
1055
1224
|
const input = bindCommandArgv(catalogSpec, argv.slice(dbSub === undefined ? 1 : 2))
|
|
1056
1225
|
markReflecting(command)
|
|
@@ -1073,7 +1242,7 @@ async function main(): Promise<void> {
|
|
|
1073
1242
|
// A thrown error is nifra's own reporting path, not a silent abort: clear the sentinel so the
|
|
1074
1243
|
// hint stays reserved for the case where the process died inside the app import.
|
|
1075
1244
|
markReflecting(undefined)
|
|
1076
|
-
|
|
1245
|
+
reportFailure(err, argv)
|
|
1077
1246
|
const candidate =
|
|
1078
1247
|
typeof err === "object" && err !== null && "exitCode" in err
|
|
1079
1248
|
? (err as { readonly exitCode?: unknown }).exitCode
|
|
@@ -1086,6 +1255,7 @@ async function main(): Promise<void> {
|
|
|
1086
1255
|
? candidate
|
|
1087
1256
|
: 1
|
|
1088
1257
|
}
|
|
1258
|
+
exitIfHeldOpen(command)
|
|
1089
1259
|
return
|
|
1090
1260
|
}
|
|
1091
1261
|
if (command === "verify") {
|
|
@@ -1135,6 +1305,7 @@ async function main(): Promise<void> {
|
|
|
1135
1305
|
console.error(formatCliError(err))
|
|
1136
1306
|
process.exitCode = 1
|
|
1137
1307
|
}
|
|
1308
|
+
exitIfHeldOpen(command)
|
|
1138
1309
|
return
|
|
1139
1310
|
}
|
|
1140
1311
|
// `upgrade` is a pure cwd file-transformer (package.json pins + import moves) driven by a per-release
|
|
@@ -1150,15 +1321,17 @@ async function main(): Promise<void> {
|
|
|
1150
1321
|
json: argv.includes("--json"),
|
|
1151
1322
|
list: argv.includes("--list"),
|
|
1152
1323
|
verify: !argv.includes("--no-verify"),
|
|
1324
|
+
hooks: !argv.includes("--no-hooks"),
|
|
1153
1325
|
allowDowngrade: argv.includes("--allow-downgrade"),
|
|
1154
1326
|
exact: argv.includes("--exact"),
|
|
1155
1327
|
cliVersion: CLI_VERSION,
|
|
1156
1328
|
})
|
|
1157
1329
|
if (!ok) process.exitCode = 1
|
|
1158
1330
|
} catch (err) {
|
|
1159
|
-
|
|
1331
|
+
reportFailure(err, argv)
|
|
1160
1332
|
process.exitCode = 1
|
|
1161
1333
|
}
|
|
1334
|
+
exitIfHeldOpen(command)
|
|
1162
1335
|
return
|
|
1163
1336
|
}
|
|
1164
1337
|
// `port` is a pure cwd-based portability linter (scans source, doesn't run the app) - like `check`/
|
|
@@ -1169,17 +1342,32 @@ async function main(): Promise<void> {
|
|
|
1169
1342
|
process.exitCode = 1
|
|
1170
1343
|
return
|
|
1171
1344
|
}
|
|
1345
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
1346
|
+
console.log(HELP)
|
|
1347
|
+
return
|
|
1348
|
+
}
|
|
1172
1349
|
const flags = parseFlags(argv.slice(1))
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1350
|
+
try {
|
|
1351
|
+
const app = await loadApp(process.cwd(), flags.out)
|
|
1352
|
+
if (command !== "start" && process.env.NIFRA_BUN_DEV_TOKEN === undefined) {
|
|
1353
|
+
const { unservedRoutesWarning } = await import("./introspect.ts")
|
|
1354
|
+
const unserved = unservedRoutesWarning(app)
|
|
1355
|
+
if (unserved !== undefined) console.warn(unserved)
|
|
1356
|
+
}
|
|
1357
|
+
if (command !== "start") {
|
|
1358
|
+
const { refreshRouteTypes, watchRouteTypes } = await import("./route-types.ts")
|
|
1359
|
+
refreshRouteTypes(app.cwd)
|
|
1360
|
+
// A relaunched `nifra dev --bun` child runs beside its parent, which already watches.
|
|
1361
|
+
if (command === "dev" && process.env.NIFRA_BUN_DEV_TOKEN === undefined)
|
|
1362
|
+
watchRouteTypes(app.cwd)
|
|
1363
|
+
}
|
|
1364
|
+
if (command === "dev") await dev(app, flags)
|
|
1365
|
+
else if (command === "build") await buildForTarget(app, flags.target, flags)
|
|
1366
|
+
else await start(app, flags)
|
|
1367
|
+
} finally {
|
|
1368
|
+
// `dev` and `start` keep serving after they return; a build is over.
|
|
1369
|
+
if (command === "build") exitIfHeldOpen(command)
|
|
1179
1370
|
}
|
|
1180
|
-
if (command === "dev") await dev(app, flags)
|
|
1181
|
-
else if (command === "build") await buildForTarget(app, flags.target, flags)
|
|
1182
|
-
else await start(app, flags)
|
|
1183
1371
|
}
|
|
1184
1372
|
|
|
1185
1373
|
/**
|
|
@@ -1278,12 +1466,29 @@ function findCliCommand(values: readonly string[]): number {
|
|
|
1278
1466
|
return values.findIndex((value) => isCliCommandToken(value))
|
|
1279
1467
|
}
|
|
1280
1468
|
|
|
1469
|
+
/**
|
|
1470
|
+
* The command vector from one runtime argv: the command, then its arguments. Arguments typed before
|
|
1471
|
+
* the command (`nifra --json check`) follow the command's own, ahead of any `--`, rather than being
|
|
1472
|
+
* dropped. Only when index 1 is the script: a runtime flag there means the leading entries are the
|
|
1473
|
+
* runtime's own, not the caller's.
|
|
1474
|
+
*/
|
|
1475
|
+
export function commandArgv(values: readonly string[], commandIndex: number): readonly string[] {
|
|
1476
|
+
const command = values.slice(commandIndex)
|
|
1477
|
+
const script = values[1]
|
|
1478
|
+
if (commandIndex <= 2 || script === undefined || script.startsWith("-")) return command
|
|
1479
|
+
const leading = values.slice(2, commandIndex)
|
|
1480
|
+
const separator = command.indexOf("--")
|
|
1481
|
+
return separator === -1
|
|
1482
|
+
? [...command, ...leading]
|
|
1483
|
+
: [...command.slice(0, separator), ...leading, ...command.slice(separator)]
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1281
1486
|
function cliArgv(): readonly string[] {
|
|
1282
1487
|
const reexec = reexecCliArgv()
|
|
1283
1488
|
if (reexec !== undefined) return reexec
|
|
1284
1489
|
for (const values of [Bun.argv, process.argv]) {
|
|
1285
1490
|
const commandIndex = findCliCommand(values)
|
|
1286
|
-
if (commandIndex >= 0) return values
|
|
1491
|
+
if (commandIndex >= 0) return commandArgv(values, commandIndex)
|
|
1287
1492
|
}
|
|
1288
1493
|
return Bun.argv.slice(2)
|
|
1289
1494
|
}
|
|
@@ -1349,7 +1554,7 @@ if (isMainModule() || isBunDevReexecChild() || hasCliCommandToken()) {
|
|
|
1349
1554
|
try {
|
|
1350
1555
|
await main()
|
|
1351
1556
|
} catch (err) {
|
|
1352
|
-
|
|
1557
|
+
reportFailure(err, cliArgv())
|
|
1353
1558
|
process.exitCode = 1
|
|
1354
1559
|
}
|
|
1355
1560
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An app's `clientModule` when its config names a first-party file. The browser bundle's generated
|
|
3
|
+
* entry imports it, so the build classifies that file, and everything it imports, as browser code.
|
|
4
|
+
*/
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
6
|
+
import { join } from "node:path"
|
|
7
|
+
import type * as TS from "typescript"
|
|
8
|
+
import { CONFIG_FILE, FRAMEWORK_FILE } from "./app-files.ts"
|
|
9
|
+
import { stripComments } from "./check-scan.ts"
|
|
10
|
+
import { configPathReader, exportedInitializer } from "./internal/config-paths.ts"
|
|
11
|
+
import type { SpecifierResolver } from "./internal/project-specifiers.ts"
|
|
12
|
+
|
|
13
|
+
const CLIENT_MODULE_EXPORT =
|
|
14
|
+
/\bexport\s+const\s+clientModule\s*(?::\s*string\s*)?=\s*(?:"([^"\\\r\n]*)"|'([^'\\\r\n]*)')/
|
|
15
|
+
|
|
16
|
+
export interface ClientModuleRoot {
|
|
17
|
+
/** The project-relative config file that names it. */
|
|
18
|
+
readonly config: string
|
|
19
|
+
readonly line: number
|
|
20
|
+
readonly specifier: string
|
|
21
|
+
/** The real path of the file it names. */
|
|
22
|
+
readonly file: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The project-relative directory of every app among `files`: one holding a nifra config. */
|
|
26
|
+
export function appDirs(files: readonly string[]): string[] {
|
|
27
|
+
const apps = new Set<string>()
|
|
28
|
+
for (const file of files) {
|
|
29
|
+
for (const config of [CONFIG_FILE, FRAMEWORK_FILE]) {
|
|
30
|
+
if (file === config) apps.add("")
|
|
31
|
+
else if (file.endsWith(`/${config}`)) apps.add(file.slice(0, -config.length - 1))
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return [...apps]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The file `clientModule` names in `app`, read the way the loader reads it: from `nifra.config.ts`
|
|
39
|
+
* when it exists, else `backend/framework.ts`; a string against the app root, and, with `ts`, a
|
|
40
|
+
* `resolve`/`join` of the config's own folder against that folder. `undefined` for a package
|
|
41
|
+
* specifier, a value built any other way, or a path to no file.
|
|
42
|
+
*/
|
|
43
|
+
export function clientModuleRoot(
|
|
44
|
+
root: string,
|
|
45
|
+
app: string,
|
|
46
|
+
resolve: SpecifierResolver,
|
|
47
|
+
ts?: typeof TS,
|
|
48
|
+
): ClientModuleRoot | undefined {
|
|
49
|
+
const config = existsSync(join(root, app, CONFIG_FILE)) ? CONFIG_FILE : FRAMEWORK_FILE
|
|
50
|
+
const path = join(root, app, config)
|
|
51
|
+
if (!existsSync(path)) return undefined
|
|
52
|
+
const text = readFileSync(path, "utf8")
|
|
53
|
+
const found = ts === undefined ? literalClientModule(text) : staticClientModule(ts, path, text)
|
|
54
|
+
if (found === undefined) return undefined
|
|
55
|
+
const file =
|
|
56
|
+
found.base === "app"
|
|
57
|
+
? resolve(join(root, app, CONFIG_FILE), found.specifier)
|
|
58
|
+
: resolve(path, found.specifier.startsWith("../") ? found.specifier : `./${found.specifier}`)
|
|
59
|
+
if (file === undefined) return undefined
|
|
60
|
+
return {
|
|
61
|
+
config: app === "" ? config : `${app}/${config}`,
|
|
62
|
+
line: text.slice(0, found.index).split("\n").length,
|
|
63
|
+
specifier: found.specifier,
|
|
64
|
+
file,
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface FoundClientModule {
|
|
69
|
+
readonly base: "app" | "config"
|
|
70
|
+
readonly specifier: string
|
|
71
|
+
readonly index: number
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function literalClientModule(text: string): FoundClientModule | undefined {
|
|
75
|
+
// `stripComments` keeps offsets, so the match index is a position in `text`.
|
|
76
|
+
const match = CLIENT_MODULE_EXPORT.exec(stripComments(text))
|
|
77
|
+
if (match === null) return undefined
|
|
78
|
+
return { base: "app", specifier: match[1] ?? match[2] ?? "", index: match.index }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function staticClientModule(
|
|
82
|
+
ts: typeof TS,
|
|
83
|
+
path: string,
|
|
84
|
+
text: string,
|
|
85
|
+
): FoundClientModule | undefined {
|
|
86
|
+
let sf: TS.SourceFile
|
|
87
|
+
try {
|
|
88
|
+
sf = ts.createSourceFile(path, text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
89
|
+
} catch {
|
|
90
|
+
// TypeScript 7 parses only what its session preloaded; the literal form still reads.
|
|
91
|
+
return literalClientModule(text)
|
|
92
|
+
}
|
|
93
|
+
const initializer = exportedInitializer(ts, sf, "clientModule")
|
|
94
|
+
const found = configPathReader(ts, sf)(initializer)
|
|
95
|
+
if (initializer === undefined || found === undefined) return undefined
|
|
96
|
+
return { base: found.base, specifier: found.path, index: initializer.getStart(sf) }
|
|
97
|
+
}
|