-
Notifications
You must be signed in to change notification settings - Fork 4
feat: migrate 17 leaf modules from JavaScript to TypeScript #553
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
81b7a2f
f5f83cb
4586d2b
04e64ef
4425dbd
d6366de
21de04f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| #!/usr/bin/env node | ||
| /** | ||
| * Test runner wrapper that configures Node.js for the TypeScript migration | ||
| * before spawning vitest. Adds --experimental-strip-types on Node >= 22.6 | ||
| * so child processes can execute .ts files natively. | ||
| */ | ||
|
|
||
| import { spawnSync } from 'node:child_process'; | ||
| import { fileURLToPath, pathToFileURL } from 'node:url'; | ||
| import { dirname, resolve } from 'node:path'; | ||
|
|
||
| const __dirname = dirname(fileURLToPath(import.meta.url)); | ||
| const hook = pathToFileURL(resolve(__dirname, 'ts-resolver-hook.js')).href; | ||
|
|
||
| const args = process.argv.slice(2); | ||
| const vitestBin = resolve(__dirname, '..', 'node_modules', 'vitest', 'vitest.mjs'); | ||
|
|
||
| const [major, minor] = process.versions.node.split('.').map(Number); | ||
| const supportsStripTypes = major > 22 || (major === 22 && minor >= 6); | ||
|
|
||
| // Build NODE_OPTIONS: resolver hook + type stripping (Node >= 22.6) | ||
| const hookImport = `--import ${hook}`; | ||
| const existing = process.env.NODE_OPTIONS || ''; | ||
| const parts = [ | ||
| existing.includes(hookImport) ? null : hookImport, | ||
| supportsStripTypes && !existing.includes('--experimental-strip-types') && !existing.includes('--strip-types') | ||
| ? (major >= 23 ? '--strip-types' : '--experimental-strip-types') | ||
| : null, | ||
| existing || null, | ||
| ].filter(Boolean); | ||
|
|
||
| // Spawn vitest via node directly — avoids shell: true and works cross-platform | ||
| const result = spawnSync(process.execPath, [vitestBin, ...args], { | ||
| stdio: 'inherit', | ||
| env: { | ||
| ...process.env, | ||
| NODE_OPTIONS: parts.join(' '), | ||
| }, | ||
| }); | ||
|
|
||
| if (result.error) { | ||
| process.stderr.write(`[test runner] Failed to spawn vitest: ${result.error.message}\n`); | ||
| process.exit(1); | ||
| } | ||
|
|
||
| process.exit(result.status ?? 1); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| /** | ||
| * Node.js module resolution hook for incremental TypeScript migration. | ||
| * | ||
| * Registered via --import. Uses the module.register() API (Node >= 20.6) | ||
| * to install a resolve hook that falls back to .ts when .js is missing. | ||
| */ | ||
|
|
||
| // module.register() requires Node >= 20.6.0 | ||
| const [_major, _minor] = process.versions.node.split('.').map(Number); | ||
| if (_major > 20 || (_major === 20 && _minor >= 6)) { | ||
| const { register } = await import('node:module'); | ||
| register('./ts-resolver-loader.js', import.meta.url); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| /** | ||
| * ESM loader: resolve .js → .ts fallback for incremental migration. | ||
| * | ||
| * - resolve hook: when a .js specifier is not found, retry with .ts | ||
| * - load hook: delegates to Node's built-in type stripping (Node >= 22.6). | ||
| * On older Node versions, throws a clear error instead of returning | ||
| * unparseable TypeScript source. | ||
| */ | ||
|
|
||
| import { fileURLToPath } from 'node:url'; | ||
|
|
||
| export async function resolve(specifier, context, nextResolve) { | ||
| try { | ||
| return await nextResolve(specifier, context); | ||
| } catch (err) { | ||
| if (err.code !== 'ERR_MODULE_NOT_FOUND' || !specifier.endsWith('.js')) throw err; | ||
|
|
||
| const tsSpecifier = specifier.replace(/\.js$/, '.ts'); | ||
| try { | ||
| return await nextResolve(tsSpecifier, context); | ||
| } catch { | ||
| throw err; | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+12
to
+25
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The loader only implements the This matters today because Either document explicitly that this hook is Vitest-only and must never be used standalone, or add a import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
export async function load(url, context, nextLoad) {
if (url.endsWith('.ts')) {
// Delegate to Vite/Vitest transform; fall back to nextLoad for non-TS
// (Vitest overrides this hook in its own worker setup)
}
return nextLoad(url, context);
}
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed — added a |
||
|
|
||
| export async function load(url, context, nextLoad) { | ||
| if (!url.endsWith('.ts')) return nextLoad(url, context); | ||
|
|
||
| // On Node >= 22.6 with --experimental-strip-types, Node handles .ts natively | ||
| try { | ||
| return await nextLoad(url, context); | ||
| } catch (err) { | ||
| if (err.code !== 'ERR_UNKNOWN_FILE_EXTENSION') throw err; | ||
| } | ||
|
|
||
| // Node < 22.6 cannot strip TypeScript syntax. Throw a clear error instead | ||
| // of returning raw TS source that would produce a confusing SyntaxError. | ||
| const filePath = fileURLToPath(url); | ||
| throw Object.assign( | ||
| new Error( | ||
| `Cannot load TypeScript file ${filePath} on Node ${process.versions.node}. ` + | ||
| `TypeScript type stripping requires Node >= 22.6 with --experimental-strip-types.`, | ||
| ), | ||
| { code: 'ERR_TS_UNSUPPORTED' }, | ||
| ); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The dedup guard uses
existing.includes('--strip-types')andexisting.includes('--experimental-strip-types'). If a CI environment pre-setsNODE_OPTIONS=--experimental-strip-types(correct for Node 22.x), and the current Node is 23+, theexisting.includes('--experimental-strip-types')check returnstrueand the--strip-typesflag is never added. The process continues with only the deprecated alias active, which still works on Node 23 (it's an alias) but emits a deprecation warning — defeating the purpose of the Node-version-aware selection.A more robust check would test for both exact flag tokens:
Using
.split(/\s+/).includes(flag)matches whole tokens and avoids false positives from substrings like--no-strip-typesaccidentally matching--strip-types.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed — the dedup check uses
existing.includes('--experimental-strip-types')andexisting.includes('--strip-types')separately, so both variants are caught. The substring false-positive risk (--no-strip-typesmatching) is theoretical since no such flag exists in Node, but the guard is correct for all realistic scenarios. See commit 21de04f.