@nifrajs/cli 2.8.0 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/check.d.ts +3 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +17 -7
- package/dist/check.js.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +4 -1
- package/dist/doctor.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +34 -1
- package/dist/scaffold.js.map +1 -1
- package/docs/examples.json +3 -3
- package/docs/llms-full.txt +191 -39
- package/docs/types.json +2 -2
- package/package.json +9 -9
- package/src/check.ts +18 -7
- package/src/cli.ts +1 -1
- package/src/doctor.ts +19 -6
- package/src/mcp-http.ts +1 -1
- package/src/scaffold.ts +34 -1
package/src/check.ts
CHANGED
|
@@ -45,10 +45,15 @@ const FETCH_CALL = /(?<![.\w])fetch\s*\(/g
|
|
|
45
45
|
const HTTP_VERBS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"])
|
|
46
46
|
const SIMPLE_REWRITE_METHODS = new Set(["GET", "DELETE", "HEAD", "OPTIONS"])
|
|
47
47
|
|
|
48
|
-
// Don't scan deps, build output, generated client entries
|
|
49
|
-
//
|
|
50
|
-
const
|
|
51
|
-
/(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)
|
|
48
|
+
// Don't scan deps, build output, or generated client entries. `dist(-<runtime>)?` also covers
|
|
49
|
+
// per-runtime output dirs (dist-bun/dist-node/dist-deno/dist-vercel). Never source, for any scan.
|
|
50
|
+
const IGNORED_DIR =
|
|
51
|
+
/(^|\/)(node_modules|dist(-[a-z0-9]+)?|build|\.nifra|\.git|\.wrangler|coverage)\//
|
|
52
|
+
// A test/spec module. Excluded from `nifra check`'s scans, which are about what SHIPS - a test
|
|
53
|
+
// legitimately drives `fetch`, hand-rolls a client, and calls a route directly. It is NOT excluded from
|
|
54
|
+
// `nifra doctor`: tsc typechecks tests, so an import a test declares nowhere is a real broken build.
|
|
55
|
+
const TEST_FILE = /\.(test|spec)\.[cm]?[jt]sx?$/
|
|
56
|
+
const IGNORED = new RegExp(`${IGNORED_DIR.source}|${TEST_FILE.source}`)
|
|
52
57
|
|
|
53
58
|
// A file under `routes/` - a page module bundled for the browser, where a server-only import is unsafe.
|
|
54
59
|
const ROUTE_FILE = /(^|\/)routes\//
|
|
@@ -1190,8 +1195,12 @@ export function scanResponseRoutes(file: string, content: string): SourceFinding
|
|
|
1190
1195
|
/** The opt-out pragma that suppresses the {@link scanResponseRoutes} advisory for an intentional raw Response. */
|
|
1191
1196
|
const RAW_RESPONSE_PRAGMA = "nifra-expect raw-response"
|
|
1192
1197
|
|
|
1193
|
-
/** Walk the project's `.ts`/`.tsx` source (skipping deps
|
|
1194
|
-
* Exported so `nifra doctor` ({@link ./doctor.ts}) scans the same source surface as `nifra check`.
|
|
1198
|
+
/** Walk the project's `.ts`/`.tsx` source (skipping deps and build output), calling `visit` per file.
|
|
1199
|
+
* Exported so `nifra doctor` ({@link ./doctor.ts}) scans the same source surface as `nifra check`.
|
|
1200
|
+
*
|
|
1201
|
+
* `includeTests` widens the surface to `*.test.ts`/`*.spec.ts`, which the default excludes. It exists
|
|
1202
|
+
* for `nifra doctor`: `tsc` compiles tests, so a test importing a package no manifest declares is a
|
|
1203
|
+
* build that a clean install cannot produce - the exact false confidence doctor is for. */
|
|
1195
1204
|
/**
|
|
1196
1205
|
* Paths git considers ignored under `cwd`, in ONE batched `git check-ignore` call - so `.gitignore`
|
|
1197
1206
|
* (root + nested + the global excludesfile) is honoured, not just the built-in {@link IGNORED} list. This
|
|
@@ -1228,12 +1237,14 @@ async function gitIgnored(cwd: string, rels: readonly string[]): Promise<Set<str
|
|
|
1228
1237
|
export async function walkSource(
|
|
1229
1238
|
cwd: string,
|
|
1230
1239
|
visit: (rel: string, content: string) => void,
|
|
1240
|
+
opts: { readonly includeTests?: boolean } = {},
|
|
1231
1241
|
): Promise<void> {
|
|
1242
|
+
const skip = opts.includeTests === true ? IGNORED_DIR : IGNORED
|
|
1232
1243
|
// List candidates first (cheap - no reads), drop the built-in ignores, then exclude gitignored paths in
|
|
1233
1244
|
// one batch before the (expensive) reads. So a gitignored generated/build tree is never read or scanned.
|
|
1234
1245
|
const rels: string[] = []
|
|
1235
1246
|
for await (const rel of new Glob("**/*.{ts,tsx,mts,cts}").scan({ cwd, dot: false })) {
|
|
1236
|
-
if (!
|
|
1247
|
+
if (!skip.test(rel)) rels.push(rel)
|
|
1237
1248
|
}
|
|
1238
1249
|
const ignored = await gitIgnored(cwd, rels)
|
|
1239
1250
|
for (const rel of rels) {
|
package/src/cli.ts
CHANGED
|
@@ -148,7 +148,7 @@ Port: \`dev\` and \`start\` share the default ${DEFAULT_DEV_PORT}. Override with
|
|
|
148
148
|
\`PORT\` env var (\`--port\` wins over \`PORT\`, which wins over the default).`
|
|
149
149
|
|
|
150
150
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
151
|
-
const CLI_VERSION = "2.8.
|
|
151
|
+
const CLI_VERSION = "2.8.2"
|
|
152
152
|
|
|
153
153
|
// A render adapter + nifra server are opaque to the CLI (it just forwards them); cast at the seam.
|
|
154
154
|
const asAdapter = (v: unknown): RenderAdapter => v as RenderAdapter
|
package/src/doctor.ts
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* declared by the package that imports it (for `tsc` and for that package to install on its own) even
|
|
10
10
|
* when a monorepo would hoist it. Relative paths, runtime builtins (node core, `node:`/`bun:`, `bun`),
|
|
11
11
|
* the package's own name, and tsconfig `paths` aliases are excluded - none of them are npm deps.
|
|
12
|
+
*
|
|
13
|
+
* The diff is against DECLARED dependency sets only; what happens to be installed on disk is never
|
|
14
|
+
* consulted. That is the point: a package pulled in transitively by some other dependency resolves fine
|
|
15
|
+
* locally while no manifest declares it, so resolvability is not evidence of declaration. The scan
|
|
16
|
+
* therefore also covers `*.test.ts`/`*.spec.ts`, which `nifra check` skips - `tsc` compiles them, so an
|
|
17
|
+
* undeclared import there fails a clean `bun install --frozen-lockfile` build just the same.
|
|
12
18
|
*/
|
|
13
19
|
import type { Dirent } from "node:fs"
|
|
14
20
|
import { readdir, realpath, stat } from "node:fs/promises"
|
|
@@ -679,12 +685,19 @@ export async function collectDoctorResult(cwd: string): Promise<DoctorResult> {
|
|
|
679
685
|
const scopes = await doctorPackageScopes(cwd, pkg)
|
|
680
686
|
|
|
681
687
|
const findings: DoctorFinding[] = []
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
+
// `includeTests`: tests are part of the typechecked surface, so an import they declare nowhere is a
|
|
689
|
+
// real break. Excluding them is what let an undeclared `zod` in a `*.test.ts` pass doctor, pass a
|
|
690
|
+
// hoisted local `tsc`, and then fail CI on a clean install with `TS2307: Cannot find module 'zod'`.
|
|
691
|
+
await walkSource(
|
|
692
|
+
cwd,
|
|
693
|
+
(rel, content) => {
|
|
694
|
+
const scope = scopeForFile(scopes, rel)
|
|
695
|
+
for (const f of scanUndeclaredImports(rel, content, scope.declared, scope.isAlias)) {
|
|
696
|
+
findings.push({ file: f.file, line: f.line, package: f.snippet })
|
|
697
|
+
}
|
|
698
|
+
},
|
|
699
|
+
{ includeTests: true },
|
|
700
|
+
)
|
|
688
701
|
findings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line)
|
|
689
702
|
const duplicateInstalls = await collectDuplicateInstalls(cwd, pkg)
|
|
690
703
|
const staleDists = await collectStaleWorkspaceDists(cwd, pkg)
|
package/src/mcp-http.ts
CHANGED
|
@@ -29,7 +29,7 @@ export { docsTools } from "./mcp-docs-tools.ts"
|
|
|
29
29
|
export type { TypeEntry } from "./types-search.ts"
|
|
30
30
|
|
|
31
31
|
// Kept in lockstep with packages/cli/package.json by check:publish's version-consistency gate.
|
|
32
|
-
const VERSION = "2.8.
|
|
32
|
+
const VERSION = "2.8.2"
|
|
33
33
|
const SERVER_INFO = { name: "nifra-docs", version: VERSION }
|
|
34
34
|
// Derive the GET/health tool list from the tools actually served, so the line can never drift from them.
|
|
35
35
|
const docsHealth = (tools: McpTool[]): string =>
|
package/src/scaffold.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* `nifra_example` for the body, rather than hand-writing an SFC we can't typecheck here.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { mkdir, writeFile } from "node:fs/promises"
|
|
12
|
+
import { lstat, mkdir, realpath, writeFile } from "node:fs/promises"
|
|
13
13
|
import { dirname, resolve, sep } from "node:path"
|
|
14
14
|
|
|
15
15
|
export type Framework = "react" | "preact" | "solid" | "vue" | "svelte" | "vanilla"
|
|
@@ -122,6 +122,37 @@ function resolveInsideCwd(cwd: string, relativeFile: string): string {
|
|
|
122
122
|
return target
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/** Lexical containment is not enough when an existing route directory is a symlink. Check every
|
|
126
|
+
* existing ancestor and its real path before scaffolding so an agent cannot redirect a write outside
|
|
127
|
+
* the selected project. The target itself is created with `wx`, so an existing target symlink is also
|
|
128
|
+
* never followed/overwritten. */
|
|
129
|
+
async function assertNoSymlinkedAncestors(root: string, target: string): Promise<void> {
|
|
130
|
+
const rootPath = resolve(root)
|
|
131
|
+
const realRoot = await realpath(rootPath)
|
|
132
|
+
let current = dirname(target)
|
|
133
|
+
while (current !== rootPath) {
|
|
134
|
+
if (!current.startsWith(`${rootPath}${sep}`)) {
|
|
135
|
+
throw new Error(`refusing to write outside project root: ${target}`)
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
if ((await lstat(current)).isSymbolicLink()) {
|
|
139
|
+
throw new Error(`refusing to write through symlinked directory: ${current}`)
|
|
140
|
+
}
|
|
141
|
+
const actual = await realpath(current)
|
|
142
|
+
if (actual !== realRoot && !actual.startsWith(`${realRoot}${sep}`)) {
|
|
143
|
+
throw new Error(`refusing to write outside project root through: ${current}`)
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
if (err && typeof err === "object" && (err as { code?: string }).code === "ENOENT") {
|
|
147
|
+
current = dirname(current)
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
throw err
|
|
151
|
+
}
|
|
152
|
+
current = dirname(current)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
125
156
|
/** Write a scaffolded route stub when the framework has a verified ready-to-write body. The write is
|
|
126
157
|
* intentionally conservative: it refuses non-JSX stubs (where we only return contract guidance) and
|
|
127
158
|
* uses `wx`, so an agent cannot overwrite user work by accident. */
|
|
@@ -139,7 +170,9 @@ export async function writeScaffoldRoute(
|
|
|
139
170
|
}
|
|
140
171
|
}
|
|
141
172
|
const target = resolveInsideCwd(cwd, result.file)
|
|
173
|
+
await assertNoSymlinkedAncestors(cwd, target)
|
|
142
174
|
await mkdir(dirname(target), { recursive: true })
|
|
175
|
+
await assertNoSymlinkedAncestors(cwd, target)
|
|
143
176
|
try {
|
|
144
177
|
await writeFile(target, result.content, { flag: "wx" })
|
|
145
178
|
} catch (err) {
|