@nifrajs/cli 4.0.0 → 5.0.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 +3 -0
- 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 +7 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +13 -3
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +20 -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 +75 -22
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1475 -310
- 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 +192 -65
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +7 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +112 -5
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +18 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +49 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +21 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +138 -38
- 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/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.map +1 -1
- package/dist/fix-recipes.js +28 -3
- 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 +2 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +17 -11
- package/dist/init-agents.js.map +1 -1
- 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 +16 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +396 -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 +2 -0
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +11 -0
- 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 +5 -3
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +6 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +55 -24
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +93 -15
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +31 -8
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +37 -7
- 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 +2 -2
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-exec.d.ts +12 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +50 -13
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +17 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +40 -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-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 +8 -2
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +7 -0
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +958 -71
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +94 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +864 -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/recipes/4.0.0.d.ts.map +1 -1
- package/dist/recipes/4.0.0.js +2 -0
- package/dist/recipes/4.0.0.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/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 +38 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +10 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +10 -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 +7 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +319 -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 +158 -43
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +24 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +221 -3
- 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 +12 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +281 -68
- 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 +192 -40
- 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 +163 -7
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/sync-manifest.d.ts +10 -2
- package/dist/sync-manifest.d.ts.map +1 -1
- package/dist/sync-manifest.js +42 -18
- package/dist/sync-manifest.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +12 -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 +69 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +420 -77
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +11 -0
- 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 +42 -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 +54 -6
- package/docs/llms-full.txt +1539 -138
- package/docs/types.json +1456 -206
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +15 -2
- package/src/capabilities-tool.ts +15 -3
- package/src/check-diagnostics.ts +73 -9
- package/src/check-scan.ts +1773 -381
- package/src/check.ts +237 -76
- package/src/cli.ts +141 -5
- package/src/client-module.ts +61 -0
- package/src/command-catalog.ts +181 -47
- 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/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 +34 -3
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +17 -11
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +424 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +25 -0
- package/src/internal/typescript-import.ts +10 -3
- package/src/introspect.ts +65 -27
- package/src/levels-tool.ts +117 -20
- package/src/load.ts +78 -14
- package/src/mcp-context.ts +7 -7
- package/src/mcp-exec.ts +60 -17
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +41 -0
- package/src/mcp-isolate.ts +9 -2
- 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 +8 -3
- package/src/migrate-layout.ts +1109 -70
- package/src/output-schemas.ts +1084 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/recipes/4.0.0.ts +2 -0
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +39 -10
- package/src/rules/codes.ts +10 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +351 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +184 -51
- package/src/rules/pages.ts +238 -3
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +352 -71
- package/src/rules/zones.ts +205 -38
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +134 -7
- package/src/sync-manifest.ts +48 -18
- package/src/test-tool.ts +18 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +487 -71
- package/src/verification-plan.ts +24 -0
- package/src/version-drift.ts +67 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
package/src/upgrade.ts
CHANGED
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
*
|
|
6
6
|
* 1. **pin sweep** - set every matching dependency to the target version across the workspace's
|
|
7
7
|
* package.json files, preserving the caret/tilde/exact style and skipping `workspace:`/`link:` specs.
|
|
8
|
+
* `overrides`, `resolutions` and `pnpm.overrides` are swept too, since they decide what installs;
|
|
9
|
+
* a version pinned anywhere else (an MCP config, a Dockerfile, another lockfile) is listed only.
|
|
8
10
|
* 2. **dependency moves** - replace removed packages with their supported successor without
|
|
9
11
|
* reserializing package.json or leaving duplicate dependency keys.
|
|
10
12
|
* 3. **import moves** - rewrite exact import specifiers to their updated module paths.
|
|
@@ -22,10 +24,29 @@
|
|
|
22
24
|
* transforms are a future addition - a recipe that needs one is the signal to add the engine, not before.
|
|
23
25
|
*/
|
|
24
26
|
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
+
import {
|
|
28
|
+
existsSync,
|
|
29
|
+
mkdirSync,
|
|
30
|
+
readdirSync,
|
|
31
|
+
readFileSync,
|
|
32
|
+
realpathSync,
|
|
33
|
+
statSync,
|
|
34
|
+
writeFileSync,
|
|
35
|
+
} from "node:fs"
|
|
36
|
+
import { dirname, join, relative, sep } from "node:path"
|
|
37
|
+
import { fileURLToPath } from "node:url"
|
|
38
|
+
import type { ReflectedHookReport } from "@nifrajs/core/reflection"
|
|
27
39
|
import { Glob } from "bun"
|
|
40
|
+
import {
|
|
41
|
+
collectHookReport,
|
|
42
|
+
diffHookReports,
|
|
43
|
+
HOOKS_BEFORE_UPGRADE_FILE,
|
|
44
|
+
type HookReportChange,
|
|
45
|
+
parseHookReport,
|
|
46
|
+
renderHookDiff,
|
|
47
|
+
} from "./hooks-report.ts"
|
|
28
48
|
import { getRecipe, listRecipeVersions, type UpgradeRecipe } from "./recipes/index.ts"
|
|
49
|
+
import { markerTarget } from "./rules/comment-markers.ts"
|
|
29
50
|
|
|
30
51
|
export interface UpgradeOptions {
|
|
31
52
|
/** Target version, e.g. "1.8.0". Required unless `list` is set. */
|
|
@@ -44,6 +65,11 @@ export interface UpgradeOptions {
|
|
|
44
65
|
readonly exact?: boolean
|
|
45
66
|
/** The running CLI's version: the newest target it applies. Default: its newest recipe. */
|
|
46
67
|
readonly cliVersion?: string
|
|
68
|
+
/**
|
|
69
|
+
* With `--write`, capture `nifra routes --hooks` before and after the edits and print the diff, so a
|
|
70
|
+
* change in which hooks run where is visible. Loads the backend. Default true; `--no-hooks` skips it.
|
|
71
|
+
*/
|
|
72
|
+
readonly hooks?: boolean
|
|
47
73
|
}
|
|
48
74
|
|
|
49
75
|
export interface PinChange {
|
|
@@ -71,6 +97,16 @@ export interface DependencyMoveChange {
|
|
|
71
97
|
readonly action: "renamed" | "removed"
|
|
72
98
|
}
|
|
73
99
|
|
|
100
|
+
/** A retired comment in the project, and the line it applied to before the release retired it. */
|
|
101
|
+
export interface RetiredMarkerSite {
|
|
102
|
+
readonly file: string
|
|
103
|
+
readonly line: number
|
|
104
|
+
readonly marker: string
|
|
105
|
+
/** The line the comment covered; `undefined` when a blank line follows its comment block. */
|
|
106
|
+
readonly covers: { readonly line: number; readonly text: string } | undefined
|
|
107
|
+
readonly instead: string
|
|
108
|
+
}
|
|
109
|
+
|
|
74
110
|
export interface UpgradePlan {
|
|
75
111
|
readonly version: string
|
|
76
112
|
readonly pins: readonly PinChange[]
|
|
@@ -78,6 +114,12 @@ export interface UpgradePlan {
|
|
|
78
114
|
readonly importMoves: readonly ImportChange[]
|
|
79
115
|
/** Pins refused because the target is BELOW the installed version (a rollback). Empty when allowed. */
|
|
80
116
|
readonly downgrades: readonly PinChange[]
|
|
117
|
+
/** Comments a release in the chain stopped honoring. Listed only, never rewritten. */
|
|
118
|
+
readonly retiredMarkers: readonly RetiredMarkerSite[]
|
|
119
|
+
/** Installed group packages linked from outside the workspace root, at another version. Never edited. */
|
|
120
|
+
readonly linkedOutside: readonly LinkedGroupInstall[]
|
|
121
|
+
/** Other versions pinned outside package.json. Never edited. */
|
|
122
|
+
readonly strayPins: readonly StrayPin[]
|
|
81
123
|
readonly notes: readonly string[]
|
|
82
124
|
}
|
|
83
125
|
|
|
@@ -203,63 +245,153 @@ export function pinSweepText(
|
|
|
203
245
|
let out = text
|
|
204
246
|
const changes: Array<Omit<PinChange, "file">> = []
|
|
205
247
|
const downgrades: Array<Omit<PinChange, "file">> = []
|
|
248
|
+
for (const { path, key, target: name, spec: rawSpec } of pinEntries(parsed)) {
|
|
249
|
+
const field = path.join(".")
|
|
250
|
+
const rule = rules.find(
|
|
251
|
+
(r) => name === r.match || (r.match.endsWith("/") && name.startsWith(r.match)),
|
|
252
|
+
)
|
|
253
|
+
if (!rule) continue
|
|
254
|
+
const next = rewriteVersionSpec(rawSpec, rule.to, exact)
|
|
255
|
+
if (next === null) continue
|
|
256
|
+
// Refuse a rollback: `upgrade <version>` pins an exact target, so an OLD target on a newer install
|
|
257
|
+
// walks dependencies backward (e.g. ^2.3.0 → ^2.0.0) and can break a shared-package peer range.
|
|
258
|
+
const current = specVersionTuple(rawSpec)
|
|
259
|
+
const target = specVersionTuple(rule.to)
|
|
260
|
+
if (
|
|
261
|
+
!allowDowngrade &&
|
|
262
|
+
current !== null &&
|
|
263
|
+
target !== null &&
|
|
264
|
+
compareSemverSpec(rawSpec, rule.to) > 0
|
|
265
|
+
) {
|
|
266
|
+
downgrades.push({ field, name: key, from: rawSpec, to: next })
|
|
267
|
+
continue
|
|
268
|
+
}
|
|
269
|
+
// Replace exactly `"key"<ws>:<ws>"spec"` inside this field's object, preserving the whitespace. A
|
|
270
|
+
// nested `overrides` entry repeated with the same spec maps the same way, so every one is replaced.
|
|
271
|
+
const range = objectRangeAt(out, path)
|
|
272
|
+
if (range === undefined) continue
|
|
273
|
+
const pattern = new RegExp(`("${escapeRegExp(key)}"\\s*:\\s*")${escapeRegExp(rawSpec)}(")`, "g")
|
|
274
|
+
const body = out.slice(range.start, range.end)
|
|
275
|
+
const replaced = body.replace(pattern, `$1${next}$2`)
|
|
276
|
+
if (replaced !== body) {
|
|
277
|
+
out = out.slice(0, range.start) + replaced + out.slice(range.end)
|
|
278
|
+
changes.push({ field, name: key, from: rawSpec, to: next })
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return { text: out, changes, downgrades }
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Where a package manager forces one version over every range: npm and Bun `overrides` (nested by
|
|
285
|
+
* parent), Yarn and Bun `resolutions`, pnpm `pnpm.overrides`. */
|
|
286
|
+
const OVERRIDE_FIELDS: readonly (readonly string[])[] = [
|
|
287
|
+
["overrides"],
|
|
288
|
+
["resolutions"],
|
|
289
|
+
["pnpm", "overrides"],
|
|
290
|
+
]
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* The package an override key pins: `@nifrajs/core`, `parent/@nifrajs/core` and the same behind a `**`
|
|
294
|
+
* glob (resolutions), `parent>@nifrajs/core` and `@nifrajs/core@<4` (pnpm).
|
|
295
|
+
*/
|
|
296
|
+
function overrideTarget(key: string): string {
|
|
297
|
+
const last = key.slice(key.lastIndexOf(">") + 1)
|
|
298
|
+
return /(?:^|\/)((?:@[^/@]+\/)?[^/@]+)(?:@[^/]*)?$/.exec(last)?.[1] ?? last
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Every string version spec a package.json declares for a package, with the field path it sits in. */
|
|
302
|
+
function pinEntries(
|
|
303
|
+
parsed: Record<string, unknown>,
|
|
304
|
+
): Array<{ path: readonly string[]; key: string; target: string; spec: string }> {
|
|
305
|
+
const entries: Array<{ path: readonly string[]; key: string; target: string; spec: string }> = []
|
|
206
306
|
for (const field of DEP_FIELDS) {
|
|
207
307
|
const deps = parsed[field]
|
|
208
308
|
if (typeof deps !== "object" || deps === null) continue
|
|
209
|
-
for (const [name,
|
|
210
|
-
if (typeof
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
target !== null &&
|
|
225
|
-
compareSemverSpec(rawSpec, rule.to) > 0
|
|
226
|
-
) {
|
|
227
|
-
downgrades.push({ field, name, from: rawSpec, to: next })
|
|
228
|
-
continue
|
|
229
|
-
}
|
|
230
|
-
// Replace exactly `"name"<ws>:<ws>"spec"`, preserving the surrounding whitespace.
|
|
231
|
-
const pattern = new RegExp(`("${escapeRegExp(name)}"\\s*:\\s*")${escapeRegExp(rawSpec)}(")`)
|
|
232
|
-
const replaced = out.replace(pattern, `$1${next}$2`)
|
|
233
|
-
if (replaced !== out) {
|
|
234
|
-
out = replaced
|
|
235
|
-
changes.push({ field, name, from: rawSpec, to: next })
|
|
309
|
+
for (const [name, spec] of Object.entries(deps))
|
|
310
|
+
if (typeof spec === "string") entries.push({ path: [field], key: name, target: name, spec })
|
|
311
|
+
}
|
|
312
|
+
for (const path of OVERRIDE_FIELDS) {
|
|
313
|
+
let value: unknown = parsed
|
|
314
|
+
for (const key of path)
|
|
315
|
+
value = typeof value === "object" && value !== null ? Reflect.get(value, key) : undefined
|
|
316
|
+
// npm nests an override under the package whose dependency it pins; `.` pins that package itself.
|
|
317
|
+
const walk = (object: object, parent: string | undefined): void => {
|
|
318
|
+
for (const [key, spec] of Object.entries(object)) {
|
|
319
|
+
const target = key === "." ? parent : overrideTarget(key)
|
|
320
|
+
if (typeof spec === "string" && target !== undefined)
|
|
321
|
+
entries.push({ path, key, target, spec })
|
|
322
|
+
else if (typeof spec === "object" && spec !== null && !Array.isArray(spec))
|
|
323
|
+
walk(spec, target)
|
|
236
324
|
}
|
|
237
325
|
}
|
|
326
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value)) walk(value, undefined)
|
|
238
327
|
}
|
|
239
|
-
return
|
|
328
|
+
return entries
|
|
240
329
|
}
|
|
241
330
|
|
|
242
|
-
|
|
331
|
+
/** The body (between the braces) of the object at `path` in a JSON text: top-level key first, then
|
|
332
|
+
* each nested one, never a same-named key deeper in the document. */
|
|
333
|
+
function objectRangeAt(
|
|
243
334
|
text: string,
|
|
244
|
-
|
|
335
|
+
path: readonly string[],
|
|
245
336
|
): { start: number; end: number } | undefined {
|
|
246
|
-
const
|
|
247
|
-
if (
|
|
248
|
-
|
|
337
|
+
const rootOpen = text.indexOf("{")
|
|
338
|
+
if (rootOpen === -1) return undefined
|
|
339
|
+
let range: { start: number; end: number } | undefined = {
|
|
340
|
+
start: rootOpen + 1,
|
|
341
|
+
end: closingBrace(text, rootOpen),
|
|
342
|
+
}
|
|
343
|
+
for (const key of path) {
|
|
344
|
+
if (range === undefined) return undefined
|
|
345
|
+
range = memberObjectRange(text, range, key)
|
|
346
|
+
}
|
|
347
|
+
return range
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** The index of the `"` that closes the string opening at `open`. */
|
|
351
|
+
function stringEnd(text: string, open: number): number {
|
|
352
|
+
for (let index = open + 1; index < text.length; index++) {
|
|
353
|
+
if (text[index] === "\\") index++
|
|
354
|
+
else if (text[index] === '"') return index
|
|
355
|
+
}
|
|
356
|
+
return text.length
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** The index of the brace that closes the one at `open`. */
|
|
360
|
+
function closingBrace(text: string, open: number): number {
|
|
249
361
|
let depth = 0
|
|
250
|
-
let quoted = false
|
|
251
|
-
let escaped = false
|
|
252
362
|
for (let index = open; index < text.length; index++) {
|
|
253
363
|
const char = text[index]
|
|
254
|
-
if (
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
364
|
+
if (char === '"') index = stringEnd(text, index)
|
|
365
|
+
else if (char === "{" || char === "[") depth++
|
|
366
|
+
else if ((char === "}" || char === "]") && --depth === 0) return index
|
|
367
|
+
}
|
|
368
|
+
return text.length
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/** The body of `key`'s object value among the direct members of the object body `within`. */
|
|
372
|
+
function memberObjectRange(
|
|
373
|
+
text: string,
|
|
374
|
+
within: { start: number; end: number },
|
|
375
|
+
key: string,
|
|
376
|
+
): { start: number; end: number } | undefined {
|
|
377
|
+
let depth = 0
|
|
378
|
+
for (let index = within.start; index < within.end; index++) {
|
|
379
|
+
const char = text[index]
|
|
380
|
+
if (char === "{" || char === "[") depth++
|
|
381
|
+
else if (char === "}" || char === "]") depth--
|
|
382
|
+
else if (char === '"') {
|
|
383
|
+
const close = stringEnd(text, index)
|
|
384
|
+
let after = close + 1
|
|
385
|
+
while (/\s/.test(text[after] ?? "")) after++
|
|
386
|
+
if (depth === 0 && text[after] === ":" && text.slice(index + 1, close) === key) {
|
|
387
|
+
after++
|
|
388
|
+
while (/\s/.test(text[after] ?? "")) after++
|
|
389
|
+
return text[after] === "{"
|
|
390
|
+
? { start: after + 1, end: closingBrace(text, after) }
|
|
391
|
+
: undefined
|
|
392
|
+
}
|
|
393
|
+
index = close
|
|
259
394
|
}
|
|
260
|
-
if (char === '"') quoted = true
|
|
261
|
-
else if (char === "{") depth++
|
|
262
|
-
else if (char === "}" && --depth === 0) return { start: open + 1, end: index }
|
|
263
395
|
}
|
|
264
396
|
return undefined
|
|
265
397
|
}
|
|
@@ -272,7 +404,7 @@ function rewriteDependencyEntry(
|
|
|
272
404
|
to: string | undefined,
|
|
273
405
|
toVersion: string | undefined,
|
|
274
406
|
): string {
|
|
275
|
-
const range =
|
|
407
|
+
const range = objectRangeAt(text, [field])
|
|
276
408
|
if (range === undefined) return text
|
|
277
409
|
const body = text.slice(range.start, range.end)
|
|
278
410
|
const key = escapeRegExp(from)
|
|
@@ -396,11 +528,13 @@ const isIgnored = (path: string): boolean =>
|
|
|
396
528
|
.slice(0, -1)
|
|
397
529
|
.some((segment) => IGNORED_DIRECTORIES.has(segment))
|
|
398
530
|
|
|
399
|
-
function scan(cwd: string, pattern: string): string[] {
|
|
531
|
+
function scan(cwd: string, pattern: string, dot = false): string[] {
|
|
400
532
|
const glob = new Glob(pattern)
|
|
401
533
|
const out: string[] = []
|
|
402
|
-
|
|
403
|
-
|
|
534
|
+
// The write boundary: a symlinked file or directory (a linked repository) is never yielded, so the
|
|
535
|
+
// upgrade can only edit files that live under `cwd`.
|
|
536
|
+
for (const path of glob.scanSync({ cwd, dot, followSymlinks: false })) {
|
|
537
|
+
if (!isIgnored(path)) out.push(path.replaceAll("\\", "/"))
|
|
404
538
|
}
|
|
405
539
|
return out.sort() // deterministic order
|
|
406
540
|
}
|
|
@@ -416,11 +550,12 @@ const groupPins = (version: string): UpgradeRecipe["pins"] => [
|
|
|
416
550
|
]
|
|
417
551
|
|
|
418
552
|
/**
|
|
419
|
-
*
|
|
420
|
-
*
|
|
553
|
+
* Every fixed-group version the package.json files under `cwd` declare with a plain semver spec, each
|
|
554
|
+
* with the files that declare it. An override counts: it decides what installs. `workspace:`, `link:`
|
|
555
|
+
* and other non-semver specs are not versions.
|
|
421
556
|
*/
|
|
422
|
-
export function
|
|
423
|
-
|
|
557
|
+
export function groupPinVersions(cwd: string): ReadonlyMap<string, readonly string[]> {
|
|
558
|
+
const versions = new Map<string, string[]>()
|
|
424
559
|
for (const rel of scan(cwd, "**/package.json")) {
|
|
425
560
|
let parsed: Record<string, unknown>
|
|
426
561
|
try {
|
|
@@ -428,18 +563,144 @@ export function installedGroupVersion(cwd: string): string | null {
|
|
|
428
563
|
} catch {
|
|
429
564
|
continue
|
|
430
565
|
}
|
|
431
|
-
for (const
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
lowest = version
|
|
439
|
-
}
|
|
440
|
-
}
|
|
566
|
+
for (const { target, spec } of pinEntries(parsed)) {
|
|
567
|
+
if (!isFixedGroup(target)) continue
|
|
568
|
+
const version = specVersion(spec)
|
|
569
|
+
if (version === null) continue
|
|
570
|
+
const files = versions.get(version) ?? []
|
|
571
|
+
if (!files.includes(rel)) files.push(rel)
|
|
572
|
+
versions.set(version, files)
|
|
441
573
|
}
|
|
442
574
|
}
|
|
575
|
+
return versions
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/** A fixed-group version spelled outside package.json, which the upgrade lists and never edits. */
|
|
579
|
+
export interface StrayPin {
|
|
580
|
+
readonly file: string
|
|
581
|
+
readonly line: number
|
|
582
|
+
/** As written: `@nifrajs/cli@3.1.0`. */
|
|
583
|
+
readonly pin: string
|
|
584
|
+
/** A lockfile other than the workspace's own: regenerate it. Listed once, with its other stale pins
|
|
585
|
+
* counted in `more`. */
|
|
586
|
+
readonly lockfile: boolean
|
|
587
|
+
readonly more: number
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const STRAY_PIN_FILES = [
|
|
591
|
+
"**/{.mcp.json,mcp.json}",
|
|
592
|
+
"**/{Dockerfile,Dockerfile.*,*.Dockerfile,*.dockerfile,Containerfile}",
|
|
593
|
+
"**/{docker-compose,compose}*.{yml,yaml}",
|
|
594
|
+
".github/workflows/*.{yml,yaml}",
|
|
595
|
+
".gitlab-ci.yml",
|
|
596
|
+
"**/*.lock",
|
|
597
|
+
"**/{package-lock.json,pnpm-lock.yaml}",
|
|
598
|
+
]
|
|
599
|
+
const LOCKFILE = /(?:\.lock|package-lock\.json|pnpm-lock\.yaml)$/
|
|
600
|
+
const GROUP_PIN =
|
|
601
|
+
/(?<![\w.@/-])(@nifrajs\/[a-z0-9._-]+|create-nifra|nifra)@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)/g
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Fixed-group versions other than `version` pinned where `nifra upgrade` does not edit: MCP configs,
|
|
605
|
+
* container files, CI workflows, and lockfiles besides the workspace's own (which install rewrites).
|
|
606
|
+
*/
|
|
607
|
+
export function strayPins(cwd: string, version: string): StrayPin[] {
|
|
608
|
+
const found: StrayPin[] = []
|
|
609
|
+
const files = new Set(STRAY_PIN_FILES.flatMap((pattern) => scan(cwd, pattern, true)))
|
|
610
|
+
for (const rel of [...files].sort()) {
|
|
611
|
+
const lockfile = LOCKFILE.test(rel)
|
|
612
|
+
if (lockfile && !/[\\/]/.test(rel)) continue
|
|
613
|
+
let text: string
|
|
614
|
+
try {
|
|
615
|
+
if (statSync(join(cwd, rel)).size > 8_000_000) continue
|
|
616
|
+
text = readFileSync(join(cwd, rel), "utf8")
|
|
617
|
+
} catch {
|
|
618
|
+
continue
|
|
619
|
+
}
|
|
620
|
+
const stale = [...text.matchAll(GROUP_PIN)].filter(
|
|
621
|
+
(match) => compareSemverSpec(match[2] ?? "", version) !== 0,
|
|
622
|
+
)
|
|
623
|
+
const site = (match: RegExpMatchArray, more: number): StrayPin => ({
|
|
624
|
+
file: rel,
|
|
625
|
+
line: text.slice(0, match.index ?? 0).split("\n").length,
|
|
626
|
+
pin: match[0],
|
|
627
|
+
lockfile,
|
|
628
|
+
more,
|
|
629
|
+
})
|
|
630
|
+
const first = stale[0]
|
|
631
|
+
if (first === undefined) continue
|
|
632
|
+
if (lockfile) found.push(site(first, stale.length - 1))
|
|
633
|
+
else for (const match of stale) found.push(site(match, 0))
|
|
634
|
+
}
|
|
635
|
+
return found
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** An installed fixed-group package, followed to its real location. */
|
|
639
|
+
export interface LinkedGroupInstall {
|
|
640
|
+
readonly package: string
|
|
641
|
+
readonly path: string
|
|
642
|
+
readonly version: string
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
const packageVersionAt = (dir: string): string | undefined => {
|
|
646
|
+
try {
|
|
647
|
+
const meta: unknown = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"))
|
|
648
|
+
const version = Reflect.get(Object(meta), "version")
|
|
649
|
+
return typeof version === "string" && version.length > 0 ? version : undefined
|
|
650
|
+
} catch {
|
|
651
|
+
return undefined
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Every fixed-group package installed in `cwd/node_modules`, realpath-resolved so a linked repository
|
|
657
|
+
* reports its own version. `inside` is false for one that resolves outside the workspace root.
|
|
658
|
+
*/
|
|
659
|
+
export function installedGroupPackages(
|
|
660
|
+
cwd: string,
|
|
661
|
+
): readonly (LinkedGroupInstall & { readonly inside: boolean })[] {
|
|
662
|
+
const modules = join(cwd, "node_modules")
|
|
663
|
+
const names = ["nifra", "create-nifra"].filter((name) => existsSync(join(modules, name)))
|
|
664
|
+
try {
|
|
665
|
+
for (const name of readdirSync(join(modules, "@nifrajs"))) names.push(`@nifrajs/${name}`)
|
|
666
|
+
} catch {
|
|
667
|
+
// No scoped installs.
|
|
668
|
+
}
|
|
669
|
+
let root: string
|
|
670
|
+
try {
|
|
671
|
+
root = realpathSync(cwd)
|
|
672
|
+
} catch {
|
|
673
|
+
root = cwd
|
|
674
|
+
}
|
|
675
|
+
const found: (LinkedGroupInstall & { inside: boolean })[] = []
|
|
676
|
+
for (const name of names) {
|
|
677
|
+
let path: string
|
|
678
|
+
try {
|
|
679
|
+
path = realpathSync(join(modules, name))
|
|
680
|
+
} catch {
|
|
681
|
+
continue
|
|
682
|
+
}
|
|
683
|
+
const version = packageVersionAt(path)
|
|
684
|
+
if (version === undefined) continue
|
|
685
|
+
found.push({
|
|
686
|
+
package: name,
|
|
687
|
+
path,
|
|
688
|
+
version,
|
|
689
|
+
inside: path === root || path.startsWith(root + sep),
|
|
690
|
+
})
|
|
691
|
+
}
|
|
692
|
+
return found
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* The lowest fixed-group version any package.json under `cwd` declares with a plain semver spec, or
|
|
697
|
+
* null when none does. Recipes newer than it still have edits to make somewhere in the workspace.
|
|
698
|
+
*/
|
|
699
|
+
export function installedGroupVersion(cwd: string): string | null {
|
|
700
|
+
let lowest: string | null = null
|
|
701
|
+
for (const version of groupPinVersions(cwd).keys()) {
|
|
702
|
+
if (lowest === null || compareSemverSpec(version, lowest) < 0) lowest = version
|
|
703
|
+
}
|
|
443
704
|
return lowest
|
|
444
705
|
}
|
|
445
706
|
|
|
@@ -475,6 +736,7 @@ export function chainRecipes(target: string, installed: string | null): ChainedR
|
|
|
475
736
|
pins: [...pins].map(([match, to]) => ({ match, to })),
|
|
476
737
|
dependencyMoves: recipes.flatMap((recipe) => recipe.dependencyMoves ?? []),
|
|
477
738
|
importMoves: recipes.flatMap((recipe) => recipe.importMoves),
|
|
739
|
+
retiredMarkers: recipes.flatMap((recipe) => recipe.retiredMarkers ?? []),
|
|
478
740
|
notes: recipes.flatMap((recipe) =>
|
|
479
741
|
(recipe.notes ?? []).map((note) => `${recipe.version}: ${note}`),
|
|
480
742
|
),
|
|
@@ -493,6 +755,10 @@ export function computeUpgrade(
|
|
|
493
755
|
const dependencyMoves: DependencyMoveChange[] = []
|
|
494
756
|
const importMoves: ImportChange[] = []
|
|
495
757
|
const downgrades: PinChange[] = []
|
|
758
|
+
const retiredMarkers: RetiredMarkerSite[] = []
|
|
759
|
+
const linkedOutside = installedGroupPackages(cwd)
|
|
760
|
+
.filter((entry) => !entry.inside && entry.version !== recipe.version)
|
|
761
|
+
.map(({ package: name, path, version }) => ({ package: name, path, version }))
|
|
496
762
|
|
|
497
763
|
if ((recipe.dependencyMoves?.length ?? 0) > 0 || recipe.pins.length > 0) {
|
|
498
764
|
for (const rel of scan(cwd, "**/package.json")) {
|
|
@@ -518,12 +784,36 @@ export function computeUpgrade(
|
|
|
518
784
|
}
|
|
519
785
|
}
|
|
520
786
|
|
|
787
|
+
// Read after the import moves, so a listed line number is the one the file has after `--write`.
|
|
788
|
+
const retired = recipe.retiredMarkers ?? []
|
|
789
|
+
if (retired.length > 0) {
|
|
790
|
+
for (const rel of scan(cwd, SOURCE_GLOB)) {
|
|
791
|
+
const text = readFileSync(join(cwd, rel), "utf8")
|
|
792
|
+
if (!retired.some(({ marker }) => text.includes(marker))) continue
|
|
793
|
+
const lines = text.split("\n")
|
|
794
|
+
for (let line = 1; line <= lines.length; line++) {
|
|
795
|
+
for (const { marker, instead } of retired) {
|
|
796
|
+
if (!(lines[line - 1] ?? "").includes(marker)) continue
|
|
797
|
+
const target = markerTarget(lines, line)
|
|
798
|
+
const covers =
|
|
799
|
+
target === undefined
|
|
800
|
+
? undefined
|
|
801
|
+
: { line: target, text: (lines[target - 1] ?? "").trim() }
|
|
802
|
+
retiredMarkers.push({ file: rel, line, marker, covers, instead })
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
521
808
|
return {
|
|
522
809
|
version: recipe.version,
|
|
523
810
|
pins,
|
|
524
811
|
dependencyMoves,
|
|
525
812
|
importMoves,
|
|
526
813
|
downgrades,
|
|
814
|
+
retiredMarkers,
|
|
815
|
+
linkedOutside,
|
|
816
|
+
strayPins: strayPins(cwd, recipe.version),
|
|
527
817
|
notes: recipe.notes ?? [],
|
|
528
818
|
}
|
|
529
819
|
}
|
|
@@ -548,7 +838,10 @@ function renderPlan(
|
|
|
548
838
|
if (
|
|
549
839
|
plan.pins.length === 0 &&
|
|
550
840
|
plan.dependencyMoves.length === 0 &&
|
|
551
|
-
plan.importMoves.length === 0
|
|
841
|
+
plan.importMoves.length === 0 &&
|
|
842
|
+
plan.retiredMarkers.length === 0 &&
|
|
843
|
+
plan.linkedOutside.length === 0 &&
|
|
844
|
+
plan.strayPins.length === 0
|
|
552
845
|
) {
|
|
553
846
|
lines.push("Already up to date - no changes.")
|
|
554
847
|
return lines.join("\n")
|
|
@@ -575,6 +868,45 @@ function renderPlan(
|
|
|
575
868
|
for (const m of plan.importMoves) lines.push(` ${m.file} ${m.from} → ${m.to} (${m.count})`)
|
|
576
869
|
lines.push("")
|
|
577
870
|
}
|
|
871
|
+
if (plan.retiredMarkers.length > 0) {
|
|
872
|
+
lines.push(
|
|
873
|
+
`${plan.retiredMarkers.length} retired comment(s) that silence nothing now; nifra check reports what each covered:`,
|
|
874
|
+
)
|
|
875
|
+
for (const site of plan.retiredMarkers) {
|
|
876
|
+
const covers =
|
|
877
|
+
site.covers === undefined
|
|
878
|
+
? "covers no line"
|
|
879
|
+
: `covers line ${site.covers.line}: ${site.covers.text}`
|
|
880
|
+
lines.push(` ${site.file}:${site.line} ${site.marker} ${covers}`)
|
|
881
|
+
lines.push(` write instead: ${site.instead}`)
|
|
882
|
+
}
|
|
883
|
+
lines.push("")
|
|
884
|
+
}
|
|
885
|
+
if (plan.linkedOutside.length > 0) {
|
|
886
|
+
lines.push(
|
|
887
|
+
"Left alone, linked from outside the workspace root (upgrade that repository on its own):",
|
|
888
|
+
)
|
|
889
|
+
for (const entry of plan.linkedOutside)
|
|
890
|
+
lines.push(` ${entry.package} ${entry.version} at ${entry.path}`)
|
|
891
|
+
lines.push("")
|
|
892
|
+
}
|
|
893
|
+
const pinned = plan.strayPins.filter((site) => !site.lockfile)
|
|
894
|
+
if (pinned.length > 0) {
|
|
895
|
+
lines.push(`Left alone, another version pinned outside package.json (update by hand):`)
|
|
896
|
+
for (const site of pinned) lines.push(` ${site.file}:${site.line} ${site.pin}`)
|
|
897
|
+
lines.push("")
|
|
898
|
+
}
|
|
899
|
+
const locks = plan.strayPins.filter((site) => site.lockfile)
|
|
900
|
+
if (locks.length > 0) {
|
|
901
|
+
lines.push(
|
|
902
|
+
`Left alone, a lockfile besides the workspace's own (regenerate it after installing):`,
|
|
903
|
+
)
|
|
904
|
+
for (const site of locks) {
|
|
905
|
+
const more = site.more === 0 ? "" : ` and ${site.more} more`
|
|
906
|
+
lines.push(` ${site.file}:${site.line} ${site.pin}${more}`)
|
|
907
|
+
}
|
|
908
|
+
lines.push("")
|
|
909
|
+
}
|
|
578
910
|
for (const note of plan.notes) lines.push(`note: ${note}`)
|
|
579
911
|
if (!write) lines.push("\nRe-run with --write to apply, then nifra check verifies.")
|
|
580
912
|
return lines.join("\n").trimEnd()
|
|
@@ -593,6 +925,74 @@ function renderDowngradeRefusal(version: string, downgrades: readonly PinChange[
|
|
|
593
925
|
].join("\n")
|
|
594
926
|
}
|
|
595
927
|
|
|
928
|
+
/** The hook report around `--write`: the diff and where the pre-upgrade report was saved, or why not. */
|
|
929
|
+
export type UpgradeHookScope =
|
|
930
|
+
| { readonly saved: string; readonly changes: readonly HookReportChange[] }
|
|
931
|
+
| { readonly error: string; readonly saved?: string }
|
|
932
|
+
|
|
933
|
+
async function hookReportBefore(cwd: string): Promise<ReflectedHookReport | { error: string }> {
|
|
934
|
+
try {
|
|
935
|
+
const { loadBackendApp } = await import("./load.ts")
|
|
936
|
+
const { backend } = await loadBackendApp(cwd)
|
|
937
|
+
return await collectHookReport(backend)
|
|
938
|
+
} catch (error) {
|
|
939
|
+
return { error: error instanceof Error ? error.message : String(error) }
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
/** The report after the edits, from a fresh process: the edited sources must not come from this one's
|
|
944
|
+
* module cache. Saves the pre-upgrade report for `nifra routes --hooks --since` after an install. */
|
|
945
|
+
async function hookScopeAcross(
|
|
946
|
+
cwd: string,
|
|
947
|
+
before: ReflectedHookReport | { error: string },
|
|
948
|
+
): Promise<UpgradeHookScope> {
|
|
949
|
+
if ("error" in before) return { error: before.error }
|
|
950
|
+
const saved = join(cwd, HOOKS_BEFORE_UPGRADE_FILE)
|
|
951
|
+
mkdirSync(dirname(saved), { recursive: true })
|
|
952
|
+
writeFileSync(saved, `${JSON.stringify(before, null, 2)}\n`)
|
|
953
|
+
const entry = fileURLToPath(
|
|
954
|
+
new URL(import.meta.url.endsWith(".ts") ? "./cli.ts" : "./cli.js", import.meta.url),
|
|
955
|
+
)
|
|
956
|
+
const child = Bun.spawn([process.execPath, entry, "routes", "--hooks", "--json"], {
|
|
957
|
+
cwd,
|
|
958
|
+
env: process.env,
|
|
959
|
+
stdout: "pipe",
|
|
960
|
+
stderr: "pipe",
|
|
961
|
+
})
|
|
962
|
+
const [text, , code] = await Promise.all([
|
|
963
|
+
new Response(child.stdout).text(),
|
|
964
|
+
new Response(child.stderr).text(),
|
|
965
|
+
child.exited,
|
|
966
|
+
])
|
|
967
|
+
const after = parseHookReport(text)
|
|
968
|
+
if (after === undefined) {
|
|
969
|
+
return {
|
|
970
|
+
error: `\`nifra routes --hooks\` exited ${code} after the edits`,
|
|
971
|
+
saved: HOOKS_BEFORE_UPGRADE_FILE,
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return { saved: HOOKS_BEFORE_UPGRADE_FILE, changes: diffHookReports(before, after) }
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function renderHookScope(scope: UpgradeHookScope): string {
|
|
978
|
+
const since = (saved: string): string[] => [
|
|
979
|
+
"The new release's hooks run once its packages are installed. After installing, compare against the report saved before the edits:",
|
|
980
|
+
` nifra routes --hooks --since ${saved}`,
|
|
981
|
+
]
|
|
982
|
+
if ("error" in scope) {
|
|
983
|
+
const head = `Hook scope: not captured (${scope.error}).`
|
|
984
|
+
return scope.saved === undefined ? head : [head, ...since(scope.saved)].join("\n")
|
|
985
|
+
}
|
|
986
|
+
return [
|
|
987
|
+
"Hook scope across the edits (nifra routes --hooks):",
|
|
988
|
+
renderHookDiff(scope.changes)
|
|
989
|
+
.split("\n")
|
|
990
|
+
.map((line) => ` ${line}`)
|
|
991
|
+
.join("\n"),
|
|
992
|
+
...since(scope.saved),
|
|
993
|
+
].join("\n")
|
|
994
|
+
}
|
|
995
|
+
|
|
596
996
|
/** The targets this CLI applies: each release recipe up to its own version, and that version. */
|
|
597
997
|
export function upgradeTargets(cliVersion: string): string[] {
|
|
598
998
|
const versions = listRecipeVersions().filter(
|
|
@@ -609,6 +1009,7 @@ function newerCliCommand(version: string, options: UpgradeOptions): string {
|
|
|
609
1009
|
options.exact === true ? "--exact" : "",
|
|
610
1010
|
options.allowDowngrade === true ? "--allow-downgrade" : "",
|
|
611
1011
|
options.verify === false ? "--no-verify" : "",
|
|
1012
|
+
options.hooks === false ? "--no-hooks" : "",
|
|
612
1013
|
options.json === true ? "--json" : "",
|
|
613
1014
|
].filter((flag) => flag !== "")
|
|
614
1015
|
return ["bunx", `@nifrajs/cli@${version}`, "upgrade", version, ...flags].join(" ")
|
|
@@ -697,21 +1098,36 @@ export async function runUpgrade(cwd: string, options: UpgradeOptions): Promise<
|
|
|
697
1098
|
return false
|
|
698
1099
|
}
|
|
699
1100
|
|
|
1101
|
+
const captureHooks = write && options.hooks !== false
|
|
1102
|
+
const before = captureHooks ? await hookReportBefore(cwd) : undefined
|
|
700
1103
|
const plan = write ? computeUpgrade(cwd, recipe, true, allowDowngrade, exact) : preview
|
|
1104
|
+
const hooks = before === undefined ? undefined : await hookScopeAcross(cwd, before)
|
|
701
1105
|
|
|
1106
|
+
// Verify only makes sense once edits are on disk. Default on after --write; opt out with --no-verify.
|
|
1107
|
+
const verify = write && options.verify !== false
|
|
702
1108
|
if (options.json) {
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1109
|
+
// The check result rides inside the one JSON document; its human report would break the stream.
|
|
1110
|
+
const report: Record<string, unknown> = { ...plan, from, steps: recipe.steps, write }
|
|
1111
|
+
if (hooks !== undefined) report.hooks = hooks
|
|
1112
|
+
let ok = true
|
|
1113
|
+
if (verify) {
|
|
1114
|
+
const { collectProjectVerification } = await import("./verification.ts")
|
|
1115
|
+
const verification = await collectProjectVerification(cwd, { lintsOnly: false })
|
|
1116
|
+
const check = await verification.check()
|
|
1117
|
+
report.check = check
|
|
1118
|
+
ok = check.ok
|
|
1119
|
+
}
|
|
1120
|
+
console.log(JSON.stringify(report, null, 2))
|
|
1121
|
+
return ok
|
|
706
1122
|
}
|
|
1123
|
+
console.log(renderPlan(plan, write, from, recipe.steps))
|
|
1124
|
+
if (hooks !== undefined) console.log(`\n${renderHookScope(hooks)}`)
|
|
707
1125
|
|
|
708
|
-
|
|
709
|
-
if (write && options.verify !== false) {
|
|
1126
|
+
if (verify) {
|
|
710
1127
|
const { runCheck } = await import("./check.ts")
|
|
711
1128
|
const ok = await runCheck(cwd, { json: false })
|
|
712
1129
|
if (!ok) {
|
|
713
|
-
|
|
714
|
-
console.error("\n[nifra] upgrade applied but `nifra check` failed - review above.")
|
|
1130
|
+
console.error("\n[nifra] upgrade applied but `nifra check` failed - review above.")
|
|
715
1131
|
return false
|
|
716
1132
|
}
|
|
717
1133
|
}
|