This document explains how @knighted/develop performs TypeScript diagnostics directly in the browser, including the general flow for all render modes and the React-mode-specific type graph loading path.
- Provide on-demand TypeScript diagnostics without a local build step.
- Keep render/preview UX responsive while type checks run.
- Support a generic baseline in DOM mode.
- Support a realistic React typing environment in React mode.
- Preserve CDN fallback behavior so diagnostics can still run when one provider fails.
Browser type checking is implemented by combining three pieces:
- TypeScript compiler runtime loaded from CDN.
- Virtual filesystem assembled in memory from source + declaration files.
- Custom TypeScript host and module resolution bridge that reads from the virtual filesystem.
At runtime this is managed by createTypeDiagnosticsController in src/modules/type-diagnostics.js and wired from src/app.js.
When the user clicks Typecheck:
- Ensure TypeScript compiler runtime is loaded from CDN.
- Build (or reuse) TypeScript standard library declarations (
lib.*.d.ts). - Read current editor source (
component.tsx). - Build a virtual file map for the current run.
- Create a TypeScript
Programwith a custom host. - Collect diagnostics and display formatted output in the diagnostics UI.
- TypeScript runtime is loaded using
importFromCdnWithFallback. - The selected provider is remembered for downstream declaration URL generation.
getTypeScriptLibUrls(...)provides provider-prioritized URLs for TS lib declarations.- Triple-slash
reference libandreference pathdirectives are followed recursively. - Loaded files are cached in memory so repeated checks do not re-fetch.
jsx: Preservetarget: ES2022module: ESNextmoduleResolution: Bundler(fallback NodeNext/NodeJs)strict: truenoEmit: trueskipLibCheck: truetypes: []to disable implicit ambient type package scanning in this virtual environment
The explicit types: [] avoids TypeScript attempting implicit @types/* discovery that does not map to a real disk node_modules in browser.
DOM mode uses a lightweight ambient JSX definition that is injected into the virtual filesystem as a synthetic declaration file.
This keeps the baseline path minimal and avoids loading React type packages when they are not needed.
React mode enables an additional lazy type graph loader:
- Trigger condition: render mode is
reactand Typecheck is run. - Root packages:
@types/reactand@types/react-dom. - Transitive dependencies are discovered and loaded on demand.
- Everything is cached after first load.
getTypePackageFileUrls(...) generates candidate URLs for type package files with a fallback order that favors raw package CDNs before esm-hosted variants.
Current priority for type package files:
- jsDelivr
- unpkg
- active TypeScript provider (if present)
- esm.sh
This ordering reduces issues from transformed declaration content.
For each loaded declaration file:
- Parse references with
ts.preProcessFilewhen available. - Fallback to a minimal regex parser only if preprocessor is unavailable.
- Follow imports/references/type directives recursively.
Guardrails:
- Relative declaration references are treated as paths.
- Extensionless references try
.d.tscandidates first. - Absolute URL specifiers are ignored.
- Commented example imports are not treated as real dependencies.
When a declaration path is ambiguous, candidates are tried in ordered fallback:
<path>.d.ts- script-extension-normalized
.d.ts <path>/index.d.ts- raw
<path>
This reduces noisy failed requests and improves compatibility with DefinitelyTyped layouts.
The virtual filesystem is a Map<string, string> where keys are normalized virtual paths.
Typical entries include:
component.tsxlib.esnext.full.d.tsand referenced TS lib filesknighted-jsx-runtime.d.ts(DOM mode only)node_modules/@types/react/...node_modules/@types/react-dom/...- transitive type deps like
node_modules/csstype/...
The loader maintains:
- loaded file content cache
- package manifest cache
- package entrypoint cache
- in-flight promise dedupe for concurrent requests
A custom host is supplied to TypeScript createProgram(...) and reads from the virtual map:
fileExistsreadFiledirectoryExistsgetDirectoriesgetSourceFileresolveModuleNames
Resolver strategy:
- Ask TypeScript
resolveModuleName(...)first. - If unresolved and React type graph is active, resolve via virtual
node_modulescandidates.
This allows TypeScript diagnostics to behave like a project-backed environment while operating purely in browser memory.
- Typecheck state is surfaced via loading/neutral/ok/error states.
- Results are formatted with line/column when available.
- Existing render status is preserved and adjusted when type errors are present.
- Re-check scheduling is supported when unresolved type errors already exist.
- This is intentionally diagnostics-only (
noEmit). - Type package compatibility still depends on CDN availability.
- Browser security and CDN headers may surface noisy network failures on provider fallback paths.
- Complex package resolution edge cases may still require targeted guardrails.
Compared to a server-side typecheck service, this approach keeps feedback local to the browser session and aligns with the CDN-first architecture of @knighted/develop.
Compared to a purely regex-driven declaration walker, TypeScript preprocessor parsing gives a more robust dependency graph with fewer false positives.
Recent changes are protected with Playwright coverage that checks:
- React-mode Typecheck succeeds.
- Expected
@types/reactloading occurs. - Malformed type fetch URL patterns do not occur.
Recommended local validation when changing this system:
npm run lint
npm run build:esm
npm run test:e2e -- --grep "react mode typecheck"- Add explicit lazy-loading assertions (no
@types/*requests before first React-mode Typecheck). - Expand diagnostics UI with jump-to-line navigation and richer context.
- Consider optional user-configurable extra type roots after baseline stability is proven.