From b405025f379b2778ead75cc39f05cdb916a19a98 Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Tue, 18 Aug 2026 15:15:08 +0530 Subject: [PATCH] feat(compiler): resolve .tsx imports and tag them as islands .wrn keeps resolution priority so existing components are unaffected when a .tsx file shares their name. Co-Authored-By: Claude Opus 5 --- packages/compiler/src/import-resolver.ts | 11 +++- .../compiler/test/island-resolution.test.ts | 64 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 packages/compiler/test/island-resolution.test.ts diff --git a/packages/compiler/src/import-resolver.ts b/packages/compiler/src/import-resolver.ts index 12601ff8..711e6063 100644 --- a/packages/compiler/src/import-resolver.ts +++ b/packages/compiler/src/import-resolver.ts @@ -11,6 +11,8 @@ export interface ImportResolverOptions { 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" }; } @@ -21,9 +23,11 @@ function candidates(path: string): string[] { path, `${path}.wrn`, `${path}.ts`, + `${path}.tsx`, `${path}.d.ts`, join(path, "index.wrn"), join(path, "index.ts"), + join(path, "index.tsx"), ]; } @@ -56,7 +60,12 @@ export function resolveWrnImport( return false; } }); - if (found) return { declaration, resolved: realpathSync(found) }; + 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, diff --git a/packages/compiler/test/island-resolution.test.ts b/packages/compiler/test/island-resolution.test.ts new file mode 100644 index 00000000..39a895f3 --- /dev/null +++ b/packages/compiler/test/island-resolution.test.ts @@ -0,0 +1,64 @@ +import { expect, test } from "bun:test"; +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { resolveWrnImport } from "../src/import-resolver.ts"; + +function appWith(files: Record) { + const root = mkdtempSync(join(tmpdir(), "wrnexus-island-")); + mkdirSync(join(root, "app"), { recursive: true }); + for (const [name, contents] of Object.entries(files)) { + writeFileSync(join(root, "app", name), contents); + } + return root; +} + +test("resolves a .tsx import and tags it as an island", () => { + const root = appWith({ "Chart.tsx": "export default function Chart() { return null; }" }); + const result = resolveWrnImport( + { source: "./Chart", specifiers: [] } as any, + join(root, "app", "page.wrn"), + { appRoot: root }, + ); + + expect(result.resolved).toContain("Chart.tsx"); + expect(result.kind).toBe("island"); +}); + +test("does not tag a .ts import as an island", () => { + const root = appWith({ "helper.ts": "export const value = 1;" }); + const result = resolveWrnImport( + { source: "./helper", specifiers: [] } as any, + join(root, "app", "page.wrn"), + { appRoot: root }, + ); + + expect(result.resolved).toContain("helper.ts"); + expect(result.kind).toBeUndefined(); +}); + +test("prefers .wrn over .tsx when both exist", () => { + const root = appWith({ + "Widget.wrn": "", + "Widget.tsx": "export default function Widget() { return null; }", + }); + const result = resolveWrnImport( + { source: "./Widget", specifiers: [] } as any, + join(root, "app", "page.wrn"), + { appRoot: root }, + ); + + expect(result.resolved).toContain("Widget.wrn"); + expect(result.kind).toBeUndefined(); +}); + +test("resolves an explicit .tsx extension as an island", () => { + const root = appWith({ "Chart.tsx": "export default function Chart() { return null; }" }); + const result = resolveWrnImport( + { source: "./Chart.tsx", specifiers: [] } as any, + join(root, "app", "page.wrn"), + { appRoot: root }, + ); + + expect(result.kind).toBe("island"); +});