@nifrajs/cli 4.0.2 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +10 -2
- package/dist/agent-files.d.ts +107 -0
- package/dist/agent-files.d.ts.map +1 -0
- package/dist/agent-files.js +162 -0
- package/dist/agent-files.js.map +1 -0
- package/dist/app-files.d.ts +6 -0
- package/dist/app-files.d.ts.map +1 -1
- package/dist/app-files.js +27 -2
- package/dist/app-files.js.map +1 -1
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +9 -2
- package/dist/assure-hydration.js.map +1 -1
- package/dist/assure.d.ts +12 -2
- package/dist/assure.d.ts.map +1 -1
- package/dist/assure.js +59 -8
- package/dist/assure.js.map +1 -1
- package/dist/capabilities-tool.d.ts.map +1 -1
- package/dist/capabilities-tool.js +36 -9
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +22 -2
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js +54 -7
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +83 -24
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +1516 -318
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts +2 -2
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +203 -70
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +215 -50
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +20 -0
- package/dist/client-module.d.ts.map +1 -0
- package/dist/client-module.js +73 -0
- package/dist/client-module.js.map +1 -0
- package/dist/command-catalog.d.ts +24 -5
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +194 -45
- package/dist/command-catalog.js.map +1 -1
- package/dist/component-check.d.ts +13 -0
- package/dist/component-check.d.ts.map +1 -0
- package/dist/component-check.js +59 -0
- package/dist/component-check.js.map +1 -0
- package/dist/contract-proof.d.ts +1 -1
- package/dist/contract-proof.d.ts.map +1 -1
- package/dist/contract-proof.js +2 -24
- package/dist/contract-proof.js.map +1 -1
- package/dist/contracts.d.ts +19 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +50 -3
- package/dist/contracts.js.map +1 -1
- package/dist/db-tool.d.ts.map +1 -1
- package/dist/db-tool.js +4 -3
- package/dist/db-tool.js.map +1 -1
- package/dist/dev-bun-config.d.ts +5 -1
- package/dist/dev-bun-config.d.ts.map +1 -1
- package/dist/dev-bun-config.js +44 -11
- package/dist/dev-bun-config.js.map +1 -1
- package/dist/dev-feed-tool.d.ts +18 -1
- package/dist/dev-feed-tool.d.ts.map +1 -1
- package/dist/dev-feed-tool.js +93 -0
- package/dist/dev-feed-tool.js.map +1 -1
- package/dist/diagnostics.d.ts +3 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/diff-tool.d.ts +9 -8
- package/dist/diff-tool.d.ts.map +1 -1
- package/dist/diff-tool.js +28 -24
- package/dist/diff-tool.js.map +1 -1
- package/dist/doctor.d.ts +13 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +23 -15
- package/dist/doctor.js.map +1 -1
- package/dist/env-file.d.ts +6 -0
- package/dist/env-file.d.ts.map +1 -1
- package/dist/env-file.js +6 -0
- package/dist/env-file.js.map +1 -1
- package/dist/fix-recipes.d.ts +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +102 -2
- package/dist/fix-recipes.js.map +1 -1
- package/dist/hand-built-manifest.d.ts +27 -0
- package/dist/hand-built-manifest.d.ts.map +1 -0
- package/dist/hand-built-manifest.js +124 -0
- package/dist/hand-built-manifest.js.map +1 -0
- package/dist/hooks-report.d.ts +23 -0
- package/dist/hooks-report.d.ts.map +1 -0
- package/dist/hooks-report.js +104 -0
- package/dist/hooks-report.js.map +1 -0
- package/dist/init-agents.d.ts +18 -2
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +20 -13
- package/dist/init-agents.js.map +1 -1
- package/dist/internal/config-paths.d.ts +16 -0
- package/dist/internal/config-paths.d.ts.map +1 -0
- package/dist/internal/config-paths.js +124 -0
- package/dist/internal/config-paths.js.map +1 -0
- package/dist/internal/installed-package.d.ts +4 -0
- package/dist/internal/installed-package.d.ts.map +1 -0
- package/dist/internal/installed-package.js +17 -0
- package/dist/internal/installed-package.js.map +1 -0
- package/dist/internal/nifra-project.d.ts +41 -0
- package/dist/internal/nifra-project.d.ts.map +1 -0
- package/dist/internal/nifra-project.js +183 -0
- package/dist/internal/nifra-project.js.map +1 -0
- package/dist/internal/project-specifiers.d.ts +29 -0
- package/dist/internal/project-specifiers.d.ts.map +1 -0
- package/dist/internal/project-specifiers.js +435 -0
- package/dist/internal/project-specifiers.js.map +1 -0
- package/dist/internal/script-kind.d.ts.map +1 -1
- package/dist/internal/script-kind.js +2 -1
- package/dist/internal/script-kind.js.map +1 -1
- package/dist/internal/source-facts.d.ts +8 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +22 -5
- package/dist/internal/source-facts.js.map +1 -1
- package/dist/internal/typescript-import.d.ts +1 -1
- package/dist/internal/typescript-import.d.ts.map +1 -1
- package/dist/internal/typescript-import.js +10 -6
- package/dist/internal/typescript-import.js.map +1 -1
- package/dist/introspect.d.ts +15 -6
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +87 -25
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +98 -23
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +33 -10
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +60 -10
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts +5 -5
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +8 -23
- package/dist/mcp-context.js.map +1 -1
- package/dist/mcp-delegate.d.ts +2 -2
- package/dist/mcp-delegate.d.ts.map +1 -1
- package/dist/mcp-delegate.js +19 -7
- package/dist/mcp-delegate.js.map +1 -1
- package/dist/mcp-exec.d.ts +13 -4
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +56 -14
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +30 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +63 -0
- package/dist/mcp-io.js.map +1 -1
- package/dist/mcp-isolate.d.ts.map +1 -1
- package/dist/mcp-isolate.js +3 -2
- package/dist/mcp-isolate.js.map +1 -1
- package/dist/mcp-path.d.ts +2 -1
- package/dist/mcp-path.d.ts.map +1 -1
- package/dist/mcp-path.js +9 -24
- package/dist/mcp-path.js.map +1 -1
- package/dist/mcp-project-child.js +1 -1
- package/dist/mcp-project-child.js.map +1 -1
- package/dist/mcp-root.d.ts.map +1 -1
- package/dist/mcp-root.js +50 -10
- package/dist/mcp-root.js.map +1 -1
- package/dist/mcp-ws.d.ts.map +1 -1
- package/dist/mcp-ws.js +3 -1
- package/dist/mcp-ws.js.map +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +48 -19
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +47 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1972 -170
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +128 -0
- package/dist/output-schemas.d.ts.map +1 -0
- package/dist/output-schemas.js +975 -0
- package/dist/output-schemas.js.map +1 -0
- package/dist/port.d.ts.map +1 -1
- package/dist/port.js +3 -2
- package/dist/port.js.map +1 -1
- package/dist/project-facts.d.ts +15 -4
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +1 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.d.ts.map +1 -1
- package/dist/project-path.js +48 -14
- package/dist/project-path.js.map +1 -1
- package/dist/recipes/5.0.0.d.ts +11 -0
- package/dist/recipes/5.0.0.d.ts.map +1 -0
- package/dist/recipes/5.0.0.js +34 -0
- package/dist/recipes/5.0.0.js.map +1 -0
- package/dist/recipes/index.d.ts +7 -0
- package/dist/recipes/index.d.ts.map +1 -1
- package/dist/recipes/index.js +2 -0
- package/dist/recipes/index.js.map +1 -1
- package/dist/release-verification.d.ts +5 -0
- package/dist/release-verification.d.ts.map +1 -1
- package/dist/release-verification.js +14 -0
- package/dist/release-verification.js.map +1 -1
- package/dist/replay.d.ts.map +1 -1
- package/dist/replay.js +12 -3
- package/dist/replay.js.map +1 -1
- package/dist/review.d.ts +3 -1
- package/dist/review.d.ts.map +1 -1
- package/dist/review.js +43 -7
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +12 -0
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +12 -0
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/comment-markers.d.ts +16 -7
- package/dist/rules/comment-markers.d.ts.map +1 -1
- package/dist/rules/comment-markers.js +58 -19
- package/dist/rules/comment-markers.js.map +1 -1
- package/dist/rules/contract-visibility.d.ts +3 -0
- package/dist/rules/contract-visibility.d.ts.map +1 -0
- package/dist/rules/contract-visibility.js +337 -0
- package/dist/rules/contract-visibility.js.map +1 -0
- package/dist/rules/data-guard.d.ts +13 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +356 -37
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/layout.d.ts +4 -0
- package/dist/rules/layout.d.ts.map +1 -0
- package/dist/rules/layout.js +19 -0
- package/dist/rules/layout.js.map +1 -0
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +178 -44
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +50 -0
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +436 -4
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/routes.d.ts +6 -0
- package/dist/rules/routes.d.ts.map +1 -1
- package/dist/rules/routes.js +111 -16
- package/dist/rules/routes.js.map +1 -1
- package/dist/rules/secrets.js +2 -2
- package/dist/rules/secrets.js.map +1 -1
- package/dist/rules/security.d.ts +13 -1
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +313 -69
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts +13 -0
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +218 -44
- package/dist/rules/zones.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +6 -4
- package/dist/scaffold.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +14 -5
- package/dist/sdk.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +290 -24
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +6 -3
- package/dist/test-tool.js.map +1 -1
- package/dist/typecheck-coverage.d.ts +20 -0
- package/dist/typecheck-coverage.d.ts.map +1 -0
- package/dist/typecheck-coverage.js +129 -0
- package/dist/typecheck-coverage.js.map +1 -0
- package/dist/upgrade.d.ts +84 -0
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +498 -78
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +16 -2
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts +22 -0
- package/dist/version-drift.d.ts.map +1 -0
- package/dist/version-drift.js +94 -0
- package/dist/version-drift.js.map +1 -0
- package/dist/web-app-options.d.ts +1 -1
- package/dist/web-app-options.d.ts.map +1 -1
- package/dist/web-app-options.js +1 -1
- package/dist/web-app-options.js.map +1 -1
- package/dist/work-graph.d.ts.map +1 -1
- package/dist/work-graph.js +20 -8
- package/dist/work-graph.js.map +1 -1
- package/docs/examples.json +55 -7
- package/docs/llms-full.txt +2043 -222
- package/docs/types.json +1573 -246
- package/package.json +11 -12
- package/src/agent-files.ts +203 -0
- package/src/app-files.ts +34 -4
- package/src/assure-hydration.ts +18 -2
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +47 -9
- package/src/check-diagnostics.ts +75 -9
- package/src/check-scan.ts +1816 -377
- package/src/check.ts +258 -81
- package/src/cli.ts +242 -37
- package/src/client-module.ts +97 -0
- package/src/command-catalog.ts +237 -52
- package/src/component-check.ts +79 -0
- package/src/contract-proof.ts +3 -25
- package/src/contracts.ts +60 -3
- package/src/db-tool.ts +5 -2
- package/src/dev-bun-config.ts +51 -10
- package/src/dev-feed-tool.ts +112 -1
- package/src/diagnostics.ts +3 -0
- package/src/diff-tool.ts +33 -26
- package/src/doctor.ts +43 -21
- package/src/env-file.ts +7 -0
- package/src/fix-recipes.ts +116 -2
- package/src/hand-built-manifest.ts +144 -0
- package/src/hooks-report.ts +135 -0
- package/src/init-agents.ts +21 -14
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/nifra-project.ts +191 -0
- package/src/internal/project-specifiers.ts +477 -0
- package/src/internal/script-kind.ts +2 -1
- package/src/internal/source-facts.ts +48 -5
- package/src/internal/typescript-import.ts +15 -6
- package/src/introspect.ts +104 -28
- package/src/levels-tool.ts +124 -29
- package/src/load.ts +107 -17
- package/src/mcp-context.ts +12 -25
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +68 -18
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +74 -0
- package/src/mcp-isolate.ts +9 -2
- package/src/mcp-path.ts +8 -22
- package/src/mcp-project-child.ts +1 -1
- package/src/mcp-root.ts +58 -8
- package/src/mcp-ws.ts +4 -2
- package/src/mcp.ts +56 -22
- package/src/migrate-layout.ts +2257 -176
- package/src/output-schemas.ts +1240 -0
- package/src/port.ts +3 -2
- package/src/project-facts.ts +17 -3
- package/src/project-path.ts +44 -13
- package/src/recipes/5.0.0.ts +35 -0
- package/src/recipes/index.ts +10 -0
- package/src/release-verification.ts +20 -0
- package/src/replay.ts +14 -3
- package/src/review.ts +44 -10
- package/src/rules/codes.ts +12 -0
- package/src/rules/comment-markers.ts +57 -21
- package/src/rules/contract-visibility.ts +383 -0
- package/src/rules/data-guard.ts +391 -42
- package/src/rules/layout.ts +22 -0
- package/src/rules/legacy.ts +206 -52
- package/src/rules/pages.ts +481 -4
- package/src/rules/routes.ts +124 -16
- package/src/rules/secrets.ts +2 -2
- package/src/rules/security.ts +391 -72
- package/src/rules/zones.ts +232 -42
- package/src/scaffold.ts +6 -4
- package/src/sdk.ts +14 -5
- package/src/stylex-migrate.ts +251 -24
- package/src/test-tool.ts +12 -3
- package/src/typecheck-coverage.ts +134 -0
- package/src/upgrade.ts +576 -72
- package/src/verification-plan.ts +33 -2
- package/src/version-drift.ts +118 -0
- package/src/web-app-options.ts +2 -2
- package/src/work-graph.ts +17 -12
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
2
|
+
import { dirname, join, relative } from "node:path"
|
|
3
|
+
import { CONFIG_FILE, FRAMEWORK_FILE } from "../app-files.ts"
|
|
4
|
+
|
|
5
|
+
const NIFRA_PACKAGE = /^(?:@nifrajs\/|nifra$)/
|
|
6
|
+
const DEPENDENCY_FIELDS = [
|
|
7
|
+
"dependencies",
|
|
8
|
+
"devDependencies",
|
|
9
|
+
"peerDependencies",
|
|
10
|
+
"optionalDependencies",
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
/** Whether a package.json text names `nifra` or a `@nifrajs/` package, or carries a `nifra` setting. */
|
|
14
|
+
function declaresNifra(manifest: string): boolean {
|
|
15
|
+
let parsed: unknown
|
|
16
|
+
try {
|
|
17
|
+
parsed = JSON.parse(manifest)
|
|
18
|
+
} catch {
|
|
19
|
+
// A manifest nobody can read proves nothing either way, so the directory stays checked.
|
|
20
|
+
return true
|
|
21
|
+
}
|
|
22
|
+
if (typeof parsed !== "object" || parsed === null) return true
|
|
23
|
+
if ("nifra" in parsed) return true
|
|
24
|
+
return DEPENDENCY_FIELDS.some((field) => {
|
|
25
|
+
const deps: unknown = Reflect.get(parsed, field)
|
|
26
|
+
return (
|
|
27
|
+
typeof deps === "object" &&
|
|
28
|
+
deps !== null &&
|
|
29
|
+
Object.keys(deps).some((name) => NIFRA_PACKAGE.test(name))
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Whether a project-relative directory ("" for the root) belongs to a nifra project, read statically.
|
|
36
|
+
* The checked root always does: running the check there says so. Below it, walking up, the first
|
|
37
|
+
* directory with a nifra config decides yes, and the first package.json decides by naming `nifra` or
|
|
38
|
+
* a `@nifrajs/` package, or carrying a `nifra` setting; a nested package that does none of that is
|
|
39
|
+
* another project, such as another framework's app kept beside this one.
|
|
40
|
+
*/
|
|
41
|
+
export function createNifraProjectProbe(root: string): (dir: string) => boolean {
|
|
42
|
+
const cache = new Map<string, boolean>()
|
|
43
|
+
const probe = (dir: string): boolean => {
|
|
44
|
+
const cached = cache.get(dir)
|
|
45
|
+
if (cached !== undefined) return cached
|
|
46
|
+
const at = join(root, dir)
|
|
47
|
+
let nifra: boolean
|
|
48
|
+
if (dir === "" || existsSync(join(at, CONFIG_FILE)) || existsSync(join(at, FRAMEWORK_FILE)))
|
|
49
|
+
nifra = true
|
|
50
|
+
else if (existsSync(join(at, "package.json"))) {
|
|
51
|
+
let manifest: string
|
|
52
|
+
try {
|
|
53
|
+
manifest = readFileSync(join(at, "package.json"), "utf8")
|
|
54
|
+
} catch {
|
|
55
|
+
manifest = ""
|
|
56
|
+
}
|
|
57
|
+
nifra = declaresNifra(manifest)
|
|
58
|
+
} else nifra = probe(dir.slice(0, Math.max(dir.lastIndexOf("/"), 0)))
|
|
59
|
+
cache.set(dir, nifra)
|
|
60
|
+
return nifra
|
|
61
|
+
}
|
|
62
|
+
return probe
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export type ProjectKind = "api" | "web"
|
|
66
|
+
|
|
67
|
+
export interface KindDeclaration {
|
|
68
|
+
/** `"api"` or `"web"` from `"nifra": { "kind" }`, or `undefined` when the package declares none. */
|
|
69
|
+
readonly kind: ProjectKind | undefined
|
|
70
|
+
/** The package.json that decides, relative to the checked root (`../package.json` above it). */
|
|
71
|
+
readonly manifest: string | undefined
|
|
72
|
+
/** The problem with a declared value that is neither `"api"` nor `"web"`. */
|
|
73
|
+
readonly invalid?: string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readJson(path: string): unknown {
|
|
77
|
+
try {
|
|
78
|
+
return JSON.parse(readFileSync(path, "utf8"))
|
|
79
|
+
} catch {
|
|
80
|
+
return undefined
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const recordOf = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
|
|
85
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
86
|
+
? Object.fromEntries(Object.entries(value))
|
|
87
|
+
: undefined
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The `"nifra": { "kind" }` that governs a project-relative directory: read from the nearest
|
|
91
|
+
* package.json at or above it, the way the zone setting is read, with no app code run.
|
|
92
|
+
*/
|
|
93
|
+
export function createKindProbe(root: string): (dir: string) => KindDeclaration {
|
|
94
|
+
const cache = new Map<string, KindDeclaration>()
|
|
95
|
+
const probe = (absolute: string): KindDeclaration => {
|
|
96
|
+
const cached = cache.get(absolute)
|
|
97
|
+
if (cached !== undefined) return cached
|
|
98
|
+
let declaration: KindDeclaration
|
|
99
|
+
const manifestPath = join(absolute, "package.json")
|
|
100
|
+
if (existsSync(manifestPath)) {
|
|
101
|
+
const manifest = relative(root, manifestPath).replaceAll("\\", "/")
|
|
102
|
+
const kind = recordOf(recordOf(readJson(manifestPath))?.nifra)?.kind
|
|
103
|
+
if (kind === undefined) declaration = { kind: undefined, manifest }
|
|
104
|
+
else if (kind === "api" || kind === "web") declaration = { kind, manifest }
|
|
105
|
+
else
|
|
106
|
+
declaration = {
|
|
107
|
+
kind: undefined,
|
|
108
|
+
manifest,
|
|
109
|
+
invalid: `[nifra] ${manifest} "nifra.kind" must be "api" or "web", not ${JSON.stringify(kind)}`,
|
|
110
|
+
}
|
|
111
|
+
} else {
|
|
112
|
+
const parent = dirname(absolute)
|
|
113
|
+
declaration = parent === absolute ? { kind: undefined, manifest: undefined } : probe(parent)
|
|
114
|
+
}
|
|
115
|
+
cache.set(absolute, declaration)
|
|
116
|
+
return declaration
|
|
117
|
+
}
|
|
118
|
+
return (dir) => probe(join(root, dir))
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Whether a project-relative directory holds web config: a `nifra.config.ts` exporting `adapter`, or
|
|
122
|
+
* `backend/framework.ts`. */
|
|
123
|
+
export function hasWebConfig(root: string, dir: string): boolean {
|
|
124
|
+
if (existsSync(join(root, dir, FRAMEWORK_FILE))) return true
|
|
125
|
+
const config = join(root, dir, CONFIG_FILE)
|
|
126
|
+
if (!existsSync(config)) return false
|
|
127
|
+
let text: string
|
|
128
|
+
try {
|
|
129
|
+
text = readFileSync(config, "utf8")
|
|
130
|
+
} catch {
|
|
131
|
+
return true
|
|
132
|
+
}
|
|
133
|
+
return /\bexport\s+(?:const|let|var|function)\s+adapter\b|\bexport\s*\{[^}]*\badapter\b/.test(
|
|
134
|
+
text,
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const WEB_PACKAGE = /^@nifrajs\/web(?:-[a-z0-9-]+)?$/
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Whether a project-relative directory ("" for the root) is inside a nifra web app. Walking up, the
|
|
142
|
+
* first directory with `nifra.config.ts` or `backend/framework.ts` makes it one; the first package.json
|
|
143
|
+
* decides instead: `"nifra": { "kind": "api" }` says no, otherwise naming `@nifrajs/web` or an adapter
|
|
144
|
+
* says yes. Nothing up to the root means no.
|
|
145
|
+
*/
|
|
146
|
+
export function createWebAppProbe(root: string): (dir: string) => boolean {
|
|
147
|
+
const cache = new Map<string, boolean>()
|
|
148
|
+
const probe = (dir: string): boolean => {
|
|
149
|
+
const cached = cache.get(dir)
|
|
150
|
+
if (cached !== undefined) return cached
|
|
151
|
+
const at = join(root, dir)
|
|
152
|
+
let web: boolean
|
|
153
|
+
if (existsSync(join(at, CONFIG_FILE)) || existsSync(join(at, FRAMEWORK_FILE))) web = true
|
|
154
|
+
else if (existsSync(join(at, "package.json"))) {
|
|
155
|
+
const manifest = recordOf(readJson(join(at, "package.json")))
|
|
156
|
+
web =
|
|
157
|
+
recordOf(manifest?.nifra)?.kind !== "api" &&
|
|
158
|
+
DEPENDENCY_FIELDS.some((field) => {
|
|
159
|
+
const deps = recordOf(manifest?.[field])
|
|
160
|
+
return deps !== undefined && Object.keys(deps).some((name) => WEB_PACKAGE.test(name))
|
|
161
|
+
})
|
|
162
|
+
} else web = dir !== "" && probe(dir.slice(0, Math.max(dir.lastIndexOf("/"), 0)))
|
|
163
|
+
cache.set(dir, web)
|
|
164
|
+
return web
|
|
165
|
+
}
|
|
166
|
+
return probe
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** A zone or server-only finding outside any web app, with the API-only setting named. */
|
|
170
|
+
export const withApiKindHint = (message: string): string =>
|
|
171
|
+
`${message}${message.endsWith(".") ? "" : "."} If this project serves no pages, declare it in package.json: "nifra": { "kind": "api" }`
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether `"nifra": { "kind": "api" }` takes a project-relative directory out of the zone checks. It
|
|
175
|
+
* does only where no web config sits at or above the directory, up to the checked root: an app that
|
|
176
|
+
* serves pages keeps its zone checks whatever its package.json says (NF-C037 reports the clash).
|
|
177
|
+
*/
|
|
178
|
+
export function createApiOnlyProbe(root: string): (dir: string) => boolean {
|
|
179
|
+
const kindOf = createKindProbe(root)
|
|
180
|
+
const web = new Map<string, boolean>()
|
|
181
|
+
const webConfigUp = (dir: string): boolean => {
|
|
182
|
+
const cached = web.get(dir)
|
|
183
|
+
if (cached !== undefined) return cached
|
|
184
|
+
const found =
|
|
185
|
+
hasWebConfig(root, dir) ||
|
|
186
|
+
(dir !== "" && webConfigUp(dir.slice(0, Math.max(dir.lastIndexOf("/"), 0))))
|
|
187
|
+
web.set(dir, found)
|
|
188
|
+
return found
|
|
189
|
+
}
|
|
190
|
+
return (dir) => kindOf(dir).kind === "api" && !webConfigUp(dir)
|
|
191
|
+
}
|
|
@@ -0,0 +1,477 @@
|
|
|
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
|
+
* Whether the compiler settings for `file` keep an import whose named specifiers are all
|
|
27
|
+
* type-only, as `import {} from "x"` (`verbatimModuleSyntax`). `true` when its tsconfig chain
|
|
28
|
+
* cannot be read, so a scan that trusts the answer keeps the edge.
|
|
29
|
+
*/
|
|
30
|
+
readonly keepsTypeOnlyImports: (file: string) => boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** How an alias specifier reached its file: what a rewrite through the same alias has to fit. */
|
|
34
|
+
interface AliasRule {
|
|
35
|
+
/** The pattern the specifier matched; `undefined` for a `baseUrl` name, which matches none. */
|
|
36
|
+
readonly key: string | undefined
|
|
37
|
+
/** Every pattern in the same scope: a rewritten name must still match `key`, or none for `baseUrl`. */
|
|
38
|
+
readonly keys: readonly string[]
|
|
39
|
+
/** The directory targets resolve against. */
|
|
40
|
+
readonly base: string
|
|
41
|
+
/** The key's targets in try order, `STAR` where the matched text goes. */
|
|
42
|
+
readonly templates: readonly string[]
|
|
43
|
+
/** The path the specifier spells before probing, and the file probing found. */
|
|
44
|
+
readonly written: string
|
|
45
|
+
readonly found: string
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const STAR = "\u0000"
|
|
49
|
+
|
|
50
|
+
const EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs", ".json"]
|
|
51
|
+
// TypeScript's ESM convention: `./x.js` names `x.ts` when only the source exists.
|
|
52
|
+
const SOURCE_FOR: Readonly<Record<string, readonly string[]>> = {
|
|
53
|
+
".js": [".ts", ".tsx"],
|
|
54
|
+
".jsx": [".tsx"],
|
|
55
|
+
".mjs": [".mts"],
|
|
56
|
+
".cjs": [".cts"],
|
|
57
|
+
}
|
|
58
|
+
// The package.json `imports` conditions a server-side bundle of this project matches, in no order:
|
|
59
|
+
// the target object's own key order decides, as in Node.
|
|
60
|
+
const CONDITIONS: ReadonlySet<string> = new Set(["bun", "import", "default"])
|
|
61
|
+
const MAX_EXTENDS = 16
|
|
62
|
+
|
|
63
|
+
interface PathsConfig {
|
|
64
|
+
/** Directory `paths` targets resolve against: `baseUrl` when set, else the defining config's. */
|
|
65
|
+
readonly pathsBase: string
|
|
66
|
+
readonly paths: ReadonlyArray<readonly [string, readonly string[]]>
|
|
67
|
+
readonly baseUrl: string | undefined
|
|
68
|
+
/** The {@link IMPORT_EMIT_OPTIONS} the chain sets, later configs winning; `undefined` when part of
|
|
69
|
+
* the chain could not be read. */
|
|
70
|
+
readonly importEmit: Readonly<Record<string, unknown>> | undefined
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// The options that decide whether an import left with only type specifiers is emitted. The last two
|
|
74
|
+
// are removed in TypeScript 6 but still read by older compilers and bundlers.
|
|
75
|
+
const IMPORT_EMIT_OPTIONS = [
|
|
76
|
+
"verbatimModuleSyntax",
|
|
77
|
+
"importsNotUsedAsValues",
|
|
78
|
+
"preserveValueImports",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
/** JSON with comments and trailing commas, as tsconfig files are written. Throws on anything else. */
|
|
82
|
+
export function parseJsonc(text: string): unknown {
|
|
83
|
+
let code = ""
|
|
84
|
+
for (let i = 0; i < text.length; ) {
|
|
85
|
+
const ch = text[i]
|
|
86
|
+
if (ch === '"') {
|
|
87
|
+
let end = i + 1
|
|
88
|
+
while (end < text.length && text[end] !== '"') end += text[end] === "\\" ? 2 : 1
|
|
89
|
+
code += text.slice(i, end + 1)
|
|
90
|
+
i = end + 1
|
|
91
|
+
} else if (ch === "/" && text[i + 1] === "/") {
|
|
92
|
+
const newline = text.indexOf("\n", i)
|
|
93
|
+
i = newline === -1 ? text.length : newline
|
|
94
|
+
} else if (ch === "/" && text[i + 1] === "*") {
|
|
95
|
+
const close = text.indexOf("*/", i + 2)
|
|
96
|
+
i = close === -1 ? text.length : close + 2
|
|
97
|
+
} else {
|
|
98
|
+
code += ch
|
|
99
|
+
i += 1
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let json = ""
|
|
103
|
+
for (let i = 0; i < code.length; i += 1) {
|
|
104
|
+
const ch = code[i]
|
|
105
|
+
if (ch === '"') {
|
|
106
|
+
let end = i + 1
|
|
107
|
+
while (end < code.length && code[end] !== '"') end += code[end] === "\\" ? 2 : 1
|
|
108
|
+
json += code.slice(i, end + 1)
|
|
109
|
+
i = end
|
|
110
|
+
} else if (ch === "," && /^\s*[}\]]/.test(code.slice(i + 1))) {
|
|
111
|
+
} else json += ch
|
|
112
|
+
}
|
|
113
|
+
return JSON.parse(json.replace(/^\uFEFF/, ""))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const record = (value: unknown): Readonly<Record<string, unknown>> | undefined =>
|
|
117
|
+
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
118
|
+
? Object.fromEntries(Object.entries(value))
|
|
119
|
+
: undefined
|
|
120
|
+
|
|
121
|
+
/** Whether `path` is a file. A path through a file (`a.ts/index.ts`) is none, not an error. */
|
|
122
|
+
export const isFile = (path: string): boolean => {
|
|
123
|
+
try {
|
|
124
|
+
return statSync(path, { throwIfNoEntry: false })?.isFile() ?? false
|
|
125
|
+
} catch {
|
|
126
|
+
return false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const real = (path: string): string => {
|
|
131
|
+
try {
|
|
132
|
+
return realpathSync(path)
|
|
133
|
+
} catch {
|
|
134
|
+
return path
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** The file a path names: as written, its source for a `.js` name, with an extension, or an index. */
|
|
139
|
+
function probe(base: string, exists: (path: string) => boolean): string | undefined {
|
|
140
|
+
if (exists(base)) return base
|
|
141
|
+
const ext = extname(base)
|
|
142
|
+
for (const source of SOURCE_FOR[ext] ?? []) {
|
|
143
|
+
const candidate = `${base.slice(0, -ext.length)}${source}`
|
|
144
|
+
if (exists(candidate)) return candidate
|
|
145
|
+
}
|
|
146
|
+
for (const extension of EXTENSIONS)
|
|
147
|
+
if (exists(`${base}${extension}`)) return `${base}${extension}`
|
|
148
|
+
for (const extension of EXTENSIONS) {
|
|
149
|
+
const index = join(base, `index${extension}`)
|
|
150
|
+
if (exists(index)) return index
|
|
151
|
+
}
|
|
152
|
+
return undefined
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `file` written the way `written` names `found`: as is, without its extension, as its folder when
|
|
157
|
+
* `found` is an index, or with the `.js` name of a `.ts` source.
|
|
158
|
+
*/
|
|
159
|
+
function sameStyle(written: string, found: string, file: string): string {
|
|
160
|
+
if (written === found) return file
|
|
161
|
+
const ext = extname(file)
|
|
162
|
+
const bare = file.slice(0, file.length - ext.length)
|
|
163
|
+
if (found.startsWith(`${written}/index.`) || found.startsWith(`${written}\\index.`))
|
|
164
|
+
return basename(file).startsWith("index.") ? dirname(file) : bare
|
|
165
|
+
if (found.startsWith(written)) return bare
|
|
166
|
+
const writtenExt = extname(written)
|
|
167
|
+
return SOURCE_FOR[writtenExt] === undefined ? file : `${bare}${writtenExt}`
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Longest-prefix `*` match, TypeScript's rule for `paths` and Node's for `imports`. */
|
|
171
|
+
function matchPattern(
|
|
172
|
+
patterns: readonly string[],
|
|
173
|
+
specifier: string,
|
|
174
|
+
): { readonly pattern: string; readonly star: string | undefined } | undefined {
|
|
175
|
+
if (patterns.includes(specifier)) return { pattern: specifier, star: undefined }
|
|
176
|
+
let best: { pattern: string; star: string } | undefined
|
|
177
|
+
for (const pattern of patterns) {
|
|
178
|
+
const at = pattern.indexOf("*")
|
|
179
|
+
if (at === -1 || pattern.indexOf("*", at + 1) !== -1) continue
|
|
180
|
+
const prefix = pattern.slice(0, at)
|
|
181
|
+
const suffix = pattern.slice(at + 1)
|
|
182
|
+
if (specifier.length < prefix.length + suffix.length) continue
|
|
183
|
+
if (!specifier.startsWith(prefix) || !specifier.endsWith(suffix)) continue
|
|
184
|
+
if (best !== undefined && prefix.length <= best.pattern.indexOf("*")) continue
|
|
185
|
+
best = { pattern, star: specifier.slice(prefix.length, specifier.length - suffix.length) }
|
|
186
|
+
}
|
|
187
|
+
return best
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface SpecifierResolverOptions {
|
|
191
|
+
/** Whether a module file exists, for resolving against a planned layout rather than the disk.
|
|
192
|
+
* Config files (tsconfig, package.json) are still read from the disk. */
|
|
193
|
+
readonly moduleExists?: (path: string) => boolean
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function createSpecifierResolver(options: SpecifierResolverOptions = {}): ProjectResolver {
|
|
197
|
+
const exists = options.moduleExists ?? isFile
|
|
198
|
+
const configs = new Map<string, PathsConfig | undefined>()
|
|
199
|
+
const tsconfigByDir = new Map<string, string | undefined>()
|
|
200
|
+
const manifests = new Map<string, Readonly<Record<string, unknown>> | undefined>()
|
|
201
|
+
const packageByDir = new Map<string, string | undefined>()
|
|
202
|
+
|
|
203
|
+
const readJsonc = (path: string): Readonly<Record<string, unknown>> | undefined => {
|
|
204
|
+
try {
|
|
205
|
+
return record(parseJsonc(readFileSync(path, "utf8")))
|
|
206
|
+
} catch {
|
|
207
|
+
return undefined
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** The nearest `tsconfig.json` (or `jsconfig.json`) at or above `dir`, as Bun's resolver picks it. */
|
|
212
|
+
const tsconfigFor = (dir: string): string | undefined => {
|
|
213
|
+
if (tsconfigByDir.has(dir)) return tsconfigByDir.get(dir)
|
|
214
|
+
const own = ["tsconfig.json", "jsconfig.json"].map((name) => join(dir, name)).find(isFile)
|
|
215
|
+
const parent = dirname(dir)
|
|
216
|
+
const found = own ?? (parent === dir ? undefined : tsconfigFor(parent))
|
|
217
|
+
tsconfigByDir.set(dir, found)
|
|
218
|
+
return found
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** An `extends` entry's file: a path relative to the config, or a package found in `node_modules`. */
|
|
222
|
+
const extendedConfig = (from: string, name: string): string | undefined => {
|
|
223
|
+
const withJson = (path: string): string | undefined =>
|
|
224
|
+
isFile(path) ? path : isFile(`${path}.json`) ? `${path}.json` : undefined
|
|
225
|
+
if (name.startsWith("./") || name.startsWith("../") || isAbsolute(name))
|
|
226
|
+
return withJson(join(dirname(from), name))
|
|
227
|
+
for (let dir = dirname(from); ; dir = dirname(dir)) {
|
|
228
|
+
const target = join(dir, "node_modules", name)
|
|
229
|
+
const direct = withJson(target)
|
|
230
|
+
if (direct !== undefined) return direct
|
|
231
|
+
const manifest = readJsonc(join(target, "package.json"))
|
|
232
|
+
const field = manifest?.tsconfig
|
|
233
|
+
const nested =
|
|
234
|
+
typeof field === "string"
|
|
235
|
+
? withJson(join(target, field))
|
|
236
|
+
: withJson(join(target, "tsconfig"))
|
|
237
|
+
if (nested !== undefined) return nested
|
|
238
|
+
if (dirname(dir) === dir) return undefined
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** The `paths`, `baseUrl` and import-emit options a config ends up with after its `extends` chain. */
|
|
243
|
+
const pathsConfig = (
|
|
244
|
+
file: string,
|
|
245
|
+
seen: ReadonlySet<string> = new Set(),
|
|
246
|
+
): PathsConfig | undefined => {
|
|
247
|
+
if (configs.has(file)) return configs.get(file)
|
|
248
|
+
if (seen.has(file) || seen.size >= MAX_EXTENDS) return undefined
|
|
249
|
+
const config = readJsonc(file)
|
|
250
|
+
let inherited: PathsConfig | undefined
|
|
251
|
+
let importEmit: Record<string, unknown> | undefined = {}
|
|
252
|
+
const extended = config?.extends
|
|
253
|
+
for (const name of typeof extended === "string"
|
|
254
|
+
? [extended]
|
|
255
|
+
: Array.isArray(extended)
|
|
256
|
+
? extended
|
|
257
|
+
: []) {
|
|
258
|
+
if (typeof name !== "string") continue
|
|
259
|
+
const base = extendedConfig(file, name)
|
|
260
|
+
const parent = base === undefined ? undefined : pathsConfig(base, new Set([...seen, file]))
|
|
261
|
+
importEmit =
|
|
262
|
+
importEmit === undefined || parent?.importEmit === undefined
|
|
263
|
+
? undefined
|
|
264
|
+
: { ...importEmit, ...parent.importEmit }
|
|
265
|
+
if (parent === undefined) continue
|
|
266
|
+
inherited = {
|
|
267
|
+
paths: parent.paths.length > 0 ? parent.paths : (inherited?.paths ?? []),
|
|
268
|
+
baseUrl: parent.baseUrl ?? inherited?.baseUrl,
|
|
269
|
+
pathsBase:
|
|
270
|
+
parent.paths.length > 0 ? parent.pathsBase : (inherited?.pathsBase ?? parent.pathsBase),
|
|
271
|
+
importEmit,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
const options = record(config?.compilerOptions)
|
|
275
|
+
if (importEmit !== undefined) {
|
|
276
|
+
for (const key of IMPORT_EMIT_OPTIONS)
|
|
277
|
+
if (options?.[key] !== undefined) importEmit = { ...importEmit, [key]: options[key] }
|
|
278
|
+
}
|
|
279
|
+
const baseUrl =
|
|
280
|
+
typeof options?.baseUrl === "string"
|
|
281
|
+
? join(dirname(file), options.baseUrl)
|
|
282
|
+
: inherited?.baseUrl
|
|
283
|
+
const ownPaths = record(options?.paths)
|
|
284
|
+
const paths =
|
|
285
|
+
ownPaths === undefined
|
|
286
|
+
? (inherited?.paths ?? [])
|
|
287
|
+
: Object.entries(ownPaths).map(
|
|
288
|
+
([pattern, targets]) =>
|
|
289
|
+
[
|
|
290
|
+
pattern,
|
|
291
|
+
Array.isArray(targets)
|
|
292
|
+
? targets.filter((t): t is string => typeof t === "string")
|
|
293
|
+
: [],
|
|
294
|
+
] as const,
|
|
295
|
+
)
|
|
296
|
+
const pathsBase =
|
|
297
|
+
baseUrl ?? (ownPaths === undefined ? (inherited?.pathsBase ?? dirname(file)) : dirname(file))
|
|
298
|
+
const resolved = config === undefined ? undefined : { paths, baseUrl, pathsBase, importEmit }
|
|
299
|
+
configs.set(file, resolved)
|
|
300
|
+
return resolved
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Keyed by config, not directory: every package name reaches here, from every folder of the project.
|
|
304
|
+
const aliases = new Map<string, string | undefined>()
|
|
305
|
+
const viaTsconfig = (fromFile: string, specifier: string): string | undefined => {
|
|
306
|
+
const tsconfig = tsconfigFor(dirname(fromFile))
|
|
307
|
+
if (tsconfig === undefined) return undefined
|
|
308
|
+
const key = `${tsconfig}\0${specifier}`
|
|
309
|
+
if (aliases.has(key)) return aliases.get(key)
|
|
310
|
+
const found = aliasTarget(tsconfig, specifier)
|
|
311
|
+
aliases.set(key, found)
|
|
312
|
+
return found
|
|
313
|
+
}
|
|
314
|
+
const aliasTarget = (tsconfig: string, specifier: string): string | undefined =>
|
|
315
|
+
pathsRule(tsconfig, specifier)?.found
|
|
316
|
+
|
|
317
|
+
/** The nearest package.json at or above `dir`: the package scope Node reads `imports` from. */
|
|
318
|
+
const packageFor = (dir: string): string | undefined => {
|
|
319
|
+
if (packageByDir.has(dir)) return packageByDir.get(dir)
|
|
320
|
+
const own = join(dir, "package.json")
|
|
321
|
+
const parent = dirname(dir)
|
|
322
|
+
const found = isFile(own) ? own : parent === dir ? undefined : packageFor(parent)
|
|
323
|
+
packageByDir.set(dir, found)
|
|
324
|
+
return found
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const importTarget = (target: unknown, star: string | undefined): string | undefined => {
|
|
328
|
+
if (typeof target === "string")
|
|
329
|
+
return target.startsWith("./") ? target.replaceAll("*", star ?? "") : undefined
|
|
330
|
+
if (Array.isArray(target)) {
|
|
331
|
+
for (const entry of target) {
|
|
332
|
+
const found = importTarget(entry, star)
|
|
333
|
+
if (found !== undefined) return found
|
|
334
|
+
}
|
|
335
|
+
return undefined
|
|
336
|
+
}
|
|
337
|
+
const conditions = record(target)
|
|
338
|
+
if (conditions === undefined) return undefined
|
|
339
|
+
for (const [condition, value] of Object.entries(conditions)) {
|
|
340
|
+
if (CONDITIONS.has(condition)) return importTarget(value, star)
|
|
341
|
+
}
|
|
342
|
+
return undefined
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const importsRule = (fromFile: string, specifier: string): AliasRule | undefined => {
|
|
346
|
+
const manifestPath = packageFor(dirname(fromFile))
|
|
347
|
+
if (manifestPath === undefined) return undefined
|
|
348
|
+
if (!manifests.has(manifestPath)) manifests.set(manifestPath, readJsonc(manifestPath))
|
|
349
|
+
const imports = record(manifests.get(manifestPath)?.imports)
|
|
350
|
+
if (imports === undefined) return undefined
|
|
351
|
+
const keys = Object.keys(imports)
|
|
352
|
+
const match = matchPattern(keys, specifier)
|
|
353
|
+
if (match === undefined) return undefined
|
|
354
|
+
const template = importTarget(imports[match.pattern], STAR)
|
|
355
|
+
if (template === undefined) return undefined
|
|
356
|
+
const base = dirname(manifestPath)
|
|
357
|
+
const written = join(base, template.replaceAll(STAR, match.star ?? ""))
|
|
358
|
+
const found = probe(written, exists)
|
|
359
|
+
return found === undefined
|
|
360
|
+
? undefined
|
|
361
|
+
: { key: match.pattern, keys, base, templates: [template], written, found }
|
|
362
|
+
}
|
|
363
|
+
const viaImports = (fromFile: string, specifier: string): string | undefined =>
|
|
364
|
+
importsRule(fromFile, specifier)?.found
|
|
365
|
+
|
|
366
|
+
const tsconfigRule = (fromFile: string, specifier: string): AliasRule | undefined => {
|
|
367
|
+
const tsconfig = tsconfigFor(dirname(fromFile))
|
|
368
|
+
return tsconfig === undefined ? undefined : pathsRule(tsconfig, specifier)
|
|
369
|
+
}
|
|
370
|
+
const pathsRule = (tsconfig: string, specifier: string): AliasRule | undefined => {
|
|
371
|
+
const config = pathsConfig(tsconfig)
|
|
372
|
+
if (config === undefined) return undefined
|
|
373
|
+
const keys = config.paths.map(([pattern]) => pattern)
|
|
374
|
+
const match = matchPattern(keys, specifier)
|
|
375
|
+
if (match === undefined) {
|
|
376
|
+
if (config.baseUrl === undefined) return undefined
|
|
377
|
+
const written = join(config.baseUrl, specifier)
|
|
378
|
+
const found = probe(written, exists)
|
|
379
|
+
return found === undefined
|
|
380
|
+
? undefined
|
|
381
|
+
: { key: undefined, keys, base: config.baseUrl, templates: [STAR], written, found }
|
|
382
|
+
}
|
|
383
|
+
// TypeScript permits at most one wildcard in each substitution, as in the alias key.
|
|
384
|
+
const targets = (config.paths.find(([pattern]) => pattern === match.pattern)?.[1] ?? []).filter(
|
|
385
|
+
(target) => target.indexOf("*") === target.lastIndexOf("*"),
|
|
386
|
+
)
|
|
387
|
+
for (const target of targets) {
|
|
388
|
+
const written = join(config.pathsBase, target.replaceAll("*", match.star ?? ""))
|
|
389
|
+
const found = probe(written, exists)
|
|
390
|
+
if (found === undefined) continue
|
|
391
|
+
const templates = targets.map((t) => t.replaceAll("*", STAR))
|
|
392
|
+
return { key: match.pattern, keys, base: config.pathsBase, templates, written, found }
|
|
393
|
+
}
|
|
394
|
+
return undefined
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
const respell = (fromFile: string, specifier: string, file: string): string | undefined => {
|
|
398
|
+
if (/^\.\.?(?:\/|$)/.test(specifier) || isAbsolute(specifier)) return undefined
|
|
399
|
+
const rule =
|
|
400
|
+
(specifier.startsWith("#") ? importsRule(fromFile, specifier) : undefined) ??
|
|
401
|
+
tsconfigRule(fromFile, specifier)
|
|
402
|
+
if (rule === undefined) return undefined
|
|
403
|
+
const spelled = sameStyle(rule.written, rule.found, file)
|
|
404
|
+
for (const template of rule.templates) {
|
|
405
|
+
const at = template.indexOf(STAR)
|
|
406
|
+
if (at === -1) continue
|
|
407
|
+
// An empty head still ends at a folder boundary: `src2/x` is not under a `baseUrl` of `src`.
|
|
408
|
+
const prefix = join(rule.base, template.slice(0, at) || "./")
|
|
409
|
+
const suffix = template.slice(at + STAR.length)
|
|
410
|
+
if (!spelled.startsWith(prefix) || !spelled.endsWith(suffix)) continue
|
|
411
|
+
if (spelled.length < prefix.length + suffix.length) continue
|
|
412
|
+
const star = spelled
|
|
413
|
+
.slice(prefix.length, spelled.length - suffix.length)
|
|
414
|
+
.replaceAll("\\", "/")
|
|
415
|
+
if (star === "" || star.startsWith("/")) continue
|
|
416
|
+
const keyAt = rule.key?.indexOf("*") ?? 0
|
|
417
|
+
if (keyAt === -1) continue
|
|
418
|
+
const next =
|
|
419
|
+
rule.key === undefined
|
|
420
|
+
? star
|
|
421
|
+
: `${rule.key.slice(0, keyAt)}${star}${rule.key.slice(keyAt + 1)}`
|
|
422
|
+
// The rewritten name must take the same route: another, longer pattern would claim it.
|
|
423
|
+
if (matchPattern(rule.keys, next)?.pattern !== rule.key) continue
|
|
424
|
+
return next
|
|
425
|
+
}
|
|
426
|
+
return undefined
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const resolved = new Map<string, string | undefined>()
|
|
430
|
+
const resolve: SpecifierResolver = (fromFile, specifier) => {
|
|
431
|
+
// Every rule below reads only the importing file's directory, so the answer is the directory's.
|
|
432
|
+
const key = `${dirname(fromFile)}\0${specifier}`
|
|
433
|
+
if (resolved.has(key)) return resolved.get(key)
|
|
434
|
+
const path = resolveFrom(fromFile, specifier)
|
|
435
|
+
resolved.set(key, path)
|
|
436
|
+
return path
|
|
437
|
+
}
|
|
438
|
+
const pathPatterns = (dir: string): readonly string[] => {
|
|
439
|
+
const tsconfig = tsconfigFor(dir)
|
|
440
|
+
const config = tsconfig === undefined ? undefined : pathsConfig(tsconfig)
|
|
441
|
+
return (config?.paths ?? []).map(([pattern]) => pattern)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const keepsTypeOnlyImports = (file: string): boolean => {
|
|
445
|
+
const tsconfig = tsconfigFor(dirname(file))
|
|
446
|
+
if (tsconfig === undefined) return false
|
|
447
|
+
const emit = pathsConfig(tsconfig)?.importEmit
|
|
448
|
+
if (emit === undefined) return true
|
|
449
|
+
return (
|
|
450
|
+
emit.verbatimModuleSyntax === true ||
|
|
451
|
+
emit.preserveValueImports === true ||
|
|
452
|
+
emit.importsNotUsedAsValues === "preserve" ||
|
|
453
|
+
emit.importsNotUsedAsValues === "error"
|
|
454
|
+
)
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return Object.assign(resolve, { respell, pathPatterns, keepsTypeOnlyImports })
|
|
458
|
+
|
|
459
|
+
function resolveFrom(fromFile: string, specifier: string): string | undefined {
|
|
460
|
+
let found: string | undefined
|
|
461
|
+
if (
|
|
462
|
+
specifier.startsWith("./") ||
|
|
463
|
+
specifier.startsWith("../") ||
|
|
464
|
+
specifier === "." ||
|
|
465
|
+
specifier === ".."
|
|
466
|
+
)
|
|
467
|
+
found = probe(join(dirname(fromFile), specifier), exists)
|
|
468
|
+
else if (isAbsolute(specifier)) found = probe(specifier, exists)
|
|
469
|
+
// A `#` name is a package.json import first; a tsconfig may still map it.
|
|
470
|
+
else if (specifier.startsWith("#"))
|
|
471
|
+
found = viaImports(fromFile, specifier) ?? viaTsconfig(fromFile, specifier)
|
|
472
|
+
else found = viaTsconfig(fromFile, specifier)
|
|
473
|
+
if (found === undefined) return undefined
|
|
474
|
+
const path = real(found)
|
|
475
|
+
return /[\\/]node_modules[\\/]/.test(path) ? undefined : path
|
|
476
|
+
}
|
|
477
|
+
}
|
|
@@ -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
|
}
|