@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
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The first-party file an import specifier names, found the way a bundler finds it: a relative or
|
|
3
|
+
* absolute path, a tsconfig `paths` alias or `baseUrl` name (following `extends`), or a package.json
|
|
4
|
+
* `imports` entry (`#x`). A bare package specifier, a file under `node_modules` and a name that matches
|
|
5
|
+
* nothing resolve to `undefined`, so a check never mistakes a dependency for project code.
|
|
6
|
+
*
|
|
7
|
+
* Never `Bun.resolveSync`: it can install a missing package and remembers answers for the life of the
|
|
8
|
+
* process, and the MCP server is long-lived. A resolver caches what it reads for one check run only.
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, realpathSync, statSync } from "node:fs"
|
|
11
|
+
import { basename, dirname, extname, isAbsolute, join } from "node:path"
|
|
12
|
+
|
|
13
|
+
/** `(absolute importing file, specifier)` to the real path of the file it loads, or `undefined`. */
|
|
14
|
+
export type SpecifierResolver = (fromFile: string, specifier: string) => string | undefined
|
|
15
|
+
|
|
16
|
+
export interface ProjectResolver extends SpecifierResolver {
|
|
17
|
+
/**
|
|
18
|
+
* `specifier`, an alias as `fromFile` writes it, rewritten through the same alias to name `file`
|
|
19
|
+
* instead, in the same style (extension, index). `undefined` when `specifier` is not an alias or no
|
|
20
|
+
* target of its alias covers `file`. Paths are absolute and unresolved, as `fromFile` is spelled.
|
|
21
|
+
*/
|
|
22
|
+
readonly respell: (fromFile: string, specifier: string, file: string) => string | undefined
|
|
23
|
+
/** The `paths` patterns of the nearest tsconfig at or above `dir`, after its `extends` chain. */
|
|
24
|
+
readonly pathPatterns: (dir: string) => readonly string[]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** How an alias specifier reached its file: what a rewrite through the same alias has to fit. */
|
|
28
|
+
interface AliasRule {
|
|
29
|
+
/** The pattern the specifier matched; `undefined` for a `baseUrl` name, which matches none. */
|
|
30
|
+
readonly key: string | undefined
|
|
31
|
+
/** Every pattern in the same scope: a rewritten name must still match `key`, or none for `baseUrl`. */
|
|
32
|
+
readonly keys: readonly string[]
|
|
33
|
+
/** The directory targets resolve against. */
|
|
34
|
+
readonly base: string
|
|
35
|
+
/** The key's targets in try order, `STAR` where the matched text goes. */
|
|
36
|
+
readonly templates: readonly string[]
|
|
37
|
+
/** The path the specifier spells before probing, and the file probing found. */
|
|
38
|
+
readonly written: string
|
|
39
|
+
readonly found: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const STAR = "\u0000"
|
|
43
|
+
|
|
44
|
+
const EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".json"]
|
|
45
|
+
// TypeScript's ESM convention: `./x.js` names `x.ts` when only the source exists.
|
|
46
|
+
const SOURCE_FOR: Readonly<Record<string, readonly string[]>> = {
|
|
47
|
+
".js": [".ts", ".tsx"],
|
|
48
|
+
".jsx": [".tsx"],
|
|
49
|
+
".mjs": [".mts"],
|
|
50
|
+
".cjs": [".cts"],
|
|
51
|
+
}
|
|
52
|
+
// The package.json `imports` conditions a server-side bundle of this project matches, in no order:
|
|
53
|
+
// the target object's own key order decides, as in Node.
|
|
54
|
+
const CONDITIONS: ReadonlySet<string> = new Set(["bun", "import", "default"])
|
|
55
|
+
const MAX_EXTENDS = 16
|
|
56
|
+
|
|
57
|
+
interface PathsConfig {
|
|
58
|
+
/** Directory `paths` targets resolve against: `baseUrl` when set, else the defining config's. */
|
|
59
|
+
readonly pathsBase: string
|
|
60
|
+
readonly paths: ReadonlyArray<readonly [string, readonly string[]]>
|
|
61
|
+
readonly baseUrl: string | undefined
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** JSON with comments and trailing commas, as tsconfig files are written. Throws on anything else. */
|
|
65
|
+
export function parseJsonc(text: string): unknown {
|
|
66
|
+
let code = ""
|
|
67
|
+
for (let i = 0; i < text.length; ) {
|
|
68
|
+
const ch = text[i]
|
|
69
|
+
if (ch === '"') {
|
|
70
|
+
let end = i + 1
|
|
71
|
+
while (end < text.length && text[end] !== '"') end += text[end] === "\\" ? 2 : 1
|
|
72
|
+
code += text.slice(i, end + 1)
|
|
73
|
+
i = end + 1
|
|
74
|
+
} else if (ch === "/" && text[i + 1] === "/") {
|
|
75
|
+
const newline = text.indexOf("\n", i)
|
|
76
|
+
i = newline === -1 ? text.length : newline
|
|
77
|
+
} else if (ch === "/" && text[i + 1] === "*") {
|
|
78
|
+
const close = text.indexOf("*/", i + 2)
|
|
79
|
+
i = close === -1 ? text.length : close + 2
|
|
80
|
+
} else {
|
|
81
|
+
code += ch
|
|
82
|
+
i += 1
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
let json = ""
|
|
86
|
+
for (let i = 0; i < code.length; i += 1) {
|
|
87
|
+
const ch = code[i]
|
|
88
|
+
if (ch === '"') {
|
|
89
|
+
let end = i + 1
|
|
90
|
+
while (end < code.length && code[end] !== '"') end += code[end] === "\\" ? 2 : 1
|
|
91
|
+
json += code.slice(i, end + 1)
|
|
92
|
+
i = end
|
|
93
|
+
} else if (ch === "," && /^\s*[}\]]/.test(code.slice(i + 1))) {
|
|
94
|
+
} else json += ch
|
|
95
|
+
}
|
|
96
|
+
return JSON.parse(json.replace(/^\uFEFF/, ""))
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const record = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
|
|
100
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
101
|
+
? Object.fromEntries(Object.entries(value))
|
|
102
|
+
: undefined
|
|
103
|
+
|
|
104
|
+
const isFile = (path: string): boolean =>
|
|
105
|
+
statSync(path, { throwIfNoEntry: false })?.isFile() ?? false
|
|
106
|
+
|
|
107
|
+
const real = (path: string): string => {
|
|
108
|
+
try {
|
|
109
|
+
return realpathSync(path)
|
|
110
|
+
} catch {
|
|
111
|
+
return path
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** The file a path names: as written, its source for a `.js` name, with an extension, or an index. */
|
|
116
|
+
function probe(base: string): string | undefined {
|
|
117
|
+
if (isFile(base)) return base
|
|
118
|
+
const ext = extname(base)
|
|
119
|
+
for (const source of SOURCE_FOR[ext] ?? []) {
|
|
120
|
+
const candidate = `${base.slice(0, -ext.length)}${source}`
|
|
121
|
+
if (isFile(candidate)) return candidate
|
|
122
|
+
}
|
|
123
|
+
for (const extension of EXTENSIONS)
|
|
124
|
+
if (isFile(`${base}${extension}`)) return `${base}${extension}`
|
|
125
|
+
for (const extension of EXTENSIONS) {
|
|
126
|
+
const index = join(base, `index${extension}`)
|
|
127
|
+
if (isFile(index)) return index
|
|
128
|
+
}
|
|
129
|
+
return undefined
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* `file` written the way `written` names `found`: as is, without its extension, as its folder when
|
|
134
|
+
* `found` is an index, or with the `.js` name of a `.ts` source.
|
|
135
|
+
*/
|
|
136
|
+
function sameStyle(written: string, found: string, file: string): string {
|
|
137
|
+
if (written === found) return file
|
|
138
|
+
const ext = extname(file)
|
|
139
|
+
const bare = file.slice(0, file.length - ext.length)
|
|
140
|
+
if (found.startsWith(`${written}/index.`) || found.startsWith(`${written}\\index.`))
|
|
141
|
+
return basename(file).startsWith("index.") ? dirname(file) : bare
|
|
142
|
+
if (found.startsWith(written)) return bare
|
|
143
|
+
const writtenExt = extname(written)
|
|
144
|
+
return SOURCE_FOR[writtenExt] === undefined ? file : `${bare}${writtenExt}`
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Longest-prefix `*` match, TypeScript's rule for `paths` and Node's for `imports`. */
|
|
148
|
+
function matchPattern(
|
|
149
|
+
patterns: readonly string[],
|
|
150
|
+
specifier: string,
|
|
151
|
+
): { readonly pattern: string; readonly star: string | undefined } | undefined {
|
|
152
|
+
if (patterns.includes(specifier)) return { pattern: specifier, star: undefined }
|
|
153
|
+
let best: { pattern: string; star: string } | undefined
|
|
154
|
+
for (const pattern of patterns) {
|
|
155
|
+
const at = pattern.indexOf("*")
|
|
156
|
+
if (at === -1 || pattern.indexOf("*", at + 1) !== -1) continue
|
|
157
|
+
const prefix = pattern.slice(0, at)
|
|
158
|
+
const suffix = pattern.slice(at + 1)
|
|
159
|
+
if (specifier.length < prefix.length + suffix.length) continue
|
|
160
|
+
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) continue
|
|
161
|
+
if (best !== undefined && prefix.length <= best.pattern.indexOf("*")) continue
|
|
162
|
+
best = { pattern, star: specifier.slice(prefix.length, specifier.length - suffix.length) }
|
|
163
|
+
}
|
|
164
|
+
return best
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createSpecifierResolver(): ProjectResolver {
|
|
168
|
+
const configs = new Map<string, PathsConfig | undefined>()
|
|
169
|
+
const tsconfigByDir = new Map<string, string | undefined>()
|
|
170
|
+
const manifests = new Map<string, Readonly<Record<string, unknown>> | undefined>()
|
|
171
|
+
const packageByDir = new Map<string, string | undefined>()
|
|
172
|
+
|
|
173
|
+
const readJsonc = (path: string): Readonly<Record<string, unknown>> | undefined => {
|
|
174
|
+
try {
|
|
175
|
+
return record(parseJsonc(readFileSync(path, "utf8")))
|
|
176
|
+
} catch {
|
|
177
|
+
return undefined
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The nearest `tsconfig.json` (or `jsconfig.json`) at or above `dir`, as Bun's resolver picks it. */
|
|
182
|
+
const tsconfigFor = (dir: string): string | undefined => {
|
|
183
|
+
if (tsconfigByDir.has(dir)) return tsconfigByDir.get(dir)
|
|
184
|
+
const own = ["tsconfig.json", "jsconfig.json"].map((name) => join(dir, name)).find(isFile)
|
|
185
|
+
const parent = dirname(dir)
|
|
186
|
+
const found = own ?? (parent === dir ? undefined : tsconfigFor(parent))
|
|
187
|
+
tsconfigByDir.set(dir, found)
|
|
188
|
+
return found
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** An `extends` entry's file: a path relative to the config, or a package found in `node_modules`. */
|
|
192
|
+
const extendedConfig = (from: string, name: string): string | undefined => {
|
|
193
|
+
const withJson = (path: string): string | undefined =>
|
|
194
|
+
isFile(path) ? path : isFile(`${path}.json`) ? `${path}.json` : undefined
|
|
195
|
+
if (name.startsWith("./") || name.startsWith("../") || isAbsolute(name))
|
|
196
|
+
return withJson(join(dirname(from), name))
|
|
197
|
+
for (let dir = dirname(from); ; dir = dirname(dir)) {
|
|
198
|
+
const target = join(dir, "node_modules", name)
|
|
199
|
+
const direct = withJson(target)
|
|
200
|
+
if (direct !== undefined) return direct
|
|
201
|
+
const manifest = readJsonc(join(target, "package.json"))
|
|
202
|
+
const field = manifest?.tsconfig
|
|
203
|
+
const nested =
|
|
204
|
+
typeof field === "string"
|
|
205
|
+
? withJson(join(target, field))
|
|
206
|
+
: withJson(join(target, "tsconfig"))
|
|
207
|
+
if (nested !== undefined) return nested
|
|
208
|
+
if (dirname(dir) === dir) return undefined
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The `paths` and `baseUrl` a config ends up with after its `extends` chain. */
|
|
213
|
+
const pathsConfig = (
|
|
214
|
+
file: string,
|
|
215
|
+
seen: ReadonlySet<string> = new Set(),
|
|
216
|
+
): PathsConfig | undefined => {
|
|
217
|
+
if (configs.has(file)) return configs.get(file)
|
|
218
|
+
if (seen.has(file) || seen.size >= MAX_EXTENDS) return undefined
|
|
219
|
+
const config = readJsonc(file)
|
|
220
|
+
let inherited: PathsConfig | undefined
|
|
221
|
+
const extended = config?.extends
|
|
222
|
+
for (const name of typeof extended === "string"
|
|
223
|
+
? [extended]
|
|
224
|
+
: Array.isArray(extended)
|
|
225
|
+
? extended
|
|
226
|
+
: []) {
|
|
227
|
+
if (typeof name !== "string") continue
|
|
228
|
+
const base = extendedConfig(file, name)
|
|
229
|
+
const parent = base === undefined ? undefined : pathsConfig(base, new Set([...seen, file]))
|
|
230
|
+
if (parent === undefined) continue
|
|
231
|
+
inherited = {
|
|
232
|
+
paths: parent.paths.length > 0 ? parent.paths : (inherited?.paths ?? []),
|
|
233
|
+
baseUrl: parent.baseUrl ?? inherited?.baseUrl,
|
|
234
|
+
pathsBase:
|
|
235
|
+
parent.paths.length > 0 ? parent.pathsBase : (inherited?.pathsBase ?? parent.pathsBase),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const options = record(config?.compilerOptions)
|
|
239
|
+
const baseUrl =
|
|
240
|
+
typeof options?.baseUrl === "string"
|
|
241
|
+
? join(dirname(file), options.baseUrl)
|
|
242
|
+
: inherited?.baseUrl
|
|
243
|
+
const ownPaths = record(options?.paths)
|
|
244
|
+
const paths =
|
|
245
|
+
ownPaths === undefined
|
|
246
|
+
? (inherited?.paths ?? [])
|
|
247
|
+
: Object.entries(ownPaths).map(
|
|
248
|
+
([pattern, targets]) =>
|
|
249
|
+
[
|
|
250
|
+
pattern,
|
|
251
|
+
Array.isArray(targets)
|
|
252
|
+
? targets.filter((t): t is string => typeof t === "string")
|
|
253
|
+
: [],
|
|
254
|
+
] as const,
|
|
255
|
+
)
|
|
256
|
+
const pathsBase =
|
|
257
|
+
baseUrl ?? (ownPaths === undefined ? (inherited?.pathsBase ?? dirname(file)) : dirname(file))
|
|
258
|
+
const resolved = config === undefined ? undefined : { paths, baseUrl, pathsBase }
|
|
259
|
+
configs.set(file, resolved)
|
|
260
|
+
return resolved
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Keyed by config, not directory: every package name reaches here, from every folder of the project.
|
|
264
|
+
const aliases = new Map<string, string | undefined>()
|
|
265
|
+
const viaTsconfig = (fromFile: string, specifier: string): string | undefined => {
|
|
266
|
+
const tsconfig = tsconfigFor(dirname(fromFile))
|
|
267
|
+
if (tsconfig === undefined) return undefined
|
|
268
|
+
const key = `${tsconfig}\0${specifier}`
|
|
269
|
+
if (aliases.has(key)) return aliases.get(key)
|
|
270
|
+
const found = aliasTarget(tsconfig, specifier)
|
|
271
|
+
aliases.set(key, found)
|
|
272
|
+
return found
|
|
273
|
+
}
|
|
274
|
+
const aliasTarget = (tsconfig: string, specifier: string): string | undefined =>
|
|
275
|
+
pathsRule(tsconfig, specifier)?.found
|
|
276
|
+
|
|
277
|
+
/** The nearest package.json at or above `dir`: the package scope Node reads `imports` from. */
|
|
278
|
+
const packageFor = (dir: string): string | undefined => {
|
|
279
|
+
if (packageByDir.has(dir)) return packageByDir.get(dir)
|
|
280
|
+
const own = join(dir, "package.json")
|
|
281
|
+
const parent = dirname(dir)
|
|
282
|
+
const found = isFile(own) ? own : parent === dir ? undefined : packageFor(parent)
|
|
283
|
+
packageByDir.set(dir, found)
|
|
284
|
+
return found
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const importTarget = (target: unknown, star: string | undefined): string | undefined => {
|
|
288
|
+
if (typeof target === "string")
|
|
289
|
+
return target.startsWith("./") ? target.replaceAll("*", star ?? "") : undefined
|
|
290
|
+
if (Array.isArray(target)) {
|
|
291
|
+
for (const entry of target) {
|
|
292
|
+
const found = importTarget(entry, star)
|
|
293
|
+
if (found !== undefined) return found
|
|
294
|
+
}
|
|
295
|
+
return undefined
|
|
296
|
+
}
|
|
297
|
+
const conditions = record(target)
|
|
298
|
+
if (conditions === undefined) return undefined
|
|
299
|
+
for (const [condition, value] of Object.entries(conditions)) {
|
|
300
|
+
if (CONDITIONS.has(condition)) return importTarget(value, star)
|
|
301
|
+
}
|
|
302
|
+
return undefined
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const importsRule = (fromFile: string, specifier: string): AliasRule | undefined => {
|
|
306
|
+
const manifestPath = packageFor(dirname(fromFile))
|
|
307
|
+
if (manifestPath === undefined) return undefined
|
|
308
|
+
if (!manifests.has(manifestPath)) manifests.set(manifestPath, readJsonc(manifestPath))
|
|
309
|
+
const imports = record(manifests.get(manifestPath)?.imports)
|
|
310
|
+
if (imports === undefined) return undefined
|
|
311
|
+
const keys = Object.keys(imports)
|
|
312
|
+
const match = matchPattern(keys, specifier)
|
|
313
|
+
if (match === undefined) return undefined
|
|
314
|
+
const template = importTarget(imports[match.pattern], STAR)
|
|
315
|
+
if (template === undefined) return undefined
|
|
316
|
+
const base = dirname(manifestPath)
|
|
317
|
+
const written = join(base, template.replaceAll(STAR, match.star ?? ""))
|
|
318
|
+
const found = probe(written)
|
|
319
|
+
return found === undefined
|
|
320
|
+
? undefined
|
|
321
|
+
: { key: match.pattern, keys, base, templates: [template], written, found }
|
|
322
|
+
}
|
|
323
|
+
const viaImports = (fromFile: string, specifier: string): string | undefined =>
|
|
324
|
+
importsRule(fromFile, specifier)?.found
|
|
325
|
+
|
|
326
|
+
const tsconfigRule = (fromFile: string, specifier: string): AliasRule | undefined => {
|
|
327
|
+
const tsconfig = tsconfigFor(dirname(fromFile))
|
|
328
|
+
return tsconfig === undefined ? undefined : pathsRule(tsconfig, specifier)
|
|
329
|
+
}
|
|
330
|
+
const pathsRule = (tsconfig: string, specifier: string): AliasRule | undefined => {
|
|
331
|
+
const config = pathsConfig(tsconfig)
|
|
332
|
+
if (config === undefined) return undefined
|
|
333
|
+
const keys = config.paths.map(([pattern]) => pattern)
|
|
334
|
+
const match = matchPattern(keys, specifier)
|
|
335
|
+
if (match === undefined) {
|
|
336
|
+
if (config.baseUrl === undefined) return undefined
|
|
337
|
+
const written = join(config.baseUrl, specifier)
|
|
338
|
+
const found = probe(written)
|
|
339
|
+
return found === undefined
|
|
340
|
+
? undefined
|
|
341
|
+
: { key: undefined, keys, base: config.baseUrl, templates: [STAR], written, found }
|
|
342
|
+
}
|
|
343
|
+
// TypeScript permits at most one wildcard in each substitution, as in the alias key.
|
|
344
|
+
const targets = (config.paths.find(([pattern]) => pattern === match.pattern)?.[1] ?? []).filter(
|
|
345
|
+
(target) => target.indexOf("*") === target.lastIndexOf("*"),
|
|
346
|
+
)
|
|
347
|
+
for (const target of targets) {
|
|
348
|
+
const written = join(config.pathsBase, target.replaceAll("*", match.star ?? ""))
|
|
349
|
+
const found = probe(written)
|
|
350
|
+
if (found === undefined) continue
|
|
351
|
+
const templates = targets.map((t) => t.replaceAll("*", STAR))
|
|
352
|
+
return { key: match.pattern, keys, base: config.pathsBase, templates, written, found }
|
|
353
|
+
}
|
|
354
|
+
return undefined
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const respell = (fromFile: string, specifier: string, file: string): string | undefined => {
|
|
358
|
+
if (/^\.\.?(?:\/|$)/.test(specifier) || isAbsolute(specifier)) return undefined
|
|
359
|
+
const rule =
|
|
360
|
+
(specifier.startsWith("#") ? importsRule(fromFile, specifier) : undefined) ??
|
|
361
|
+
tsconfigRule(fromFile, specifier)
|
|
362
|
+
if (rule === undefined) return undefined
|
|
363
|
+
const spelled = sameStyle(rule.written, rule.found, file)
|
|
364
|
+
for (const template of rule.templates) {
|
|
365
|
+
const at = template.indexOf(STAR)
|
|
366
|
+
if (at === -1) continue
|
|
367
|
+
// An empty head still ends at a folder boundary: `src2/x` is not under a `baseUrl` of `src`.
|
|
368
|
+
const prefix = join(rule.base, template.slice(0, at) || "./")
|
|
369
|
+
const suffix = template.slice(at + STAR.length)
|
|
370
|
+
if (!spelled.startsWith(prefix) || !spelled.endsWith(suffix)) continue
|
|
371
|
+
if (spelled.length < prefix.length + suffix.length) continue
|
|
372
|
+
const star = spelled
|
|
373
|
+
.slice(prefix.length, spelled.length - suffix.length)
|
|
374
|
+
.replaceAll("\\", "/")
|
|
375
|
+
if (star === "" || star.startsWith("/")) continue
|
|
376
|
+
const keyAt = rule.key?.indexOf("*") ?? 0
|
|
377
|
+
if (keyAt === -1) continue
|
|
378
|
+
const next =
|
|
379
|
+
rule.key === undefined
|
|
380
|
+
? star
|
|
381
|
+
: `${rule.key.slice(0, keyAt)}${star}${rule.key.slice(keyAt + 1)}`
|
|
382
|
+
// The rewritten name must take the same route: another, longer pattern would claim it.
|
|
383
|
+
if (matchPattern(rule.keys, next)?.pattern !== rule.key) continue
|
|
384
|
+
return next
|
|
385
|
+
}
|
|
386
|
+
return undefined
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const resolved = new Map<string, string | undefined>()
|
|
390
|
+
const resolve: SpecifierResolver = (fromFile, specifier) => {
|
|
391
|
+
// Every rule below reads only the importing file's directory, so the answer is the directory's.
|
|
392
|
+
const key = `${dirname(fromFile)}\0${specifier}`
|
|
393
|
+
if (resolved.has(key)) return resolved.get(key)
|
|
394
|
+
const path = resolveFrom(fromFile, specifier)
|
|
395
|
+
resolved.set(key, path)
|
|
396
|
+
return path
|
|
397
|
+
}
|
|
398
|
+
const pathPatterns = (dir: string): readonly string[] => {
|
|
399
|
+
const tsconfig = tsconfigFor(dir)
|
|
400
|
+
const config = tsconfig === undefined ? undefined : pathsConfig(tsconfig)
|
|
401
|
+
return (config?.paths ?? []).map(([pattern]) => pattern)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return Object.assign(resolve, { respell, pathPatterns })
|
|
405
|
+
|
|
406
|
+
function resolveFrom(fromFile: string, specifier: string): string | undefined {
|
|
407
|
+
let found: string | undefined
|
|
408
|
+
if (
|
|
409
|
+
specifier.startsWith("./") ||
|
|
410
|
+
specifier.startsWith("../") ||
|
|
411
|
+
specifier === "." ||
|
|
412
|
+
specifier === ".."
|
|
413
|
+
)
|
|
414
|
+
found = probe(join(dirname(fromFile), specifier))
|
|
415
|
+
else if (isAbsolute(specifier)) found = probe(specifier)
|
|
416
|
+
// A `#` name is a package.json import first; a tsconfig may still map it.
|
|
417
|
+
else if (specifier.startsWith("#"))
|
|
418
|
+
found = viaImports(fromFile, specifier) ?? viaTsconfig(fromFile, specifier)
|
|
419
|
+
else found = viaTsconfig(fromFile, specifier)
|
|
420
|
+
if (found === undefined) return undefined
|
|
421
|
+
const path = real(found)
|
|
422
|
+
return /[\\/]node_modules[\\/]/.test(path) ? undefined : path
|
|
423
|
+
}
|
|
424
|
+
}
|
|
@@ -6,6 +6,7 @@ import type * as TSApi from "typescript"
|
|
|
6
6
|
*/
|
|
7
7
|
export function scriptKindOf(ts: typeof TSApi, file: string): TSApi.ScriptKind {
|
|
8
8
|
if (file.endsWith(".tsx")) return ts.ScriptKind.TSX
|
|
9
|
-
|
|
9
|
+
// A `.svelte`/`.vue` file is scanned as its script blocks, which may be TypeScript.
|
|
10
|
+
if (/\.(?:[cm]?ts|svelte|vue)$/.test(file)) return ts.ScriptKind.TS
|
|
10
11
|
return file.endsWith(".jsx") ? ts.ScriptKind.JSX : ts.ScriptKind.JS
|
|
11
12
|
}
|
|
@@ -20,6 +20,13 @@ export interface SourceFacts {
|
|
|
20
20
|
method: string,
|
|
21
21
|
path: string,
|
|
22
22
|
): boolean | undefined
|
|
23
|
+
/** A `.method(argument, …)` member call whose first argument is written exactly as `argument`. */
|
|
24
|
+
isRouteArgumentAt(
|
|
25
|
+
source: TSApi.SourceFile,
|
|
26
|
+
position: number,
|
|
27
|
+
method: string,
|
|
28
|
+
argument: string,
|
|
29
|
+
): boolean | undefined
|
|
23
30
|
/** A call to the free function `callee` whose argument at `pathIndex` is the literal `path`. */
|
|
24
31
|
isFunctionRouteCallAt(
|
|
25
32
|
source: TSApi.SourceFile,
|
|
@@ -142,6 +149,23 @@ export function createSourceFacts(ts: TypeScriptApi): SourceFacts {
|
|
|
142
149
|
)
|
|
143
150
|
}
|
|
144
151
|
|
|
152
|
+
const isRouteArgumentAt = (
|
|
153
|
+
source: TSApi.SourceFile,
|
|
154
|
+
position: number,
|
|
155
|
+
method: string,
|
|
156
|
+
argument: string,
|
|
157
|
+
): boolean | undefined => {
|
|
158
|
+
const call = ancestorAt(ts, source, position, ts.isCallExpression)
|
|
159
|
+
if (call === undefined || !ts.isPropertyAccessExpression(call.expression)) return false
|
|
160
|
+
const first = call.arguments[0]
|
|
161
|
+
return (
|
|
162
|
+
call.expression.name.text.toUpperCase() === method &&
|
|
163
|
+
call.arguments.length > 1 &&
|
|
164
|
+
first !== undefined &&
|
|
165
|
+
first.getText(source) === argument
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
145
169
|
const isFunctionRouteCallAt = (
|
|
146
170
|
source: TSApi.SourceFile,
|
|
147
171
|
position: number,
|
|
@@ -200,6 +224,7 @@ export function createSourceFacts(ts: TypeScriptApi): SourceFacts {
|
|
|
200
224
|
parse,
|
|
201
225
|
isValueImportAt,
|
|
202
226
|
isRouteRegistrationAt,
|
|
227
|
+
isRouteArgumentAt,
|
|
203
228
|
isFunctionRouteCallAt,
|
|
204
229
|
isResponseSyntaxAt,
|
|
205
230
|
isDynamicImportAt,
|
|
@@ -48,11 +48,14 @@ export function unsupportedTypeScriptMessage(version: string): string {
|
|
|
48
48
|
return `${NIFRA_TS_UNSUPPORTED}: TypeScript ${version} is outside the compiler API versions supported by Nifra scanners. Use TypeScript 5, 6, or 7, or pin the project to TypeScript 6 (for example, \`bun add -d typescript@^6\`).`
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
export function unsupportedTypeScriptDiagnostic(
|
|
51
|
+
export function unsupportedTypeScriptDiagnostic(
|
|
52
|
+
version: string,
|
|
53
|
+
message = unsupportedTypeScriptMessage(version),
|
|
54
|
+
): Diagnostic {
|
|
52
55
|
return diagnostic({
|
|
53
56
|
code: NIFRA_TS_UNSUPPORTED,
|
|
54
57
|
severity: "error",
|
|
55
|
-
message
|
|
58
|
+
message,
|
|
56
59
|
fix: {
|
|
57
60
|
recipe: "toolchain.install-typescript",
|
|
58
61
|
command: "bun add -d typescript@^6",
|
|
@@ -412,7 +415,11 @@ function sourceEntries(
|
|
|
412
415
|
const content = sourceIndex.read(file)
|
|
413
416
|
if (content === undefined) continue
|
|
414
417
|
const absolute = isAbsolute(file) ? file : resolve(root, file)
|
|
415
|
-
|
|
418
|
+
// The session opens script extensions only; a component's script blocks are opened as TypeScript
|
|
419
|
+
// beside it, and `createSourceFile` finds them by their text.
|
|
420
|
+
const normalized = normalizedPath(
|
|
421
|
+
/\.(?:svelte|vue)$/.test(absolute) ? `${absolute}.ts` : absolute,
|
|
422
|
+
)
|
|
416
423
|
paths.push(normalized)
|
|
417
424
|
for (const variant of pathVariants(normalized)) files.set(variant, content)
|
|
418
425
|
}
|