Skip to content

Commit 47c0676

Browse files
chore: harden react-mode typecheck.
1 parent 25062c2 commit 47c0676

6 files changed

Lines changed: 721 additions & 77 deletions

File tree

docs/next-steps.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,23 @@ Focused follow-up work for `@knighted/develop`.
1919
- Detect transient CDN/module loading failures and surface a clear recovery action in-app.
2020
- Add a user-triggered retry path (for example, Reload page / Force reload) when runtime bootstrap imports fail.
2121
- Consider an optional automatic one-time retry before showing recovery controls, while avoiding infinite reload loops.
22+
23+
5. **Type reference parsing hardening (TS preprocessor-first)**
24+
- Transition declaration/reference discovery in in-browser type diagnostics to a TypeScript preprocessor-first flow (`ts.preProcessFile`) instead of regex-driven parsing.
25+
- Scope this to the lazy React type environment loader first, then evaluate whether the same parser path should be reused for all type package graph walking.
26+
- Keep current lazy-loading behavior intact: no React type graph fetch until the user switches to React render mode and triggers Typecheck.
27+
- Preserve CDN provider fallback behavior and existing diagnostics UX while changing parser internals.
28+
- Add a strict fallback contract:
29+
- Primary: `preProcessFile` outputs (`importedFiles`, `referencedFiles`, `typeReferenceDirectives`).
30+
- Secondary fallback only when unavailable: current lightweight parsing logic.
31+
- Never treat commented example code as imports/references.
32+
- Add guardrails around known failure classes discovered during development:
33+
- Relative declaration references like `global.d.ts` must resolve as file paths, not package names.
34+
- Extensionless declaration references (for example `./user-context`) must attempt `.d.ts` candidates first.
35+
- Avoid noisy parallel fetch fan-out for bad candidates; use ordered fallback to reduce 404/CORS console noise.
36+
- Add focused test coverage (unit or Playwright) that proves:
37+
- React-mode typecheck does not trigger fake fetches from commented examples in declaration files.
38+
- React-mode typecheck resolves `react` and `react-dom/client` without module-not-found diagnostics.
39+
- DOM mode still avoids React type graph hydration.
40+
- Suggested implementation prompt:
41+
- "Refactor `src/modules/type-diagnostics.js` to make TypeScript preprocessor parsing (`preProcessFile`) the source of truth for declaration graph discovery in the lazy React type loader. Keep current CDN fallback and lazy hydration semantics. Ensure references from comments are ignored, `*.d.ts`/relative path handling is correct, and candidate fetch ordering minimizes noisy failed requests. Add regression coverage for `global.d.ts` and commented `./user-context` examples. Validate with `npm run lint`, `npm run build:esm`, and targeted React/typecheck Playwright runs."

playwright/app.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,48 @@ test('transpiles TypeScript annotations in component source', async ({ page }) =
334334
await expect(page.locator('#preview-host button')).toContainText('typed')
335335
})
336336

337+
test('react mode typecheck loads types without malformed URL fetches', async ({
338+
page,
339+
}) => {
340+
await waitForInitialRender(page)
341+
342+
await ensurePanelToolsVisible(page, 'component')
343+
344+
const typeRequestUrls: string[] = []
345+
page.on('request', request => {
346+
const url = request.url()
347+
if (url.includes('@types/')) {
348+
typeRequestUrls.push(url)
349+
}
350+
})
351+
352+
await page.locator('#render-mode').selectOption('react')
353+
await page.getByRole('button', { name: 'Typecheck' }).click()
354+
355+
await page.locator('#diagnostics-toggle').click()
356+
await expect(page.locator('#diagnostics-component')).toContainText(
357+
'No TypeScript errors found.',
358+
)
359+
360+
const diagnosticsText = await page.locator('#diagnostics-component').innerText()
361+
expect(diagnosticsText).not.toContain("Cannot find type definition file for 'react'")
362+
expect(diagnosticsText).not.toContain(
363+
"Cannot find type definition file for 'react-dom'",
364+
)
365+
366+
expect(typeRequestUrls.some(url => url.includes('@types/react'))).toBeTruthy()
367+
368+
const malformedTypeRequestPatterns = [
369+
'/@types/global.d.ts/package.json',
370+
'/user-context',
371+
'/https:/',
372+
]
373+
374+
for (const pattern of malformedTypeRequestPatterns) {
375+
expect(typeRequestUrls.some(url => url.includes(pattern))).toBeFalsy()
376+
}
377+
})
378+
337379
test('clearing component source reports clear action without error status', async ({
338380
page,
339381
}) => {

src/app.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
cdnImports,
3+
getTypePackageFileUrls,
34
getTypeScriptLibUrls,
45
importFromCdnWithFallback,
56
} from './modules/cdn.js'
@@ -391,7 +392,9 @@ const typeDiagnostics = createTypeDiagnosticsController({
391392
cdnImports,
392393
importFromCdnWithFallback,
393394
getTypeScriptLibUrls,
395+
getTypePackageFileUrls,
394396
getJsxSource: () => getJsxSource(),
397+
getRenderMode: () => renderMode.value,
395398
setTypecheckButtonLoading,
396399
setTypeDiagnosticsDetails,
397400
setStatus,

src/modules/cdn.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,16 @@ const typeScriptLibBaseByProvider = {
234234
jsdelivr: `https://cdn.jsdelivr.net/npm/typescript@${typeScriptVersion}/lib`,
235235
}
236236

237+
const typePackageVersionByName = {
238+
'@types/react': '19.2.2',
239+
'@types/react-dom': '19.2.1',
240+
'@types/prop-types': '15.7.15',
241+
'@types/scheduler': '0.26.0',
242+
csstype: '3.1.3',
243+
}
244+
245+
const getTypePackageVersion = packageName => typePackageVersionByName[packageName]
246+
237247
/*
238248
* Keep a reliable fallback order for .d.ts files when the active module provider
239249
* does not host TypeScript lib declarations consistently (e.g. import maps/jspmGa).
@@ -257,6 +267,47 @@ const getTypeScriptLibProviderPriority = typeScriptProvider => {
257267
return [...new Set(ordered)]
258268
}
259269

270+
const typePackageBaseByProvider = {
271+
esm: packageName => {
272+
const version = getTypePackageVersion(packageName)
273+
const versionSegment = version ? `@${version}` : ''
274+
return `https://esm.sh/${packageName}${versionSegment}`
275+
},
276+
unpkg: packageName => {
277+
const version = getTypePackageVersion(packageName)
278+
const versionSegment = version ? `@${version}` : ''
279+
return `https://unpkg.com/${packageName}${versionSegment}`
280+
},
281+
jsdelivr: packageName => {
282+
const version = getTypePackageVersion(packageName)
283+
const versionSegment = version ? `@${version}` : ''
284+
return `https://cdn.jsdelivr.net/npm/${packageName}${versionSegment}`
285+
},
286+
}
287+
288+
export const getTypePackageFileUrls = (
289+
packageName,
290+
fileName,
291+
{ typeScriptProvider } = {},
292+
) => {
293+
const normalizedFileName =
294+
typeof fileName === 'string' && fileName.length > 0 ? fileName : 'package.json'
295+
const typePackageProviderPriority = [
296+
'jsdelivr',
297+
'unpkg',
298+
...(typeof typeScriptProvider === 'string' ? [typeScriptProvider] : []),
299+
'esm',
300+
]
301+
const providerOrderedBases = [...new Set(typePackageProviderPriority)]
302+
.map(provider => {
303+
const createBase = typePackageBaseByProvider[provider]
304+
return typeof createBase === 'function' ? createBase(packageName) : null
305+
})
306+
.filter(Boolean)
307+
308+
return providerOrderedBases.map(baseUrl => `${baseUrl}/${normalizedFileName}`)
309+
}
310+
260311
export const getTypeScriptLibUrls = (fileName, { typeScriptProvider } = {}) => {
261312
const providerOrderedBases = getTypeScriptLibProviderPriority(typeScriptProvider)
262313
.map(provider => typeScriptLibBaseByProvider[provider])

src/modules/defaults.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ export const defaultJsx = [
2727

2828
export const defaultReactJsx = [
2929
"import { useState } from 'react'",
30+
"import type { MouseEvent } from 'react'",
3031
'',
3132
'type CounterButtonProps = {',
3233
' label: string',
3334
' active: boolean',
34-
' onClick: (event: MouseEvent) => void',
35+
' onClick: (event: MouseEvent<HTMLButtonElement>) => void',
3536
'}',
3637
'',
3738
'const CounterButton = ({ label, active, onClick }: CounterButtonProps) => (',
@@ -47,7 +48,7 @@ export const defaultReactJsx = [
4748
'',
4849
'const App = () => {',
4950
' const [count, setCount] = useState(0)',
50-
' const handleClick = (_event: MouseEvent) => {',
51+
' const handleClick = (_event: MouseEvent<HTMLButtonElement>) => {',
5152
' setCount(current => current + 1)',
5253
' }',
5354
'',

0 commit comments

Comments
 (0)