.wrn keeps resolution priority so existing components are unaffected when a .tsx file shares their name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
87 lines
2.5 KiB
TypeScript
87 lines
2.5 KiB
TypeScript
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
import { dirname, extname, join, resolve } from "node:path";
|
|
import type { StructuredImportDecl } from "@wrnexus/syntax";
|
|
|
|
export type ImportMode = "legacy" | "compatible" | "explicit";
|
|
export interface ImportResolverOptions {
|
|
appRoot: string;
|
|
mode?: ImportMode;
|
|
aliases?: Record<string, string>;
|
|
}
|
|
export interface ResolvedImport {
|
|
declaration: StructuredImportDecl;
|
|
resolved?: string;
|
|
/** `.tsx` imports are React islands, not `.wrn` components. */
|
|
kind?: "island";
|
|
diagnostic?: { code: string; message: string; severity: "error" | "warning" };
|
|
}
|
|
|
|
function candidates(path: string): string[] {
|
|
return extname(path)
|
|
? [path]
|
|
: [
|
|
path,
|
|
`${path}.wrn`,
|
|
`${path}.ts`,
|
|
`${path}.tsx`,
|
|
`${path}.d.ts`,
|
|
join(path, "index.wrn"),
|
|
join(path, "index.ts"),
|
|
join(path, "index.tsx"),
|
|
];
|
|
}
|
|
|
|
export function resolveWrnImport(
|
|
declaration: StructuredImportDecl,
|
|
importer: string,
|
|
options: ImportResolverOptions,
|
|
): ResolvedImport {
|
|
const source = declaration.source;
|
|
const aliases: Record<string, string> = {
|
|
"@": "./app",
|
|
...(options.aliases ?? {}),
|
|
};
|
|
const alias = Object.keys(aliases)
|
|
.filter((key) => key.length > 0 && (source === key || source.startsWith(`${key}/`)))
|
|
.sort((left, right) => right.length - left.length)[0];
|
|
if (!source.startsWith(".") && !alias) return { declaration, resolved: source };
|
|
const base = alias
|
|
? resolve(
|
|
options.appRoot,
|
|
aliases[alias]!,
|
|
source === alias ? "" : source.slice(alias.length + 1),
|
|
)
|
|
: resolve(dirname(importer), source);
|
|
const found = candidates(base).find((candidate) => {
|
|
if (!existsSync(candidate)) return false;
|
|
try {
|
|
return statSync(candidate).isFile();
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
if (found) {
|
|
const resolved = realpathSync(found);
|
|
return resolved.endsWith(".tsx")
|
|
? { declaration, resolved, kind: "island" as const }
|
|
: { declaration, resolved };
|
|
}
|
|
const severity = (options.mode ?? "compatible") === "explicit" ? "error" : "warning";
|
|
return {
|
|
declaration,
|
|
diagnostic: {
|
|
code: "WRN-IMPORT-NOT-FOUND",
|
|
message: `Cannot resolve import '${source}' from ${importer}`,
|
|
severity,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function resolveWrnImports(
|
|
declarations: StructuredImportDecl[],
|
|
importer: string,
|
|
options: ImportResolverOptions,
|
|
): ResolvedImport[] {
|
|
return declarations.map((declaration) => resolveWrnImport(declaration, importer, options));
|
|
}
|