Skip to content

Commit 49945b4

Browse files
fix: import resolution for type checking.
1 parent c35a0e2 commit 49945b4

3 files changed

Lines changed: 223 additions & 6 deletions

File tree

playwright/diagnostics.spec.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { expect, test } from '@playwright/test'
22
import {
3+
addWorkspaceTab,
34
ensurePanelToolsVisible,
45
ensureDiagnosticsDrawerOpen,
56
getActiveComponentEditorLineNumber,
67
getActiveStylesEditorLineNumber,
8+
setWorkspaceTabSource,
79
runComponentLint,
810
runStylesLint,
911
runTypecheck,
@@ -147,6 +149,76 @@ test('dom mode typecheck resolves @knighted/jsx type-only imports', async ({ pag
147149
expect(diagnosticsText).not.toContain("Cannot find module '@knighted/jsx'")
148150
})
149151

152+
test('typecheck resolves .js import to workspace tsx module tab', async ({ page }) => {
153+
await waitForInitialRender(page)
154+
155+
await ensurePanelToolsVisible(page, 'component')
156+
await addWorkspaceTab(page)
157+
158+
await setWorkspaceTabSource(page, {
159+
fileName: 'module.tsx',
160+
kind: 'component',
161+
source: [
162+
'type ThingProps = { label: string }',
163+
'export const Thing = ({ label }: ThingProps) => <p>{label}</p>',
164+
].join('\n'),
165+
})
166+
167+
await setComponentEditorSource(
168+
page,
169+
[
170+
"import { Thing } from './module.js'",
171+
'const App = () => <Thing label="ok" />',
172+
'',
173+
].join('\n'),
174+
)
175+
176+
await runTypecheck(page)
177+
await ensureDiagnosticsDrawerOpen(page)
178+
await expect(page.locator('#diagnostics-component')).toContainText(
179+
'No TypeScript errors found.',
180+
)
181+
182+
const diagnosticsText = await page.locator('#diagnostics-component').innerText()
183+
expect(diagnosticsText).not.toContain("Cannot find module './module.js'")
184+
})
185+
186+
test('typecheck resolves parent-relative .js import to workspace tsx module tab', async ({
187+
page,
188+
}) => {
189+
await waitForInitialRender(page)
190+
191+
await ensurePanelToolsVisible(page, 'component')
192+
await addWorkspaceTab(page)
193+
194+
await setWorkspaceTabSource(page, {
195+
fileName: 'module.tsx',
196+
kind: 'component',
197+
source: [
198+
'type ThingProps = { label: string }',
199+
'export const Thing = ({ label }: ThingProps) => <p>{label}</p>',
200+
].join('\n'),
201+
})
202+
203+
await setComponentEditorSource(
204+
page,
205+
[
206+
"import { Thing } from '../components/module.js'",
207+
'const App = () => <Thing label="ok" />',
208+
'',
209+
].join('\n'),
210+
)
211+
212+
await runTypecheck(page)
213+
await ensureDiagnosticsDrawerOpen(page)
214+
await expect(page.locator('#diagnostics-component')).toContainText(
215+
'No TypeScript errors found.',
216+
)
217+
218+
const diagnosticsText = await page.locator('#diagnostics-component').innerText()
219+
expect(diagnosticsText).not.toContain("Cannot find module '../components/module.js'")
220+
})
221+
150222
test('component diagnostics rows navigate editor to reported line', async ({ page }) => {
151223
await waitForInitialRender(page)
152224

src/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2386,6 +2386,7 @@ const typeDiagnostics = createTypeDiagnosticsController({
23862386
getTypeScriptLibUrls,
23872387
getTypePackageFileUrls,
23882388
getJsxSource: () => getJsxSource(),
2389+
getWorkspaceTabs: () => buildWorkspaceTabsSnapshot(),
23892390
getRenderMode: () => renderMode.value,
23902391
setTypecheckButtonLoading,
23912392
setTypeDiagnosticsDetails,

src/modules/type-diagnostics.js

Lines changed: 150 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,7 @@ export const createTypeDiagnosticsController = ({
234234
getTypeScriptLibUrls,
235235
getTypePackageFileUrls,
236236
getJsxSource,
237+
getWorkspaceTabs = () => [],
237238
getRenderMode = () => 'dom',
238239
defaultTypeScriptLibFileName = 'lib.esnext.full.d.ts',
239240
setTypecheckButtonLoading,
@@ -248,6 +249,116 @@ export const createTypeDiagnosticsController = ({
248249
getActiveTypeDiagnosticsRuns,
249250
onIssuesDetected = () => {},
250251
}) => {
252+
const styleTabLanguages = new Set(['css', 'less', 'sass', 'module'])
253+
254+
const isStyleWorkspaceTab = tab => {
255+
if (!tab || typeof tab !== 'object') {
256+
return false
257+
}
258+
259+
const language =
260+
typeof tab.language === 'string' ? tab.language.trim().toLowerCase() : ''
261+
if (styleTabLanguages.has(language)) {
262+
return true
263+
}
264+
265+
const path = typeof tab.path === 'string' ? tab.path.trim().toLowerCase() : ''
266+
return /\.(css|less|sass|scss)$/.test(path)
267+
}
268+
269+
const toWorkspaceComponentTabs = () => {
270+
const tabs = typeof getWorkspaceTabs === 'function' ? getWorkspaceTabs() : []
271+
if (!Array.isArray(tabs)) {
272+
return []
273+
}
274+
275+
return tabs
276+
.filter(tab => tab && typeof tab === 'object' && !isStyleWorkspaceTab(tab))
277+
.map(tab => ({
278+
...tab,
279+
path:
280+
typeof tab.path === 'string' && tab.path.trim().length > 0
281+
? normalizeRelativePath(tab.path)
282+
: typeof tab.name === 'string' && tab.name.trim().length > 0
283+
? normalizeRelativePath(tab.name)
284+
: '',
285+
content: typeof tab.content === 'string' ? tab.content : '',
286+
}))
287+
.filter(tab => tab.path.length > 0)
288+
}
289+
290+
const resolveWorkspaceEntryForTypecheck = tabs => {
291+
if (!Array.isArray(tabs) || tabs.length === 0) {
292+
return null
293+
}
294+
295+
return (
296+
tabs.find(tab => tab?.role === 'entry' && typeof tab.path === 'string') ??
297+
tabs.find(tab => tab?.id === 'component' && typeof tab.path === 'string') ??
298+
tabs[0] ??
299+
null
300+
)
301+
}
302+
303+
const toJsCompatibilityCandidates = (specifier, containingFile) => {
304+
if (typeof specifier !== 'string' || !specifier.startsWith('.')) {
305+
return []
306+
}
307+
308+
const containingDirectory = dirname(containingFile)
309+
const resolvedSpecifier = joinPath(containingDirectory, specifier)
310+
const withoutJsExtension = resolvedSpecifier.replace(/\.(mjs|cjs|jsx|js)$/i, '')
311+
312+
if (withoutJsExtension === resolvedSpecifier) {
313+
return []
314+
}
315+
316+
const candidates = [
317+
`${withoutJsExtension}.ts`,
318+
`${withoutJsExtension}.tsx`,
319+
`${withoutJsExtension}.mts`,
320+
`${withoutJsExtension}.cts`,
321+
`${withoutJsExtension}/index.ts`,
322+
`${withoutJsExtension}/index.tsx`,
323+
`${withoutJsExtension}/index.mts`,
324+
`${withoutJsExtension}/index.cts`,
325+
]
326+
327+
return [...new Set(candidates)]
328+
}
329+
330+
const toTypeScriptExtension = (compiler, fileName) => {
331+
if (fileName.endsWith('.tsx')) {
332+
return compiler.Extension?.Tsx ?? '.tsx'
333+
}
334+
335+
if (fileName.endsWith('.ts')) {
336+
return compiler.Extension?.Ts ?? '.ts'
337+
}
338+
339+
if (fileName.endsWith('.mts')) {
340+
return compiler.Extension?.Mts ?? '.mts'
341+
}
342+
343+
if (fileName.endsWith('.cts')) {
344+
return compiler.Extension?.Cts ?? '.cts'
345+
}
346+
347+
if (fileName.endsWith('.d.ts')) {
348+
return compiler.Extension?.Dts ?? '.d.ts'
349+
}
350+
351+
if (fileName.endsWith('.jsx')) {
352+
return compiler.Extension?.Jsx ?? '.jsx'
353+
}
354+
355+
if (fileName.endsWith('.js')) {
356+
return compiler.Extension?.Js ?? '.js'
357+
}
358+
359+
return compiler.Extension?.Ts ?? '.ts'
360+
}
361+
251362
let typeCheckRunId = 0
252363
let typeScriptCompiler = null
253364
let typeScriptCompilerProvider = null
@@ -690,7 +801,10 @@ export const createTypeDiagnosticsController = ({
690801
}
691802

692803
const collectTypeDiagnostics = async (compiler, sourceText) => {
693-
const sourceFileName = 'component.tsx'
804+
const workspaceComponentTabs = toWorkspaceComponentTabs()
805+
const resolvedEntryTab = resolveWorkspaceEntryForTypecheck(workspaceComponentTabs)
806+
const sourceFileName = resolvedEntryTab?.path || 'component.tsx'
807+
const typecheckSourceFileName = sourceFileName.replace(/\.(jsx?|mjs|cjs)$/i, '.tsx')
694808
const jsxTypesFileName = 'knighted-jsx-runtime.d.ts'
695809
const renderMode = getRenderMode()
696810
const isReactMode = renderMode === 'react'
@@ -701,7 +815,15 @@ export const createTypeDiagnosticsController = ({
701815
reactTypes = await ensureReactTypeFiles(compiler)
702816
}
703817

704-
const files = new Map([[sourceFileName, sourceText], ...libFiles.entries()])
818+
const files = new Map(libFiles.entries())
819+
820+
if (workspaceComponentTabs.length > 0) {
821+
for (const tab of workspaceComponentTabs) {
822+
files.set(tab.path, tab.content)
823+
}
824+
}
825+
826+
files.set(typecheckSourceFileName, sourceText)
705827

706828
if (!isReactMode) {
707829
files.set(jsxTypesFileName, domJsxTypes)
@@ -723,6 +845,8 @@ export const createTypeDiagnosticsController = ({
723845
compiler.ModuleResolutionKind?.NodeJs,
724846
types: [],
725847
strict: true,
848+
allowJs: true,
849+
checkJs: false,
726850
noEmit: true,
727851
skipLibCheck: true,
728852
}
@@ -776,6 +900,22 @@ export const createTypeDiagnosticsController = ({
776900
return resolved.resolvedModule
777901
}
778902

903+
const jsCompatibilityCandidates = toJsCompatibilityCandidates(
904+
moduleName,
905+
containingFile,
906+
)
907+
const matchedWorkspaceModule = jsCompatibilityCandidates.find(candidate =>
908+
files.has(candidate),
909+
)
910+
911+
if (matchedWorkspaceModule) {
912+
return {
913+
resolvedFileName: matchedWorkspaceModule,
914+
extension: toTypeScriptExtension(compiler, matchedWorkspaceModule),
915+
isExternalLibraryImport: false,
916+
}
917+
}
918+
779919
if (!reactTypes) {
780920
return undefined
781921
}
@@ -813,9 +953,13 @@ export const createTypeDiagnosticsController = ({
813953

814954
const scriptKind = normalizedFileName.endsWith('.tsx')
815955
? compiler.ScriptKind?.TSX
816-
: normalizedFileName.endsWith('.d.ts')
817-
? compiler.ScriptKind?.TS
818-
: compiler.ScriptKind?.TS
956+
: normalizedFileName.endsWith('.jsx')
957+
? compiler.ScriptKind?.JSX
958+
: normalizedFileName.endsWith('.js')
959+
? compiler.ScriptKind?.JS
960+
: normalizedFileName.endsWith('.d.ts')
961+
? compiler.ScriptKind?.TS
962+
: compiler.ScriptKind?.TS
819963

820964
return compiler.createSourceFile(
821965
normalizedFileName,
@@ -834,7 +978,7 @@ export const createTypeDiagnosticsController = ({
834978
resolveModuleNames,
835979
}
836980

837-
const rootNames = [sourceFileName]
981+
const rootNames = [typecheckSourceFileName]
838982
if (!isReactMode) {
839983
rootNames.push(jsxTypesFileName)
840984
}

0 commit comments

Comments
 (0)