@nifrajs/cli 5.0.0 → 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 +7 -2
- package/dist/assure-hydration.d.ts.map +1 -1
- package/dist/assure-hydration.js +4 -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 +24 -7
- package/dist/capabilities-tool.js.map +1 -1
- package/dist/check-diagnostics.d.ts +2 -0
- package/dist/check-diagnostics.d.ts.map +1 -1
- package/dist/check-diagnostics.js.map +1 -1
- package/dist/check-scan.d.ts +8 -2
- package/dist/check-scan.d.ts.map +1 -1
- package/dist/check-scan.js +43 -10
- package/dist/check-scan.js.map +1 -1
- package/dist/check.d.ts.map +1 -1
- package/dist/check.js +39 -33
- package/dist/check.js.map +1 -1
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +106 -48
- package/dist/cli.js.map +1 -1
- package/dist/client-module.d.ts +5 -3
- package/dist/client-module.d.ts.map +1 -1
- package/dist/client-module.js +34 -10
- package/dist/client-module.js.map +1 -1
- package/dist/command-catalog.d.ts +9 -1
- package/dist/command-catalog.d.ts.map +1 -1
- package/dist/command-catalog.js +67 -22
- package/dist/command-catalog.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/fix-recipes.d.ts +2 -0
- package/dist/fix-recipes.d.ts.map +1 -1
- package/dist/fix-recipes.js +93 -12
- package/dist/fix-recipes.js.map +1 -1
- package/dist/init-agents.d.ts +16 -0
- package/dist/init-agents.d.ts.map +1 -1
- package/dist/init-agents.js +3 -2
- 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/project-specifiers.d.ts +14 -1
- package/dist/internal/project-specifiers.d.ts.map +1 -1
- package/dist/internal/project-specifiers.js +54 -15
- package/dist/internal/project-specifiers.js.map +1 -1
- package/dist/internal/source-facts.d.ts +6 -1
- package/dist/internal/source-facts.d.ts.map +1 -1
- package/dist/internal/source-facts.js +11 -5
- package/dist/internal/source-facts.js.map +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 +9 -0
- package/dist/introspect.d.ts.map +1 -1
- package/dist/introspect.js +32 -1
- package/dist/introspect.js.map +1 -1
- package/dist/levels-tool.d.ts.map +1 -1
- package/dist/levels-tool.js +5 -8
- package/dist/levels-tool.js.map +1 -1
- package/dist/load.d.ts +2 -2
- package/dist/load.d.ts.map +1 -1
- package/dist/load.js +23 -3
- package/dist/load.js.map +1 -1
- package/dist/mcp-context.d.ts.map +1 -1
- package/dist/mcp-context.js +6 -21
- 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 +1 -0
- package/dist/mcp-exec.d.ts.map +1 -1
- package/dist/mcp-exec.js +8 -3
- package/dist/mcp-exec.js.map +1 -1
- package/dist/mcp-http.js +1 -1
- package/dist/mcp-io.d.ts +13 -0
- package/dist/mcp-io.d.ts.map +1 -1
- package/dist/mcp-io.js +23 -0
- package/dist/mcp-io.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.d.ts.map +1 -1
- package/dist/mcp.js +40 -17
- package/dist/mcp.js.map +1 -1
- package/dist/migrate-layout.d.ts +40 -1
- package/dist/migrate-layout.d.ts.map +1 -1
- package/dist/migrate-layout.js +1316 -228
- package/dist/migrate-layout.js.map +1 -1
- package/dist/output-schemas.d.ts +34 -0
- package/dist/output-schemas.d.ts.map +1 -1
- package/dist/output-schemas.js +132 -21
- package/dist/output-schemas.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/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/review.d.ts.map +1 -1
- package/dist/review.js +5 -0
- package/dist/review.js.map +1 -1
- package/dist/rules/codes.d.ts +3 -1
- package/dist/rules/codes.d.ts.map +1 -1
- package/dist/rules/codes.js +3 -1
- package/dist/rules/codes.js.map +1 -1
- package/dist/rules/data-guard.d.ts +6 -0
- package/dist/rules/data-guard.d.ts.map +1 -1
- package/dist/rules/data-guard.js +37 -0
- package/dist/rules/data-guard.js.map +1 -1
- package/dist/rules/legacy.d.ts.map +1 -1
- package/dist/rules/legacy.js +20 -1
- package/dist/rules/legacy.js.map +1 -1
- package/dist/rules/pages.d.ts +29 -3
- package/dist/rules/pages.d.ts.map +1 -1
- package/dist/rules/pages.js +236 -22
- package/dist/rules/pages.js.map +1 -1
- package/dist/rules/security.d.ts +3 -2
- package/dist/rules/security.d.ts.map +1 -1
- package/dist/rules/security.js +42 -11
- package/dist/rules/security.js.map +1 -1
- package/dist/rules/zones.d.ts.map +1 -1
- package/dist/rules/zones.js +31 -9
- package/dist/rules/zones.js.map +1 -1
- package/dist/stylex-migrate.d.ts.map +1 -1
- package/dist/stylex-migrate.js +127 -17
- package/dist/stylex-migrate.js.map +1 -1
- package/dist/test-tool.d.ts.map +1 -1
- package/dist/test-tool.js +3 -9
- package/dist/test-tool.js.map +1 -1
- package/dist/upgrade.d.ts +20 -5
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +86 -9
- package/dist/upgrade.js.map +1 -1
- package/dist/verification-plan.d.ts.map +1 -1
- package/dist/verification-plan.js +5 -2
- package/dist/verification-plan.js.map +1 -1
- package/dist/version-drift.d.ts.map +1 -1
- package/dist/version-drift.js +55 -3
- package/dist/version-drift.js.map +1 -1
- package/docs/examples.json +2 -2
- package/docs/llms-full.txt +481 -61
- package/docs/types.json +147 -70
- package/package.json +11 -11
- package/src/assure-hydration.ts +4 -1
- package/src/assure.ts +88 -10
- package/src/capabilities-tool.ts +33 -7
- package/src/check-diagnostics.ts +2 -0
- package/src/check-scan.ts +56 -9
- package/src/check.ts +55 -39
- package/src/cli.ts +104 -35
- package/src/client-module.ts +45 -9
- package/src/command-catalog.ts +77 -21
- package/src/dev-feed-tool.ts +112 -1
- package/src/fix-recipes.ts +101 -10
- package/src/init-agents.ts +4 -3
- package/src/internal/config-paths.ts +154 -0
- package/src/internal/installed-package.ts +15 -0
- package/src/internal/project-specifiers.ts +69 -16
- package/src/internal/source-facts.ts +23 -5
- package/src/internal/typescript-import.ts +5 -3
- package/src/introspect.ts +39 -1
- package/src/levels-tool.ts +7 -9
- package/src/load.ts +29 -3
- package/src/mcp-context.ts +5 -18
- package/src/mcp-delegate.ts +22 -10
- package/src/mcp-exec.ts +9 -2
- package/src/mcp-http.ts +1 -1
- package/src/mcp-io.ts +33 -0
- package/src/mcp-path.ts +8 -22
- package/src/mcp.ts +48 -19
- package/src/migrate-layout.ts +1496 -237
- package/src/output-schemas.ts +179 -23
- package/src/project-path.ts +44 -13
- package/src/release-verification.ts +20 -0
- package/src/review.ts +5 -0
- package/src/rules/codes.ts +3 -1
- package/src/rules/data-guard.ts +40 -0
- package/src/rules/legacy.ts +22 -1
- package/src/rules/pages.ts +266 -24
- package/src/rules/security.ts +49 -11
- package/src/rules/zones.ts +30 -7
- package/src/stylex-migrate.ts +117 -17
- package/src/test-tool.ts +3 -9
- package/src/upgrade.ts +96 -8
- package/src/verification-plan.ts +9 -2
- package/src/version-drift.ts +53 -2
package/src/dev-feed-tool.ts
CHANGED
|
@@ -793,6 +793,117 @@ export const logsSpec: CommandSpec<LogsInput, LogsOutput> = {
|
|
|
793
793
|
json: (out) => out,
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
+
export interface ExplainInput {
|
|
797
|
+
readonly error?: string | undefined
|
|
798
|
+
readonly stack?: string | undefined
|
|
799
|
+
readonly name?: string | undefined
|
|
800
|
+
readonly port?: number | undefined
|
|
801
|
+
readonly dir?: string | undefined
|
|
802
|
+
readonly json?: boolean | undefined
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** A diagnostic, or `NIFRA_NONE` with why there was nothing to explain. */
|
|
806
|
+
export type ExplainOutput =
|
|
807
|
+
| (Diagnostic & { readonly entry?: object; readonly note?: string })
|
|
808
|
+
| { code: "NIFRA_NONE"; message: string }
|
|
809
|
+
|
|
810
|
+
const explained = (out: ExplainOutput): out is Diagnostic => out.code !== "NIFRA_NONE"
|
|
811
|
+
|
|
812
|
+
export const explainSpec: CommandSpec<ExplainInput, ExplainOutput> = {
|
|
813
|
+
name: "explain",
|
|
814
|
+
summary:
|
|
815
|
+
"Turn an error into a structured diagnostic: its code, the frame in your source with a codeframe, and the cause and fix when nifra recognises it. With no message, the running dev server's latest error.",
|
|
816
|
+
input: {
|
|
817
|
+
jsonSchema: {
|
|
818
|
+
type: "object",
|
|
819
|
+
properties: {
|
|
820
|
+
error: { type: "string", description: "The error message to explain." },
|
|
821
|
+
stack: {
|
|
822
|
+
type: "string",
|
|
823
|
+
description: "Its stack trace, for the codeframe and source frame.",
|
|
824
|
+
},
|
|
825
|
+
name: { type: "string", description: "Its class name, such as TypeError." },
|
|
826
|
+
port: SHARED_PROPERTIES.port,
|
|
827
|
+
dir: { type: "string", description: "The app directory." },
|
|
828
|
+
json: { type: "boolean" },
|
|
829
|
+
},
|
|
830
|
+
additionalProperties: false,
|
|
831
|
+
},
|
|
832
|
+
parse: (value) => {
|
|
833
|
+
const raw = fields(value)
|
|
834
|
+
return {
|
|
835
|
+
error: text(raw, "error"),
|
|
836
|
+
stack: text(raw, "stack"),
|
|
837
|
+
name: text(raw, "name"),
|
|
838
|
+
port: count(raw, "port"),
|
|
839
|
+
dir: text(raw, "dir"),
|
|
840
|
+
json: flag(raw, "json"),
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
},
|
|
844
|
+
output: {
|
|
845
|
+
version: 1,
|
|
846
|
+
jsonSchema: {
|
|
847
|
+
type: "object",
|
|
848
|
+
properties: { code: { type: "string" }, message: { type: "string" } },
|
|
849
|
+
required: ["code", "message"],
|
|
850
|
+
},
|
|
851
|
+
parse: (value) => {
|
|
852
|
+
if (isDiagnostic(value)) return value
|
|
853
|
+
if (
|
|
854
|
+
typeof value === "object" &&
|
|
855
|
+
value !== null &&
|
|
856
|
+
"code" in value &&
|
|
857
|
+
value.code === "NIFRA_NONE" &&
|
|
858
|
+
"message" in value &&
|
|
859
|
+
typeof value.message === "string"
|
|
860
|
+
)
|
|
861
|
+
return { code: "NIFRA_NONE", message: value.message }
|
|
862
|
+
throw new TypeError("explain output must be a diagnostic or NIFRA_NONE")
|
|
863
|
+
},
|
|
864
|
+
},
|
|
865
|
+
transports: ["cli"],
|
|
866
|
+
stability: "experimental",
|
|
867
|
+
argv: {
|
|
868
|
+
positionals: ["error"],
|
|
869
|
+
flags: [
|
|
870
|
+
{ name: "stack", field: "stack", type: "string" },
|
|
871
|
+
{ name: "name", field: "name", type: "string" },
|
|
872
|
+
{ name: "port", field: "port", type: "number" },
|
|
873
|
+
{ name: "dir", field: "dir", type: "string" },
|
|
874
|
+
{ name: "json", field: "json", type: "boolean" },
|
|
875
|
+
],
|
|
876
|
+
},
|
|
877
|
+
async run(input, ctx) {
|
|
878
|
+
const root = resolve(ctx.cwd, input.dir ?? ".")
|
|
879
|
+
if (input.error === undefined && input.stack === undefined) {
|
|
880
|
+
return explainLatest(root, input.port, ctx.signal)
|
|
881
|
+
}
|
|
882
|
+
const { buildDiagnostic } = await import("@nifrajs/web/diagnostic")
|
|
883
|
+
const { explainSourceGate } = await import("./mcp-exec.ts")
|
|
884
|
+
const error = new Error(input.error ?? "")
|
|
885
|
+
if (input.name !== undefined) error.name = input.name
|
|
886
|
+
// Without --stack the frames would be this command's own, not the app's.
|
|
887
|
+
error.stack = input.stack ?? `${error.name}: ${error.message}`
|
|
888
|
+
return buildDiagnostic(error, { root, showSource: explainSourceGate(root) })
|
|
889
|
+
},
|
|
890
|
+
render: (out) => {
|
|
891
|
+
if (!explained(out)) return [out.message]
|
|
892
|
+
const at = topFrame(out)
|
|
893
|
+
return [
|
|
894
|
+
`${out.code} ${headline(out)}`,
|
|
895
|
+
...(at === undefined ? [] : [` at ${at}`]),
|
|
896
|
+
...(out.codeframe?.lines ?? []).map(
|
|
897
|
+
(line) => ` ${line.caret ? ">" : " "} ${String(line.number).padStart(4)} | ${line.text}`,
|
|
898
|
+
),
|
|
899
|
+
...(out.cause === undefined ? [] : [` cause: ${out.cause}`]),
|
|
900
|
+
...(out.fix === undefined ? [] : [` fix: ${out.fix}`]),
|
|
901
|
+
]
|
|
902
|
+
},
|
|
903
|
+
exitCode: (out) => (explained(out) ? 0 : 2),
|
|
904
|
+
json: (out) => out,
|
|
905
|
+
}
|
|
906
|
+
|
|
796
907
|
// ---------------------------------------------------------------------------------------------------
|
|
797
908
|
// nifra_explain / nifra_inspect reads
|
|
798
909
|
// ---------------------------------------------------------------------------------------------------
|
|
@@ -802,7 +913,7 @@ export async function explainLatest(
|
|
|
802
913
|
cwd: string,
|
|
803
914
|
port: number | undefined,
|
|
804
915
|
signal?: AbortSignal,
|
|
805
|
-
): Promise<
|
|
916
|
+
): Promise<ExplainOutput> {
|
|
806
917
|
const { findDevServer, describeLookup, readDevFeed } = await import("./dev-server-client.ts")
|
|
807
918
|
const lookup = await findDevServer(cwd, { port }, signal)
|
|
808
919
|
if (lookup.status === "live") {
|
package/src/fix-recipes.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { resolveWorkspaceLinkedPackage } from "./workspace-link.ts"
|
|
|
7
7
|
export interface FixRecipe {
|
|
8
8
|
readonly id: string
|
|
9
9
|
readonly description: string
|
|
10
|
+
/** Run only by `nifra fix --code <code>`: the change it makes is a decision a person reviews first. */
|
|
11
|
+
readonly onlyWhenNamed?: true
|
|
10
12
|
apply(root: string, diagnostic: Diagnostic): Promise<readonly string[]>
|
|
11
13
|
readonly verify: string
|
|
12
14
|
}
|
|
@@ -84,22 +86,39 @@ registerFixRecipe({
|
|
|
84
86
|
description:
|
|
85
87
|
"Serve the backend over HTTP at /api by exporting apiPrefix from the framework config.",
|
|
86
88
|
verify: "nifra check",
|
|
87
|
-
async apply(root
|
|
88
|
-
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
89
|
+
async apply(root) {
|
|
90
|
+
const { CONFIG_FILE, FRAMEWORK_FILE } = await import("./app-files.ts")
|
|
91
|
+
const { apiPrefixFix } = await import("./rules/pages.ts")
|
|
92
|
+
const frameworkPath = await resolveInsideProject(root, FRAMEWORK_FILE)
|
|
93
|
+
const configPath = await resolveInsideProject(root, CONFIG_FILE)
|
|
94
|
+
if (frameworkPath === undefined || configPath === undefined) return []
|
|
95
|
+
const source = async (path: string): Promise<string | undefined> => {
|
|
96
|
+
const file = Bun.file(path)
|
|
97
|
+
return (await file.exists()) ? await file.text() : undefined
|
|
98
|
+
}
|
|
99
|
+
const next = apiPrefixFix({
|
|
100
|
+
framework: await source(frameworkPath),
|
|
101
|
+
config: await source(configPath),
|
|
102
|
+
})
|
|
103
|
+
const edits: readonly (readonly [string, string, string | undefined])[] = [
|
|
104
|
+
[FRAMEWORK_FILE, frameworkPath, next.framework],
|
|
105
|
+
[CONFIG_FILE, configPath, next.config],
|
|
106
|
+
]
|
|
107
|
+
const changed: string[] = []
|
|
108
|
+
for (const [file, path, text] of edits) {
|
|
109
|
+
if (text === undefined) continue
|
|
110
|
+
await writeFile(path, text, "utf8")
|
|
111
|
+
changed.push(file)
|
|
112
|
+
}
|
|
113
|
+
return changed
|
|
97
114
|
},
|
|
98
115
|
})
|
|
99
116
|
|
|
100
117
|
registerFixRecipe({
|
|
101
118
|
id: "contracts.snapshot",
|
|
102
119
|
description: "Refresh the opt-in contract lock after reviewing an intentional route change.",
|
|
120
|
+
// Rewriting the lock accepts whatever the routes changed to, which is the review the lock exists for.
|
|
121
|
+
onlyWhenNamed: true,
|
|
103
122
|
verify: "nifra contracts check",
|
|
104
123
|
async apply(root) {
|
|
105
124
|
const { snapshotContracts } = await import("./contracts.ts")
|
|
@@ -158,6 +177,78 @@ registerFixRecipe({
|
|
|
158
177
|
},
|
|
159
178
|
})
|
|
160
179
|
|
|
180
|
+
registerFixRecipe({
|
|
181
|
+
id: "route.split",
|
|
182
|
+
description:
|
|
183
|
+
"Move a route frontend file's server-only exports, and the code only they use, into its x.backend.ts.",
|
|
184
|
+
verify: "nifra check",
|
|
185
|
+
async apply(root, diagnostic) {
|
|
186
|
+
const file = diagnostic.file
|
|
187
|
+
if (file === undefined || (await resolveInsideProject(root, file)) === undefined) return []
|
|
188
|
+
const { splitRouteFile } = await import("./migrate-layout.ts")
|
|
189
|
+
return splitRouteFile(root, file)
|
|
190
|
+
},
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
registerFixRecipe({
|
|
194
|
+
id: "layout.migrate",
|
|
195
|
+
description:
|
|
196
|
+
"Move the app onto the frontend/backend layout, as `nifra migrate layout --write` does.",
|
|
197
|
+
onlyWhenNamed: true,
|
|
198
|
+
verify: "nifra check --lints-only",
|
|
199
|
+
async apply(root) {
|
|
200
|
+
const { migrateLayout } = await import("./migrate-layout.ts")
|
|
201
|
+
const result = await migrateLayout(root, { write: true })
|
|
202
|
+
return [
|
|
203
|
+
...new Set([
|
|
204
|
+
...result.moves.map((move) => move.to),
|
|
205
|
+
...result.splits.flatMap((split) => [split.file, split.backend]),
|
|
206
|
+
...result.rewritten,
|
|
207
|
+
...result.tooling.map((edit) => edit.file),
|
|
208
|
+
]),
|
|
209
|
+
].sort()
|
|
210
|
+
},
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
registerFixRecipe({
|
|
214
|
+
id: "schemas.draft",
|
|
215
|
+
description:
|
|
216
|
+
"Draft the missing loader or action output schema from the function's return type, for the author to review.",
|
|
217
|
+
verify: "nifra check --lints-only",
|
|
218
|
+
async apply(root, diagnostic) {
|
|
219
|
+
const file = diagnostic.file
|
|
220
|
+
if (file === undefined) return []
|
|
221
|
+
const path = await resolveInsideProject(root, file)
|
|
222
|
+
if (path === undefined) return []
|
|
223
|
+
const schema = diagnostic.evidence
|
|
224
|
+
?.find((line) => line.startsWith("missing: "))
|
|
225
|
+
?.slice("missing: ".length)
|
|
226
|
+
if (schema !== "loaderOutput" && schema !== "actionOutput") return []
|
|
227
|
+
const { planOutputSchemas } = await import("./output-schemas.ts")
|
|
228
|
+
const source = await readFile(path, "utf8")
|
|
229
|
+
// Only this declaration: other schemas in the file, generated or hand-written, stay as they are.
|
|
230
|
+
const plan = await planOutputSchemas(
|
|
231
|
+
root,
|
|
232
|
+
[file],
|
|
233
|
+
(read) => (read === file ? source : undefined),
|
|
234
|
+
{
|
|
235
|
+
draft: (at, name) => at === file && name === schema,
|
|
236
|
+
},
|
|
237
|
+
)
|
|
238
|
+
const blocked =
|
|
239
|
+
plan.warnings.length > 0
|
|
240
|
+
? plan.warnings.join("; ")
|
|
241
|
+
: plan.entries.find((entry) => entry.schema === schema && entry.reason !== undefined)
|
|
242
|
+
?.reason
|
|
243
|
+
if (blocked !== undefined)
|
|
244
|
+
throw new Error(`[nifra] no ${schema} was drafted in ${file}: ${blocked}`)
|
|
245
|
+
const drafted = plan.edits.get(file)
|
|
246
|
+
if (drafted === undefined) return []
|
|
247
|
+
await writeFile(path, drafted, "utf8")
|
|
248
|
+
return [file]
|
|
249
|
+
},
|
|
250
|
+
})
|
|
251
|
+
|
|
161
252
|
/**
|
|
162
253
|
* How many typecheck passes the reserved-segment codemod will run. A nested collision
|
|
163
254
|
* (`/api/delete/then`) only reveals its second site once the first is rewritten, so one pass is not
|
package/src/init-agents.ts
CHANGED
|
@@ -258,7 +258,7 @@ function nameMember(
|
|
|
258
258
|
return { text: rewritten, named }
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
-
interface PinScan {
|
|
261
|
+
export interface PinScan {
|
|
262
262
|
/** The input with every stale pin rewritten - every other character untouched. */
|
|
263
263
|
readonly text: string
|
|
264
264
|
/** Pins found, current or stale. */
|
|
@@ -302,8 +302,9 @@ function repinRegistry(text: string, target: string, member?: string): PinScan {
|
|
|
302
302
|
return named ? { ...scan, text: rewritten, named: member } : scan
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
-
/** Every file that carries the pin, and the part of it --sync-mcp
|
|
306
|
-
|
|
305
|
+
/** Every file that carries the pin, and the part of it --sync-mcp and `nifra upgrade --write` may
|
|
306
|
+
* rewrite. */
|
|
307
|
+
export const PIN_SITES: readonly {
|
|
307
308
|
readonly path: string
|
|
308
309
|
readonly repin: (text: string, target: string, member?: string) => PinScan
|
|
309
310
|
}[] = [
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The module path a nifra config names, read from its source without running it. The loader reads a
|
|
3
|
+
* string from the app root (`"./src/client.tsx"`, or an alias or package name the bundler resolves);
|
|
4
|
+
* `resolve`/`join` from `node:path` of the config's own folder and literal segments, or
|
|
5
|
+
* `fileURLToPath(new URL("x", import.meta.url))`, names a file relative to the config itself.
|
|
6
|
+
*/
|
|
7
|
+
import { posix } from "node:path"
|
|
8
|
+
import type * as TS from "typescript"
|
|
9
|
+
|
|
10
|
+
export interface StaticConfigPath {
|
|
11
|
+
/** `"app"`: read from the app root as the loader reads a string. `"config"`: from the config's folder. */
|
|
12
|
+
readonly base: "app" | "config"
|
|
13
|
+
readonly path: string
|
|
14
|
+
/** The string literal's text, quotes excluded, for an `"app"` path: [start, end). */
|
|
15
|
+
readonly span?: readonly [number, number]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const OWN_DIR = new Set(["import.meta.dir", "import.meta.dirname", "__dirname"])
|
|
19
|
+
const PATH_MODULES = new Set(["node:path", "path", "node:path/posix", "path/posix"])
|
|
20
|
+
const URL_MODULES = new Set(["node:url", "url"])
|
|
21
|
+
|
|
22
|
+
interface PathBindings {
|
|
23
|
+
/** Local names of `resolve` and `join`. */
|
|
24
|
+
readonly calls: ReadonlySet<string>
|
|
25
|
+
/** Default and namespace imports of `node:path`. */
|
|
26
|
+
readonly namespaces: ReadonlySet<string>
|
|
27
|
+
readonly fileURLToPath: ReadonlySet<string>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function pathBindings(ts: typeof TS, sf: TS.SourceFile): PathBindings {
|
|
31
|
+
const calls = new Set<string>()
|
|
32
|
+
const namespaces = new Set<string>()
|
|
33
|
+
const fileURLToPath = new Set<string>()
|
|
34
|
+
for (const statement of sf.statements) {
|
|
35
|
+
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier))
|
|
36
|
+
continue
|
|
37
|
+
const from = statement.moduleSpecifier.text
|
|
38
|
+
const clause = statement.importClause
|
|
39
|
+
if (clause === undefined || clause.isTypeOnly) continue
|
|
40
|
+
const isPath = PATH_MODULES.has(from)
|
|
41
|
+
if (!isPath && !URL_MODULES.has(from)) continue
|
|
42
|
+
if (isPath && clause.name !== undefined) namespaces.add(clause.name.text)
|
|
43
|
+
const bindings = clause.namedBindings
|
|
44
|
+
if (bindings === undefined) continue
|
|
45
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
46
|
+
if (isPath) namespaces.add(bindings.name.text)
|
|
47
|
+
continue
|
|
48
|
+
}
|
|
49
|
+
for (const element of bindings.elements) {
|
|
50
|
+
if (element.isTypeOnly) continue
|
|
51
|
+
const imported = (element.propertyName ?? element.name).text
|
|
52
|
+
if (isPath && (imported === "resolve" || imported === "join")) calls.add(element.name.text)
|
|
53
|
+
else if (!isPath && imported === "fileURLToPath") fileURLToPath.add(element.name.text)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return { calls, namespaces, fileURLToPath }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const unwrap = (ts: typeof TS, node: TS.Expression): TS.Expression => {
|
|
60
|
+
let current = node
|
|
61
|
+
while (
|
|
62
|
+
ts.isAsExpression(current) ||
|
|
63
|
+
ts.isSatisfiesExpression(current) ||
|
|
64
|
+
ts.isParenthesizedExpression(current) ||
|
|
65
|
+
ts.isNonNullExpression(current)
|
|
66
|
+
)
|
|
67
|
+
current = current.expression
|
|
68
|
+
return current
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isLiteral = (
|
|
72
|
+
ts: typeof TS,
|
|
73
|
+
node: TS.Node | undefined,
|
|
74
|
+
): node is TS.StringLiteral | TS.NoSubstitutionTemplateLiteral =>
|
|
75
|
+
node !== undefined && (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node))
|
|
76
|
+
|
|
77
|
+
/** A readable static path, normalized; `undefined` for an absolute one or a URL. */
|
|
78
|
+
function relativeSegments(segments: readonly string[]): string | undefined {
|
|
79
|
+
if (segments.some((segment) => segment.startsWith("/") || /^[a-z][a-z\d+.-]*:/i.test(segment)))
|
|
80
|
+
return undefined
|
|
81
|
+
const path = posix.normalize(posix.join(...segments))
|
|
82
|
+
return path === "." ? undefined : path
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Create a reader for one config file. It answers for an expression the config exports, so a path
|
|
87
|
+
* built any other way (a variable, `process.cwd()`, a template with substitutions) is `undefined`.
|
|
88
|
+
*/
|
|
89
|
+
export function configPathReader(
|
|
90
|
+
ts: typeof TS,
|
|
91
|
+
sf: TS.SourceFile,
|
|
92
|
+
): (node: TS.Expression | undefined) => StaticConfigPath | undefined {
|
|
93
|
+
const bindings = pathBindings(ts, sf)
|
|
94
|
+
const isPathCall = (callee: TS.Expression): boolean =>
|
|
95
|
+
ts.isIdentifier(callee)
|
|
96
|
+
? bindings.calls.has(callee.text)
|
|
97
|
+
: ts.isPropertyAccessExpression(callee) &&
|
|
98
|
+
ts.isIdentifier(callee.expression) &&
|
|
99
|
+
bindings.namespaces.has(callee.expression.text) &&
|
|
100
|
+
(callee.name.text === "resolve" || callee.name.text === "join")
|
|
101
|
+
return (node) => {
|
|
102
|
+
if (node === undefined) return undefined
|
|
103
|
+
const value = unwrap(ts, node)
|
|
104
|
+
if (isLiteral(ts, value))
|
|
105
|
+
return { base: "app", path: value.text, span: [value.getStart(sf) + 1, value.getEnd() - 1] }
|
|
106
|
+
if (!ts.isCallExpression(value)) return undefined
|
|
107
|
+
const [first, ...rest] = value.arguments
|
|
108
|
+
if (isPathCall(value.expression)) {
|
|
109
|
+
if (first === undefined || !OWN_DIR.has(first.getText(sf)) || rest.length === 0)
|
|
110
|
+
return undefined
|
|
111
|
+
const segments: string[] = []
|
|
112
|
+
for (const segment of rest) {
|
|
113
|
+
if (!isLiteral(ts, segment)) return undefined
|
|
114
|
+
segments.push(segment.text)
|
|
115
|
+
}
|
|
116
|
+
const path = relativeSegments(segments)
|
|
117
|
+
return path === undefined ? undefined : { base: "config", path }
|
|
118
|
+
}
|
|
119
|
+
if (
|
|
120
|
+
ts.isIdentifier(value.expression) &&
|
|
121
|
+
bindings.fileURLToPath.has(value.expression.text) &&
|
|
122
|
+
first !== undefined &&
|
|
123
|
+
ts.isNewExpression(first) &&
|
|
124
|
+
first.expression.getText(sf) === "URL" &&
|
|
125
|
+
first.arguments?.length === 2 &&
|
|
126
|
+
first.arguments[1]?.getText(sf) === "import.meta.url" &&
|
|
127
|
+
isLiteral(ts, first.arguments[0])
|
|
128
|
+
) {
|
|
129
|
+
const path = relativeSegments([first.arguments[0].text])
|
|
130
|
+
return path === undefined ? undefined : { base: "config", path }
|
|
131
|
+
}
|
|
132
|
+
return undefined
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The `export const <name> = ...` initializer in a config, or `undefined`. */
|
|
137
|
+
export function exportedInitializer(
|
|
138
|
+
ts: typeof TS,
|
|
139
|
+
sf: TS.SourceFile,
|
|
140
|
+
name: string,
|
|
141
|
+
): TS.Expression | undefined {
|
|
142
|
+
for (const statement of sf.statements) {
|
|
143
|
+
if (
|
|
144
|
+
!ts.isVariableStatement(statement) ||
|
|
145
|
+
!statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)
|
|
146
|
+
)
|
|
147
|
+
continue
|
|
148
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
149
|
+
if (ts.isIdentifier(declaration.name) && declaration.name.text === name)
|
|
150
|
+
return declaration.initializer
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return undefined
|
|
154
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
|
|
4
|
+
/** The manifest of the installed package `name`, as Node finds it: the nearest `node_modules/<name>`
|
|
5
|
+
* at or above the importing file. A workspace link resolves through its symlink. */
|
|
6
|
+
export function installedManifest(fromFile: string, name: string): string | undefined {
|
|
7
|
+
let dir = dirname(fromFile)
|
|
8
|
+
for (;;) {
|
|
9
|
+
const manifest = join(dir, "node_modules", name, "package.json")
|
|
10
|
+
if (existsSync(manifest)) return manifest
|
|
11
|
+
const parent = dirname(dir)
|
|
12
|
+
if (parent === dir) return undefined
|
|
13
|
+
dir = parent
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -22,6 +22,12 @@ export interface ProjectResolver extends SpecifierResolver {
|
|
|
22
22
|
readonly respell: (fromFile: string, specifier: string, file: string) => string | undefined
|
|
23
23
|
/** The `paths` patterns of the nearest tsconfig at or above `dir`, after its `extends` chain. */
|
|
24
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
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
/** How an alias specifier reached its file: what a rewrite through the same alias has to fit. */
|
|
@@ -59,8 +65,19 @@ interface PathsConfig {
|
|
|
59
65
|
readonly pathsBase: string
|
|
60
66
|
readonly paths: ReadonlyArray<readonly [string, readonly string[]]>
|
|
61
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
|
|
62
71
|
}
|
|
63
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
|
+
|
|
64
81
|
/** JSON with comments and trailing commas, as tsconfig files are written. Throws on anything else. */
|
|
65
82
|
export function parseJsonc(text: string): unknown {
|
|
66
83
|
let code = ""
|
|
@@ -101,8 +118,14 @@ const record = (value: unknown): Readonly<Record<string, unknown>> | undefined =
|
|
|
101
118
|
? Object.fromEntries(Object.entries(value))
|
|
102
119
|
: undefined
|
|
103
120
|
|
|
104
|
-
|
|
105
|
-
|
|
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
|
+
}
|
|
106
129
|
|
|
107
130
|
const real = (path: string): string => {
|
|
108
131
|
try {
|
|
@@ -113,18 +136,18 @@ const real = (path: string): string => {
|
|
|
113
136
|
}
|
|
114
137
|
|
|
115
138
|
/** 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 (
|
|
139
|
+
function probe(base: string, exists: (path: string) => boolean): string | undefined {
|
|
140
|
+
if (exists(base)) return base
|
|
118
141
|
const ext = extname(base)
|
|
119
142
|
for (const source of SOURCE_FOR[ext] ?? []) {
|
|
120
143
|
const candidate = `${base.slice(0, -ext.length)}${source}`
|
|
121
|
-
if (
|
|
144
|
+
if (exists(candidate)) return candidate
|
|
122
145
|
}
|
|
123
146
|
for (const extension of EXTENSIONS)
|
|
124
|
-
if (
|
|
147
|
+
if (exists(`${base}${extension}`)) return `${base}${extension}`
|
|
125
148
|
for (const extension of EXTENSIONS) {
|
|
126
149
|
const index = join(base, `index${extension}`)
|
|
127
|
-
if (
|
|
150
|
+
if (exists(index)) return index
|
|
128
151
|
}
|
|
129
152
|
return undefined
|
|
130
153
|
}
|
|
@@ -164,7 +187,14 @@ function matchPattern(
|
|
|
164
187
|
return best
|
|
165
188
|
}
|
|
166
189
|
|
|
167
|
-
export
|
|
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
|
|
168
198
|
const configs = new Map<string, PathsConfig | undefined>()
|
|
169
199
|
const tsconfigByDir = new Map<string, string | undefined>()
|
|
170
200
|
const manifests = new Map<string, Readonly<Record<string, unknown>> | undefined>()
|
|
@@ -209,7 +239,7 @@ export function createSpecifierResolver(): ProjectResolver {
|
|
|
209
239
|
}
|
|
210
240
|
}
|
|
211
241
|
|
|
212
|
-
/** The `paths
|
|
242
|
+
/** The `paths`, `baseUrl` and import-emit options a config ends up with after its `extends` chain. */
|
|
213
243
|
const pathsConfig = (
|
|
214
244
|
file: string,
|
|
215
245
|
seen: ReadonlySet<string> = new Set(),
|
|
@@ -218,6 +248,7 @@ export function createSpecifierResolver(): ProjectResolver {
|
|
|
218
248
|
if (seen.has(file) || seen.size >= MAX_EXTENDS) return undefined
|
|
219
249
|
const config = readJsonc(file)
|
|
220
250
|
let inherited: PathsConfig | undefined
|
|
251
|
+
let importEmit: Record<string, unknown> | undefined = {}
|
|
221
252
|
const extended = config?.extends
|
|
222
253
|
for (const name of typeof extended === "string"
|
|
223
254
|
? [extended]
|
|
@@ -227,15 +258,24 @@ export function createSpecifierResolver(): ProjectResolver {
|
|
|
227
258
|
if (typeof name !== "string") continue
|
|
228
259
|
const base = extendedConfig(file, name)
|
|
229
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 }
|
|
230
265
|
if (parent === undefined) continue
|
|
231
266
|
inherited = {
|
|
232
267
|
paths: parent.paths.length > 0 ? parent.paths : (inherited?.paths ?? []),
|
|
233
268
|
baseUrl: parent.baseUrl ?? inherited?.baseUrl,
|
|
234
269
|
pathsBase:
|
|
235
270
|
parent.paths.length > 0 ? parent.pathsBase : (inherited?.pathsBase ?? parent.pathsBase),
|
|
271
|
+
importEmit,
|
|
236
272
|
}
|
|
237
273
|
}
|
|
238
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
|
+
}
|
|
239
279
|
const baseUrl =
|
|
240
280
|
typeof options?.baseUrl === "string"
|
|
241
281
|
? join(dirname(file), options.baseUrl)
|
|
@@ -255,7 +295,7 @@ export function createSpecifierResolver(): ProjectResolver {
|
|
|
255
295
|
)
|
|
256
296
|
const pathsBase =
|
|
257
297
|
baseUrl ?? (ownPaths === undefined ? (inherited?.pathsBase ?? dirname(file)) : dirname(file))
|
|
258
|
-
const resolved = config === undefined ? undefined : { paths, baseUrl, pathsBase }
|
|
298
|
+
const resolved = config === undefined ? undefined : { paths, baseUrl, pathsBase, importEmit }
|
|
259
299
|
configs.set(file, resolved)
|
|
260
300
|
return resolved
|
|
261
301
|
}
|
|
@@ -315,7 +355,7 @@ export function createSpecifierResolver(): ProjectResolver {
|
|
|
315
355
|
if (template === undefined) return undefined
|
|
316
356
|
const base = dirname(manifestPath)
|
|
317
357
|
const written = join(base, template.replaceAll(STAR, match.star ?? ""))
|
|
318
|
-
const found = probe(written)
|
|
358
|
+
const found = probe(written, exists)
|
|
319
359
|
return found === undefined
|
|
320
360
|
? undefined
|
|
321
361
|
: { key: match.pattern, keys, base, templates: [template], written, found }
|
|
@@ -335,7 +375,7 @@ export function createSpecifierResolver(): ProjectResolver {
|
|
|
335
375
|
if (match === undefined) {
|
|
336
376
|
if (config.baseUrl === undefined) return undefined
|
|
337
377
|
const written = join(config.baseUrl, specifier)
|
|
338
|
-
const found = probe(written)
|
|
378
|
+
const found = probe(written, exists)
|
|
339
379
|
return found === undefined
|
|
340
380
|
? undefined
|
|
341
381
|
: { key: undefined, keys, base: config.baseUrl, templates: [STAR], written, found }
|
|
@@ -346,7 +386,7 @@ export function createSpecifierResolver(): ProjectResolver {
|
|
|
346
386
|
)
|
|
347
387
|
for (const target of targets) {
|
|
348
388
|
const written = join(config.pathsBase, target.replaceAll("*", match.star ?? ""))
|
|
349
|
-
const found = probe(written)
|
|
389
|
+
const found = probe(written, exists)
|
|
350
390
|
if (found === undefined) continue
|
|
351
391
|
const templates = targets.map((t) => t.replaceAll("*", STAR))
|
|
352
392
|
return { key: match.pattern, keys, base: config.pathsBase, templates, written, found }
|
|
@@ -401,7 +441,20 @@ export function createSpecifierResolver(): ProjectResolver {
|
|
|
401
441
|
return (config?.paths ?? []).map(([pattern]) => pattern)
|
|
402
442
|
}
|
|
403
443
|
|
|
404
|
-
|
|
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 })
|
|
405
458
|
|
|
406
459
|
function resolveFrom(fromFile: string, specifier: string): string | undefined {
|
|
407
460
|
let found: string | undefined
|
|
@@ -411,8 +464,8 @@ export function createSpecifierResolver(): ProjectResolver {
|
|
|
411
464
|
specifier === "." ||
|
|
412
465
|
specifier === ".."
|
|
413
466
|
)
|
|
414
|
-
found = probe(join(dirname(fromFile), specifier))
|
|
415
|
-
else if (isAbsolute(specifier)) found = probe(specifier)
|
|
467
|
+
found = probe(join(dirname(fromFile), specifier), exists)
|
|
468
|
+
else if (isAbsolute(specifier)) found = probe(specifier, exists)
|
|
416
469
|
// A `#` name is a package.json import first; a tsconfig may still map it.
|
|
417
470
|
else if (specifier.startsWith("#"))
|
|
418
471
|
found = viaImports(fromFile, specifier) ?? viaTsconfig(fromFile, specifier)
|