From f4a6c817be48dc761e180301d3914229caa70c0b Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Fri, 23 Jan 2026 06:55:51 +0100 Subject: [PATCH 01/10] fix: refactor --- packages/jiti-tsc/project.json | 19 +++++++++++++++++-- packages/jiti-tsc/src/cli.ts | 22 +++++++++++++--------- packages/jiti-tsc/src/index.ts | 8 ++++++++ packages/jiti-tsc/tools/cli.ts | 24 ++++++++++++++++++++++++ packages/jiti-tsc/tools/helper.ts | 3 +++ packages/jiti-tsc/tools/test-app.ts | 8 ++++++++ packages/jiti-tsc/tools/tsconfig.json | 10 ++++++++++ 7 files changed, 83 insertions(+), 11 deletions(-) create mode 100644 packages/jiti-tsc/tools/cli.ts create mode 100644 packages/jiti-tsc/tools/helper.ts create mode 100644 packages/jiti-tsc/tools/test-app.ts create mode 100644 packages/jiti-tsc/tools/tsconfig.json diff --git a/packages/jiti-tsc/project.json b/packages/jiti-tsc/project.json index fcf8414..cdbe99e 100644 --- a/packages/jiti-tsc/project.json +++ b/packages/jiti-tsc/project.json @@ -33,12 +33,27 @@ ] } }, - "demo": { + "cli-test": { "executor": "nx:run-commands", "options": { - "command": "nx run @push-based/jiti-tsc --inputFile=./tools/jiti-tsc/demo/input-schema.ts --outputFile=./tools/jiti-tsc/demo/output-schema.ts" + "command": "node dist/src/cli.js tools/test-app.ts", + "cwd": "{projectRoot}", + "env": { + "JITI_TSCONFIG_PATH": "tools/tsconfig.json" + } }, "dependsOn": ["build"] + }, + "import-test": { + "dependsOn": ["build"], + "executor": "nx:run-commands", + "options": { + "command": "node dist/src/cli.js tools/cli.ts", + "cwd": "{projectRoot}", + "env": { + "JITI_TSCONFIG_PATH": "tools/tsconfig.json" + } + } } } } diff --git a/packages/jiti-tsc/src/cli.ts b/packages/jiti-tsc/src/cli.ts index 1976053..953834c 100644 --- a/packages/jiti-tsc/src/cli.ts +++ b/packages/jiti-tsc/src/cli.ts @@ -9,14 +9,18 @@ if (!file) { process.exit(1); } -const child = spawn(process.execPath, [file, ...rest], { - stdio: 'inherit', - env: { - ...process.env, - NODE_OPTIONS: [process.env['NODE_OPTIONS'], '--import @push-based/jiti-tsc'] - .filter(Boolean) - .join(' '), +const child = spawn( + process.execPath, + [ + '--import', + '@push-based/jiti-tsc', + file, + ...rest, + ], + { + stdio: 'inherit', + env: process.env, }, -}); -// eslint-disable-next-line n/no-process-exit +); + child.on('exit', code => process.exit(code ?? 1)); diff --git a/packages/jiti-tsc/src/index.ts b/packages/jiti-tsc/src/index.ts index 5a36fe9..d1925ac 100644 --- a/packages/jiti-tsc/src/index.ts +++ b/packages/jiti-tsc/src/index.ts @@ -1,5 +1,13 @@ import { registerJitiTsconfig } from './lib/jiti/register.js'; +// DX safety net: warn about incorrect usage with npm/npx +if (process.env['npm_execpath']) { + console.warn( + '[jiti-tsc] Detected npm/npx execution. ' + + 'Do not use --import jiti-tsc with npm.', + ); +} + try { await registerJitiTsconfig(); } catch (error) { diff --git a/packages/jiti-tsc/tools/cli.ts b/packages/jiti-tsc/tools/cli.ts new file mode 100644 index 0000000..fdb3370 --- /dev/null +++ b/packages/jiti-tsc/tools/cli.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env node +// Test script for jiti-tsc path aliases +console.log('Testing jiti-tsc path aliases:'); +console.log('NODE_OPTIONS:', process.env.NODE_OPTIONS); +console.log('JITI_*:', Object.entries(process.env).filter(([k]) => k.startsWith('JITI_'))); + +// Test importing with path alias +try { + const { helper } = await import('@tools/helper'); + console.log('✓ Successfully imported @tools/helper'); + console.log('Result:', helper()); +} catch (e: unknown) { + console.log('✗ Failed to import @tools/helper:', e instanceof Error ? e.message : String(e)); +} + +// Test importing jiti-tsc itself +try { + await import('@push-based/jiti-tsc'); + console.log('✓ jiti-tsc imported successfully'); +} catch (e: unknown) { + console.log('✗ Failed to import jiti-tsc:', e instanceof Error ? e.message : String(e)); +} + +export {}; diff --git a/packages/jiti-tsc/tools/helper.ts b/packages/jiti-tsc/tools/helper.ts new file mode 100644 index 0000000..07132f5 --- /dev/null +++ b/packages/jiti-tsc/tools/helper.ts @@ -0,0 +1,3 @@ +export function helper() { + return 'Hello from jiti-tsc tools!'; +} \ No newline at end of file diff --git a/packages/jiti-tsc/tools/test-app.ts b/packages/jiti-tsc/tools/test-app.ts new file mode 100644 index 0000000..5c640c3 --- /dev/null +++ b/packages/jiti-tsc/tools/test-app.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import { helper } from '@tools/helper'; + +console.log('Testing jiti-tsc path aliases:'); +console.log('NODE_OPTIONS:', process.env.NODE_OPTIONS); +console.log('JITI_TSCONFIG_PATH:', process.env.JITI_TSCONFIG_PATH); +console.log('Result:', helper()); +console.log('✓ jiti-tsc working correctly!'); \ No newline at end of file diff --git a/packages/jiti-tsc/tools/tsconfig.json b/packages/jiti-tsc/tools/tsconfig.json new file mode 100644 index 0000000..f3cc316 --- /dev/null +++ b/packages/jiti-tsc/tools/tsconfig.json @@ -0,0 +1,10 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@tools/*": ["./*"] + } + }, + "include": ["*.ts"] +} \ No newline at end of file From 2b3818dcfdaeacddda83391933b65fcbf87ac6e8 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Fri, 23 Jan 2026 08:25:50 +0100 Subject: [PATCH 02/10] fix: refactor --- packages/jiti-tsc/src/cli.ts | 46 +++++++++++-------- packages/jiti-tsc/src/index.ts | 5 +- .../jiti-tsc/src/lib/jiti/import-module.ts | 2 +- packages/jiti-tsc/src/lib/jiti/register.ts | 16 +++++++ 4 files changed, 48 insertions(+), 21 deletions(-) diff --git a/packages/jiti-tsc/src/cli.ts b/packages/jiti-tsc/src/cli.ts index 953834c..1627ed6 100644 --- a/packages/jiti-tsc/src/cli.ts +++ b/packages/jiti-tsc/src/cli.ts @@ -1,26 +1,36 @@ #!/usr/bin/env node import { spawn } from 'node:child_process'; +import fs from 'node:fs'; -const [_nodePath, _scriptPath, file, ...rest] = process.argv; +const [, , cmd, ...rest] = process.argv; -if (!file) { - console.error('Usage: jiti-tsc '); - // eslint-disable-next-line n/no-process-exit +if (!cmd) { + console.error('Usage: jiti-tsc [...args]'); process.exit(1); } -const child = spawn( - process.execPath, - [ - '--import', - '@push-based/jiti-tsc', - file, - ...rest, - ], - { - stdio: 'inherit', - env: process.env, - }, -); +// Detect if the first argument is a TypeScript file or a command +// This allows the CLI to work in two modes: +// 1. jiti-tsc file.ts → runs TypeScript files with jiti support +// 2. jiti-tsc command args → runs shell commands (npm scripts, binaries, etc.) +const isFile = + fs.existsSync(cmd) && + fs.statSync(cmd).isFile() && + /\.(ts|tsx|mts|cts)$/.test(cmd); -child.on('exit', code => process.exit(code ?? 1)); +if (isFile) { + // ✅ TypeScript file: Use --import to enable jiti-tsc TypeScript support + const child = spawn( + process.execPath, + ['--import', '@push-based/jiti-tsc', cmd, ...rest], + { stdio: 'inherit' }, + ); + child.on('exit', code => process.exit(code ?? 1)); +} else { + // ✅ Shell command: Run directly with shell support for npm scripts/binaries + const child = spawn(cmd, rest, { + stdio: 'inherit', + shell: true, // important for npm-installed binaries + }); + child.on('exit', code => process.exit(code ?? 1)); +} diff --git a/packages/jiti-tsc/src/index.ts b/packages/jiti-tsc/src/index.ts index d1925ac..f23afad 100644 --- a/packages/jiti-tsc/src/index.ts +++ b/packages/jiti-tsc/src/index.ts @@ -11,6 +11,7 @@ if (process.env['npm_execpath']) { try { await registerJitiTsconfig(); } catch (error) { - console.error('[jiti-tsc] Failed to register:', error); - throw error; + // If registration fails (e.g., when not loaded as --import), try to handle gracefully + console.warn('[jiti-tsc] Registration failed, continuing without TypeScript support:', error instanceof Error ? error.message : String(error)); + // Don't throw - allow the process to continue } diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.ts b/packages/jiti-tsc/src/lib/jiti/import-module.ts index 20c228e..79c07f2 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.ts @@ -93,7 +93,7 @@ export const mapTsJsxToJitiJsx = (tsJsxMode: number): boolean => * | jsx | boolean | jsx | JsxEmit (0-5) | TS JsxEmit enum (0-5) => boolean JSX processing. | */ export type MappableJitiOptions = Partial< - Pick + Pick >; /** * Parse TypeScript compiler options to mappable jiti options diff --git a/packages/jiti-tsc/src/lib/jiti/register.ts b/packages/jiti-tsc/src/lib/jiti/register.ts index b732885..cd1347e 100644 --- a/packages/jiti-tsc/src/lib/jiti/register.ts +++ b/packages/jiti-tsc/src/lib/jiti/register.ts @@ -14,12 +14,28 @@ export async function registerJitiTsconfig() { try { const jitiOptions = await jitiOptionsFromTsConfig(tsconfigPath); + // Add semver and other problematic modules to nativeModules to avoid Node.js v24 issues + jitiOptions.nativeModules = [ + ...(jitiOptions.nativeModules || []), + 'semver', + 'semver/classes/range', + 'semver/classes/comparator', + ]; const envVars = jitiOptionsToEnv(jitiOptions); Object.entries(envVars).forEach( // eslint-disable-next-line functional/immutable-data ([k, v]) => v != null && (process.env[k] = v), ); + + // Debug logging when JITI_DEBUG is enabled + if (process.env['JITI_DEBUG']) { + console.log('[jiti-tsc] NODE_OPTIONS:', process.env['NODE_OPTIONS']); + console.log('[jiti-tsc] JITI_* env vars:'); + Object.entries(process.env) + .filter(([k]) => k.startsWith('JITI_')) + .forEach(([k,v]) => console.log(k,v)) + } } catch { console.warn( `[jiti-tsc] Failed to load tsconfig from ${tsconfigPath}, continuing without tsconfig`, From 0240a05c235604cfca106686f79e68a6a1de07d6 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Fri, 23 Jan 2026 08:27:47 +0100 Subject: [PATCH 03/10] fix: refactor --- packages/jiti-tsc/src/index.ts | 7 +++++-- packages/jiti-tsc/src/lib/jiti/import-module.ts | 5 ++++- packages/jiti-tsc/src/lib/jiti/register.ts | 6 +++--- packages/jiti-tsc/tools/cli.ts | 15 ++++++++++++--- packages/jiti-tsc/tools/helper.ts | 2 +- packages/jiti-tsc/tools/test-app.ts | 2 +- packages/jiti-tsc/tools/tsconfig.json | 2 +- 7 files changed, 27 insertions(+), 12 deletions(-) diff --git a/packages/jiti-tsc/src/index.ts b/packages/jiti-tsc/src/index.ts index f23afad..602112c 100644 --- a/packages/jiti-tsc/src/index.ts +++ b/packages/jiti-tsc/src/index.ts @@ -4,7 +4,7 @@ import { registerJitiTsconfig } from './lib/jiti/register.js'; if (process.env['npm_execpath']) { console.warn( '[jiti-tsc] Detected npm/npx execution. ' + - 'Do not use --import jiti-tsc with npm.', + 'Do not use --import jiti-tsc with npm.', ); } @@ -12,6 +12,9 @@ try { await registerJitiTsconfig(); } catch (error) { // If registration fails (e.g., when not loaded as --import), try to handle gracefully - console.warn('[jiti-tsc] Registration failed, continuing without TypeScript support:', error instanceof Error ? error.message : String(error)); + console.warn( + '[jiti-tsc] Registration failed, continuing without TypeScript support:', + error instanceof Error ? error.message : String(error), + ); // Don't throw - allow the process to continue } diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.ts b/packages/jiti-tsc/src/lib/jiti/import-module.ts index 79c07f2..dc211cc 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.ts @@ -93,7 +93,10 @@ export const mapTsJsxToJitiJsx = (tsJsxMode: number): boolean => * | jsx | boolean | jsx | JsxEmit (0-5) | TS JsxEmit enum (0-5) => boolean JSX processing. | */ export type MappableJitiOptions = Partial< - Pick + Pick< + JitiOptions, + 'alias' | 'interopDefault' | 'sourceMaps' | 'jsx' | 'nativeModules' + > >; /** * Parse TypeScript compiler options to mappable jiti options diff --git a/packages/jiti-tsc/src/lib/jiti/register.ts b/packages/jiti-tsc/src/lib/jiti/register.ts index cd1347e..27370f5 100644 --- a/packages/jiti-tsc/src/lib/jiti/register.ts +++ b/packages/jiti-tsc/src/lib/jiti/register.ts @@ -32,9 +32,9 @@ export async function registerJitiTsconfig() { if (process.env['JITI_DEBUG']) { console.log('[jiti-tsc] NODE_OPTIONS:', process.env['NODE_OPTIONS']); console.log('[jiti-tsc] JITI_* env vars:'); - Object.entries(process.env) - .filter(([k]) => k.startsWith('JITI_')) - .forEach(([k,v]) => console.log(k,v)) + Object.entries(process.env) + .filter(([k]) => k.startsWith('JITI_')) + .forEach(([k, v]) => console.log(k, v)); } } catch { console.warn( diff --git a/packages/jiti-tsc/tools/cli.ts b/packages/jiti-tsc/tools/cli.ts index fdb3370..8495ecb 100644 --- a/packages/jiti-tsc/tools/cli.ts +++ b/packages/jiti-tsc/tools/cli.ts @@ -2,7 +2,10 @@ // Test script for jiti-tsc path aliases console.log('Testing jiti-tsc path aliases:'); console.log('NODE_OPTIONS:', process.env.NODE_OPTIONS); -console.log('JITI_*:', Object.entries(process.env).filter(([k]) => k.startsWith('JITI_'))); +console.log( + 'JITI_*:', + Object.entries(process.env).filter(([k]) => k.startsWith('JITI_')), +); // Test importing with path alias try { @@ -10,7 +13,10 @@ try { console.log('✓ Successfully imported @tools/helper'); console.log('Result:', helper()); } catch (e: unknown) { - console.log('✗ Failed to import @tools/helper:', e instanceof Error ? e.message : String(e)); + console.log( + '✗ Failed to import @tools/helper:', + e instanceof Error ? e.message : String(e), + ); } // Test importing jiti-tsc itself @@ -18,7 +24,10 @@ try { await import('@push-based/jiti-tsc'); console.log('✓ jiti-tsc imported successfully'); } catch (e: unknown) { - console.log('✗ Failed to import jiti-tsc:', e instanceof Error ? e.message : String(e)); + console.log( + '✗ Failed to import jiti-tsc:', + e instanceof Error ? e.message : String(e), + ); } export {}; diff --git a/packages/jiti-tsc/tools/helper.ts b/packages/jiti-tsc/tools/helper.ts index 07132f5..786698d 100644 --- a/packages/jiti-tsc/tools/helper.ts +++ b/packages/jiti-tsc/tools/helper.ts @@ -1,3 +1,3 @@ export function helper() { return 'Hello from jiti-tsc tools!'; -} \ No newline at end of file +} diff --git a/packages/jiti-tsc/tools/test-app.ts b/packages/jiti-tsc/tools/test-app.ts index 5c640c3..2328640 100644 --- a/packages/jiti-tsc/tools/test-app.ts +++ b/packages/jiti-tsc/tools/test-app.ts @@ -5,4 +5,4 @@ console.log('Testing jiti-tsc path aliases:'); console.log('NODE_OPTIONS:', process.env.NODE_OPTIONS); console.log('JITI_TSCONFIG_PATH:', process.env.JITI_TSCONFIG_PATH); console.log('Result:', helper()); -console.log('✓ jiti-tsc working correctly!'); \ No newline at end of file +console.log('✓ jiti-tsc working correctly!'); diff --git a/packages/jiti-tsc/tools/tsconfig.json b/packages/jiti-tsc/tools/tsconfig.json index f3cc316..7d25004 100644 --- a/packages/jiti-tsc/tools/tsconfig.json +++ b/packages/jiti-tsc/tools/tsconfig.json @@ -7,4 +7,4 @@ } }, "include": ["*.ts"] -} \ No newline at end of file +} From 210d0dc1e62949737c8cfafc0c2556355c09252e Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Tue, 3 Feb 2026 23:47:16 +0100 Subject: [PATCH 04/10] fix: refactor --- e2e/ts-jiti-e2e/tests/jiti.e2e.test.ts | 134 ++++++++++++++++--------- packages/jiti-tsc/src/cli.ts | 4 +- 2 files changed, 87 insertions(+), 51 deletions(-) diff --git a/e2e/ts-jiti-e2e/tests/jiti.e2e.test.ts b/e2e/ts-jiti-e2e/tests/jiti.e2e.test.ts index d3f7a08..e44042a 100644 --- a/e2e/ts-jiti-e2e/tests/jiti.e2e.test.ts +++ b/e2e/ts-jiti-e2e/tests/jiti.e2e.test.ts @@ -4,10 +4,10 @@ import { TEST_OUTPUT_DIR, executeProcess, fsFromJson, + removeColorCodes, } from '@push-based/test-utils'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import stripAnsi from 'strip-ansi'; import { expect } from 'vitest'; const __filename = fileURLToPath(import.meta.url); @@ -19,6 +19,42 @@ const toSlug = (str: string): string => .replace(/\s+/g, '-') .replace(/[^a-z\d-]/g, ''); +const binJitiTscContent = `#!/usr/bin/env node +console.log(\`Executed over jiti-tsc\`); +console.log('args:', process.argv.slice(2)); +`; + +const binImportJitiTscContent = `#!/usr/bin/env node +console.log(\`Executed over --import jiti-tsc\`); +console.log('args:', process.argv.slice(2)); +`; + +const loadTsAContent = `import { x } from './b.ts'; console.log(x); +console.log('args:', process.argv.slice(2));`; + +const loadTsBContent = `export const x = 'load-ts';`; + +const execTsAContent = `console.log('exec-ts'); +console.log('args:', process.argv.slice(2));`; + +const execTsLoadTsAContent = `import { x } from './b.js'; console.log(x); +console.log('args:', process.argv.slice(2));`; + +const execTsLoadTsBContent = `export const x = 'exec-ts-load-ts';`; + +const tsconfigPathAContent = `import { x } from '@/b.js'; console.log(x); +console.log('args:', process.argv.slice(2));`; + +const tsconfigPathBContent = `export const x = 'exec-ts-tsconfig-load-ts';`; + +const tsconfigPathComplexBContent = `import { y } from '@/c.js'; export const x = y;`; + +const tsconfigPathComplexCContent = `export const y = 'exec-ts-tsconfig-load-ts-tsconfig';`; + +const tsconfigPathContent = { + compilerOptions: { baseUrl: '.', paths: { '@/*': ['./*'] } }, +}; + describe('CLI jiti', () => { const envRoot = path.join(E2E_ENVIRONMENTS_DIR, nxTargetProject()); const describeName = 'CLI jiti'; @@ -36,13 +72,10 @@ describe('CLI jiti', () => { it('should execute ts file with jiti-tsc', async () => { const baseFolder = getTestDir('exec-jiti-tsc'); const cleanup = await fsFromJson({ - [path.join(baseFolder, 'bin.ts')]: `#!/usr/bin/env node -console.log(\`Executed over jiti-tsc\`); -console.log('args:', process.argv.slice(2)); -`, + [path.join(baseFolder, 'bin.ts')]: binJitiTscContent, }); - const { code, stdout } = await executeProcess({ + const { code, stdout, stderr } = await executeProcess({ command: 'npx', args: [ '@push-based/jiti-tsc', @@ -54,8 +87,12 @@ console.log('args:', process.argv.slice(2)); }); expect(code).toBe(0); - expect(stripAnsi(stdout)).toContain('Executed over jiti-tsc'); - expect(stripAnsi(stdout)).toContain("args: [ '--test-arg=123' ]"); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'Executed over jiti-tsc', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--test-arg=123' ]", + ); await cleanup(); }); @@ -63,15 +100,14 @@ console.log('args:', process.argv.slice(2)); it('should load .ts', async () => { const d = getTestDir('load-ts'); const cleanup = await fsFromJson({ - [path.join(d, 'a.js')]: `import { x } from './b.ts'; console.log(x); -console.log('args:', process.argv.slice(2));`, - [path.join(d, 'b.ts')]: `export const x = 'load-ts';`, + [path.join(d, 'a.ts')]: loadTsAContent, + [path.join(d, 'b.ts')]: loadTsBContent, }); - const { code, stdout } = await executeProcess({ + const { code, stdout, stderr } = await executeProcess({ command: 'npx', args: [ '@push-based/jiti-tsc', - path.relative(envRoot, path.join(d, 'a.js')), + path.relative(envRoot, path.join(d, 'a.ts')), '--load-arg=test', ], cwd: envRoot, @@ -79,17 +115,18 @@ console.log('args:', process.argv.slice(2));`, }); expect(code).toBe(0); - expect(stripAnsi(stdout)).toContain("args: [ '--load-arg=test' ]"); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--load-arg=test' ]", + ); await cleanup(); }); it('should exec .ts', async () => { const d = getTestDir('exec-ts'); const cleanup = await fsFromJson({ - [path.join(d, 'a.ts')]: `console.log('exec-ts'); -console.log('args:', process.argv.slice(2));`, + [path.join(d, 'a.ts')]: execTsAContent, }); - const { code, stdout } = await executeProcess({ + const { code, stdout, stderr } = await executeProcess({ command: 'npx', args: [ '@push-based/jiti-tsc', @@ -101,18 +138,19 @@ console.log('args:', process.argv.slice(2));`, }); expect(code).toBe(0); - expect(stripAnsi(stdout)).toContain("args: [ '--exec-arg=value' ]"); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--exec-arg=value' ]", + ); await cleanup(); }); it('should exec .ts loading .ts', async () => { const d = getTestDir('exec-ts-load-ts'); const cleanup = await fsFromJson({ - [path.join(d, 'a.ts')]: `import { x } from './b.js'; console.log(x); -console.log('args:', process.argv.slice(2));`, - [path.join(d, 'b.ts')]: `export const x = 'exec-ts-load-ts';`, + [path.join(d, 'a.ts')]: execTsLoadTsAContent, + [path.join(d, 'b.ts')]: execTsLoadTsBContent, }); - const { code, stdout } = await executeProcess({ + const { code, stdout, stderr } = await executeProcess({ command: 'npx', args: [ '@push-based/jiti-tsc', @@ -124,21 +162,20 @@ console.log('args:', process.argv.slice(2));`, }); expect(code).toBe(0); - expect(stripAnsi(stdout)).toContain("args: [ '--load-ts-arg=hello' ]"); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--load-ts-arg=hello' ]", + ); await cleanup(); }); it('should exec .ts with tsconfig.path loading .ts', async () => { const d = getTestDir('exec-ts-tsconfig-load-ts'); const cleanup = await fsFromJson({ - [path.join(d, 'tsconfig.json')]: { - compilerOptions: { baseUrl: '.', paths: { '@/*': ['./*'] } }, - }, - [path.join(d, 'a.ts')]: `import { x } from '@/b.js'; console.log(x); -console.log('args:', process.argv.slice(2));`, - [path.join(d, 'b.ts')]: `export const x = 'exec-ts-tsconfig-load-ts';`, + [path.join(d, 'tsconfig.json')]: tsconfigPathContent, + [path.join(d, 'a.ts')]: tsconfigPathAContent, + [path.join(d, 'b.ts')]: tsconfigPathBContent, }); - const { code, stdout } = await executeProcess({ + const { code, stdout, stderr } = await executeProcess({ command: 'npx', args: [ '@push-based/jiti-tsc', @@ -154,23 +191,21 @@ console.log('args:', process.argv.slice(2));`, }); expect(code).toBe(0); - expect(stripAnsi(stdout)).toContain("args: [ '--tsconfig-arg=path-test' ]"); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--tsconfig-arg=path-test' ]", + ); await cleanup(); }); it('should exec .ts with tsconfig.path loading .ts with tsconfig.path', async () => { const d = getTestDir('exec-ts-tsconfig-load-ts-tsconfig'); const cleanup = await fsFromJson({ - [path.join(d, 'tsconfig.json')]: { - compilerOptions: { baseUrl: '.', paths: { '@/*': ['./*'] } }, - }, - [path.join(d, 'a.ts')]: `import { x } from '@/b.js'; console.log(x); -console.log('args:', process.argv.slice(2));`, - [path.join(d, 'b.ts')]: `import { y } from '@/c.js'; export const x = y;`, - [path.join(d, 'c.ts')]: - `export const y = 'exec-ts-tsconfig-load-ts-tsconfig';`, + [path.join(d, 'tsconfig.json')]: tsconfigPathContent, + [path.join(d, 'a.ts')]: tsconfigPathAContent, + [path.join(d, 'b.ts')]: tsconfigPathComplexBContent, + [path.join(d, 'c.ts')]: tsconfigPathComplexCContent, }); - const { code, stdout } = await executeProcess({ + const { code, stdout, stderr } = await executeProcess({ command: 'npx', args: [ '@push-based/jiti-tsc', @@ -186,20 +221,19 @@ console.log('args:', process.argv.slice(2));`, }); expect(code).toBe(0); - expect(stripAnsi(stdout)).toContain("args: [ '--complex-arg=multi-file' ]"); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--complex-arg=multi-file' ]", + ); await cleanup(); }); it('should execute ts file with --import jiti-tsc', async () => { const baseFolder = getTestDir('exec-jiti-tsc'); const cleanup = await fsFromJson({ - [path.join(baseFolder, 'bin.ts')]: `#!/usr/bin/env node -console.log(\`Executed over --import jiti-tsc\`); -console.log('args:', process.argv.slice(2)); -`, + [path.join(baseFolder, 'bin.ts')]: binImportJitiTscContent, }); - const { code, stdout } = await executeProcess({ + const { code, stdout, stderr } = await executeProcess({ command: process.execPath, args: [ path.relative(envRoot, path.join(baseFolder, 'bin.ts')), @@ -213,8 +247,12 @@ console.log('args:', process.argv.slice(2)); }); expect(code).toBe(0); - expect(stripAnsi(stdout)).toContain('Executed over --import jiti-tsc'); - expect(stripAnsi(stdout)).toContain("args: [ '--myArg=42' ]"); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'Executed over --import jiti-tsc', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--myArg=42' ]", + ); await cleanup(); }); diff --git a/packages/jiti-tsc/src/cli.ts b/packages/jiti-tsc/src/cli.ts index 1627ed6..88c31e3 100644 --- a/packages/jiti-tsc/src/cli.ts +++ b/packages/jiti-tsc/src/cli.ts @@ -19,7 +19,6 @@ const isFile = /\.(ts|tsx|mts|cts)$/.test(cmd); if (isFile) { - // ✅ TypeScript file: Use --import to enable jiti-tsc TypeScript support const child = spawn( process.execPath, ['--import', '@push-based/jiti-tsc', cmd, ...rest], @@ -27,10 +26,9 @@ if (isFile) { ); child.on('exit', code => process.exit(code ?? 1)); } else { - // ✅ Shell command: Run directly with shell support for npm scripts/binaries const child = spawn(cmd, rest, { stdio: 'inherit', - shell: true, // important for npm-installed binaries + shell: true, // important for npm-installed binaries on windows }); child.on('exit', code => process.exit(code ?? 1)); } From 103be8cbde45fa9b8341dd231b4e26c7e1661073 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Wed, 4 Feb 2026 00:08:21 +0100 Subject: [PATCH 05/10] fix: refactor --- ...i.e2e.test.ts => cli-jiti-tsc.e2e.test.ts} | 0 .../tests/register-jiti-tsc.e2e.test.ts | 276 ++++++++++++++++++ packages/jiti-tsc/package.json | 6 +- packages/jiti-tsc/src/cli.ts | 2 +- packages/jiti-tsc/src/index.ts | 32 +- .../src/lib/jiti/import-module.int.test.ts | 86 ------ .../jiti-tsc/src/lib/jiti/import-module.ts | 61 +++- .../src/lib/jiti/import-module.unit.test.ts | 18 ++ packages/jiti-tsc/src/loader.ts | 20 ++ 9 files changed, 380 insertions(+), 121 deletions(-) rename e2e/ts-jiti-e2e/tests/{jiti.e2e.test.ts => cli-jiti-tsc.e2e.test.ts} (100%) create mode 100644 e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts create mode 100644 packages/jiti-tsc/src/loader.ts diff --git a/e2e/ts-jiti-e2e/tests/jiti.e2e.test.ts b/e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts similarity index 100% rename from e2e/ts-jiti-e2e/tests/jiti.e2e.test.ts rename to e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts diff --git a/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts b/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts new file mode 100644 index 0000000..dbd33c8 --- /dev/null +++ b/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts @@ -0,0 +1,276 @@ +import { nxTargetProject } from '@push-based/test-nx-utils'; +import { + E2E_ENVIRONMENTS_DIR, + TEST_OUTPUT_DIR, + executeProcess, + fsFromJson, + removeColorCodes, +} from '@push-based/test-utils'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { expect } from 'vitest'; + +const __filename = fileURLToPath(import.meta.url); +const testFileName = path.basename(__filename); + +const toSlug = (str: string): string => + str + .toLowerCase() + .replace(/\s+/g, '-') + .replace(/[^a-z\d-]/g, ''); + +const binImportJitiTscContent = `#!/usr/bin/env node +console.log(\`Executed over --import @push-based/jiti-tsc/register\`); +console.log('args:', process.argv.slice(2)); +`; + +const loadTsAContent = `import { x } from './b.ts'; console.log(x); +console.log('args:', process.argv.slice(2));`; + +const loadTsBContent = `export const x = 'load-ts-loader';`; + +const execTsAContent = `console.log('exec-ts-loader'); +console.log('args:', process.argv.slice(2));`; + +const execTsLoadTsAContent = `import { x } from './b.js'; console.log(x); +console.log('args:', process.argv.slice(2));`; + +const execTsLoadTsBContent = `export const x = 'exec-ts-load-ts-loader';`; + +const tsconfigPathAContent = `import { x } from '@/b.js'; console.log(x); +console.log('args:', process.argv.slice(2));`; + +const tsconfigPathBContent = `export const x = 'exec-ts-tsconfig-load-ts-loader';`; + +const tsconfigPathComplexBContent = `import { y } from '@/c.js'; export const x = y;`; + +const tsconfigPathComplexCContent = `export const y = 'exec-ts-tsconfig-load-ts-tsconfig-loader';`; + +const tsconfigPathContent = { + compilerOptions: { baseUrl: '.', paths: { '@/*': ['./*'] } }, +}; + +describe('Loader hook (--import)', () => { + const envRoot = path.join(E2E_ENVIRONMENTS_DIR, nxTargetProject()); + const describeName = 'Loader hook (--import)'; + const describeSlug = toSlug(describeName); + + const getTestDir = (itName: string) => + path.join( + envRoot, + TEST_OUTPUT_DIR, + testFileName, + describeSlug, + toSlug(itName), + ); + + it('should execute ts file with --import @push-based/jiti-tsc/register loader hook', async () => { + const baseFolder = getTestDir('exec-loader-hook'); + const cleanup = await fsFromJson({ + [path.join(baseFolder, 'bin.ts')]: binImportJitiTscContent, + }); + + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + '--import', + '@push-based/jiti-tsc/register', + path.relative(envRoot, path.join(baseFolder, 'bin.ts')), + '--myArg=42', + ], + cwd: envRoot, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'Executed over --import @push-based/jiti-tsc/register', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--myArg=42' ]", + ); + + await cleanup(); + }); + + it('should load .ts files through loader hook', async () => { + const d = getTestDir('load-ts-loader'); + const cleanup = await fsFromJson({ + [path.join(d, 'a.ts')]: loadTsAContent, + [path.join(d, 'b.ts')]: loadTsBContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + '--import', + '@push-based/jiti-tsc/register', + path.relative(envRoot, path.join(d, 'a.ts')), + '--load-arg=test', + ], + cwd: envRoot, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'load-ts-loader', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--load-arg=test' ]", + ); + await cleanup(); + }); + + it('should exec .ts through loader hook', async () => { + const d = getTestDir('exec-ts-loader'); + const cleanup = await fsFromJson({ + [path.join(d, 'a.ts')]: execTsAContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + '--import', + '@push-based/jiti-tsc/register', + path.relative(envRoot, path.join(d, 'a.ts')), + '--exec-arg=value', + ], + cwd: envRoot, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'exec-ts-loader', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--exec-arg=value' ]", + ); + await cleanup(); + }); + + it('should exec .ts loading .ts through loader hook', async () => { + const d = getTestDir('exec-ts-load-ts-loader'); + const cleanup = await fsFromJson({ + [path.join(d, 'a.ts')]: execTsLoadTsAContent, + [path.join(d, 'b.ts')]: execTsLoadTsBContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + '--import', + '@push-based/jiti-tsc/register', + path.relative(envRoot, path.join(d, 'a.ts')), + '--load-ts-arg=hello', + ], + cwd: envRoot, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'exec-ts-load-ts-loader', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--load-ts-arg=hello' ]", + ); + await cleanup(); + }); + + it('should exec .ts with tsconfig.path loading .ts through loader hook', async () => { + const d = getTestDir('exec-ts-tsconfig-load-ts-loader'); + const cleanup = await fsFromJson({ + [path.join(d, 'tsconfig.json')]: tsconfigPathContent, + [path.join(d, 'a.ts')]: tsconfigPathAContent, + [path.join(d, 'b.ts')]: tsconfigPathBContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + '--import', + '@push-based/jiti-tsc/register', + path.relative(envRoot, path.join(d, 'a.ts')), + '--tsconfig-arg=path-test', + ], + cwd: envRoot, + env: { + ...process.env, + JITI_TSCONFIG_PATH: path.resolve(path.join(d, 'tsconfig.json')), + }, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'exec-ts-tsconfig-load-ts-loader', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--tsconfig-arg=path-test' ]", + ); + await cleanup(); + }); + + it('should exec .ts with tsconfig.path loading .ts with tsconfig.path through loader hook', async () => { + const d = getTestDir('exec-ts-tsconfig-load-ts-tsconfig-loader'); + const cleanup = await fsFromJson({ + [path.join(d, 'tsconfig.json')]: tsconfigPathContent, + [path.join(d, 'a.ts')]: tsconfigPathAContent, + [path.join(d, 'b.ts')]: tsconfigPathComplexBContent, + [path.join(d, 'c.ts')]: tsconfigPathComplexCContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + '--import', + '@push-based/jiti-tsc/register', + path.relative(envRoot, path.join(d, 'a.ts')), + '--complex-arg=multi-file', + ], + cwd: envRoot, + env: { + ...process.env, + JITI_TSCONFIG_PATH: path.resolve(path.join(d, 'tsconfig.json')), + }, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'exec-ts-tsconfig-load-ts-tsconfig-loader', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--complex-arg=multi-file' ]", + ); + await cleanup(); + }); + + it('should work with NODE_OPTIONS environment variable', async () => { + const baseFolder = getTestDir('exec-node-options'); + const cleanup = await fsFromJson({ + [path.join(baseFolder, 'bin.ts')]: binImportJitiTscContent, + }); + + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + path.relative(envRoot, path.join(baseFolder, 'bin.ts')), + '--myArg=42', + ], + cwd: envRoot, + silent: true, + env: { + ...process.env, + NODE_OPTIONS: '--import @push-based/jiti-tsc/register', + }, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'Executed over --import @push-based/jiti-tsc/register', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--myArg=42' ]", + ); + + await cleanup(); + }); +}); diff --git a/packages/jiti-tsc/package.json b/packages/jiti-tsc/package.json index d77d09f..a30c049 100644 --- a/packages/jiti-tsc/package.json +++ b/packages/jiti-tsc/package.json @@ -10,8 +10,12 @@ "import": "./src/index.js", "types": "./src/index.d.ts" }, + "./register": { + "import": "./src/loader.js", + "types": "./src/index.d.ts" + }, "./jiti-tsc": { - "import": "./src/index.js", + "import": "./src/loader.js", "types": "./src/index.d.ts" } }, diff --git a/packages/jiti-tsc/src/cli.ts b/packages/jiti-tsc/src/cli.ts index 88c31e3..38a03de 100644 --- a/packages/jiti-tsc/src/cli.ts +++ b/packages/jiti-tsc/src/cli.ts @@ -21,7 +21,7 @@ const isFile = if (isFile) { const child = spawn( process.execPath, - ['--import', '@push-based/jiti-tsc', cmd, ...rest], + ['--import', '@push-based/jiti-tsc/register', cmd, ...rest], { stdio: 'inherit' }, ); child.on('exit', code => process.exit(code ?? 1)); diff --git a/packages/jiti-tsc/src/index.ts b/packages/jiti-tsc/src/index.ts index 602112c..0278d5d 100644 --- a/packages/jiti-tsc/src/index.ts +++ b/packages/jiti-tsc/src/index.ts @@ -1,20 +1,12 @@ -import { registerJitiTsconfig } from './lib/jiti/register.js'; - -// DX safety net: warn about incorrect usage with npm/npx -if (process.env['npm_execpath']) { - console.warn( - '[jiti-tsc] Detected npm/npx execution. ' + - 'Do not use --import jiti-tsc with npm.', - ); -} - -try { - await registerJitiTsconfig(); -} catch (error) { - // If registration fails (e.g., when not loaded as --import), try to handle gracefully - console.warn( - '[jiti-tsc] Registration failed, continuing without TypeScript support:', - error instanceof Error ? error.message : String(error), - ); - // Don't throw - allow the process to continue -} +// Export runtime functions for programmatic use +export { registerJitiTsconfig } from './lib/jiti/register.js'; +export { + importModule, + createTsJiti, + jitiOptionsFromTsConfig, +} from './lib/jiti/import-module.js'; +export { loadTargetConfig } from './lib/jiti/load-ts-config.js'; +export { + JITI_TSCONFIG_PATH_ENV_VAR, + JITI_VERBOSE_ENV_VAR, +} from './lib/jiti/constants.js'; diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.int.test.ts b/packages/jiti-tsc/src/lib/jiti/import-module.int.test.ts index 3d5f4db..e69de29 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.int.test.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.int.test.ts @@ -1,86 +0,0 @@ -import path from 'node:path'; -import { describe, expect, it, vi } from 'vitest'; -import { importModule } from './import-module.js'; - -describe('importModule', () => { - const mockDir = path.join( - process.cwd(), - 'packages', - 'jiti-tsc', - 'mocks', - 'fixtures', - ); - - it('should load a valid ES module', async () => { - await expect( - importModule({ - filepath: path.join(mockDir, 'valid-es-module-export.mjs'), - }), - ).resolves.toBe('valid-es-module-export'); - }); - - it('should load a valid CommonJS module', async () => { - await expect( - importModule({ - filepath: path.join(mockDir, 'valid-cjs-module-export.cjs'), - }), - ).resolves.toBe('valid-cjs-module-export'); - }); - - it('should load an ES module without default export', async () => { - await expect( - importModule({ - filepath: path.join(mockDir, 'no-default-export.mjs'), - }), - ).resolves.toEqual( - expect.objectContaining({ exportedVar: 'exported-variable' }), - ); - }); - - it('should load a valid TS module with a default export', async () => { - await expect( - importModule({ - filepath: path.join(mockDir, 'valid-ts-default-export.ts'), - }), - ).resolves.toBe('valid-ts-default-export'); - }); - - it('imports module with default tsconfig when tsconfig undefined', async () => { - vi.clearAllMocks(); - await expect( - importModule({ - filepath: path.join(mockDir, 'valid-ts-default-export.ts'), - }), - ).resolves.toBe('valid-ts-default-export'); - }); - - it('imports module with custom tsconfig', async () => { - vi.clearAllMocks(); - await expect( - importModule({ - filepath: path.join(mockDir, 'tsconfig-setup', 'import-alias.ts'), - tsconfig: path.join(mockDir, 'tsconfig-setup', 'tsconfig.json'), - }), - ).resolves.toBe('valid-ts-default-export-utils-export'); - }); - - it('should throw if the file does not exist', async () => { - await expect( - importModule({ filepath: 'path/to/non-existent-export.mjs' }), - ).rejects.toThrow( - `File '${path.resolve('path/to/non-existent-export.mjs')}' does not exist`, - ); - }); - - it('should throw if path is a directory', async () => { - await expect(importModule({ filepath: mockDir })).rejects.toThrow( - `Expected '${mockDir}' to be a file`, - ); - }); - - it('should load valid JSON', async () => { - await expect( - importModule({ filepath: path.join(mockDir, 'invalid-js-file.json') }), - ).resolves.toStrictEqual({ key: 'value' }); - }); -}); diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.ts b/packages/jiti-tsc/src/lib/jiti/import-module.ts index dc211cc..e752e9c 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.ts @@ -1,18 +1,44 @@ import { createJiti as createJitiSource } from 'jiti'; import { stat } from 'node:fs/promises'; import path from 'node:path'; +import { pathToFileURL } from 'node:url'; import type { CompilerOptions } from 'typescript'; -import { fileExists } from '../utils/file-system.js'; -import { settlePromise } from '../utils/promises.js'; import { loadTargetConfig } from './load-ts-config.js'; +import { settlePromise } from './promises.js'; // For unknown reason, we can't import `JitiOptions` directly in this repository type JitiOptions = Exclude[1], undefined>; +/** + * Known packages that must be loaded natively (not transformed by jiti). + * These packages rely on import.meta.url being a real file:// URL. + * When jiti transforms modules, import.meta.url becomes 'about:blank', + * causing errors in packages that use new URL(..., import.meta.url). + */ +export const JITI_NATIVE_MODULES = [ + //'@vitest/eslint-plugin', + //'@code-pushup/eslint-config', + //'lighthouse', +] as const; + export type ImportModuleOptions = JitiOptions & { filepath: string; tsconfig?: string; }; + +export function toFileUrl(filepath: string): string { + // Handle Windows absolute paths (C:\Users\... or C:/Users/...) on all platforms + // pathToFileURL on non-Windows systems treats Windows paths as relative paths + const windowsAbsolutePathMatch = filepath.match(/^([A-Za-z]:)([\\/].*)$/); + if (windowsAbsolutePathMatch) { + const [, drive, rest] = windowsAbsolutePathMatch; + // Normalize backslashes to forward slashes and construct file URL manually + const normalizedPath = `${drive}${rest?.replace(/\\/g, '/')}`; + return `file:///${normalizedPath}`; + } + return pathToFileURL(filepath).href; +} + export async function importModule( options: ImportModuleOptions, ): Promise { @@ -38,7 +64,9 @@ export async function importModule( tsconfigPath: tsconfig, }); - return (await jitiInstance.import(absoluteFilePath, { default: true })) as T; + return (await jitiInstance.import(absoluteFilePath, { + default: true, + })) as T; } /** @@ -91,6 +119,7 @@ export const mapTsJsxToJitiJsx = (tsJsxMode: number): boolean => * | interopDefault | boolean | esModuleInterop | boolean | Enable default import interop. | * | sourceMaps | boolean | sourceMap | boolean | Enable sourcemap generation. | * | jsx | boolean | jsx | JsxEmit (0-5) | TS JsxEmit enum (0-5) => boolean JSX processing. | + * | nativeModules | string[] | - | - | Modules to load natively without jiti transformation. | */ export type MappableJitiOptions = Partial< Pick< @@ -148,21 +177,27 @@ export async function createTsJiti( createJiti: (typeof import('jiti'))['createJiti'] = createJitiSource, ) { const { tsconfigPath, ...jitiOptions } = options; - - const fallbackTsconfigPath = path.resolve(process.cwd(), 'tsconfig.json'); - const validPath: null | string = - tsconfigPath == null - ? (await fileExists(fallbackTsconfigPath)) - ? fallbackTsconfigPath - : null - : path.resolve(process.cwd(), tsconfigPath); - + tsconfigPath != null ? path.resolve(process.cwd(), tsconfigPath) : null; const tsDerivedJitiOptions: MappableJitiOptions = validPath ? await jitiOptionsFromTsConfig(validPath) : {}; - return createJiti(id, { ...jitiOptions, ...tsDerivedJitiOptions }); + return createJiti(id, { + ...jitiOptions, + ...tsDerivedJitiOptions, + alias: { + ...jitiOptions.alias, + ...tsDerivedJitiOptions.alias, + }, + nativeModules: [ + ...new Set([ + ...JITI_NATIVE_MODULES, + ...(jitiOptions.nativeModules ?? []), + ]), + ], + tryNative: true, + }); } /** diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.unit.test.ts b/packages/jiti-tsc/src/lib/jiti/import-module.unit.test.ts index a5e8e89..6d2e5d0 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.unit.test.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.unit.test.ts @@ -1,8 +1,11 @@ +import path from 'node:path'; +import { pathToFileURL } from 'node:url'; import type { CompilerOptions } from 'typescript'; import { describe, expect, it } from 'vitest'; import { mapTsPathsToJitiAlias, parseTsConfigToJitiConfig, + toFileUrl, } from './import-module.js'; describe('mapTsPathsToJitiAlias', () => { @@ -107,3 +110,18 @@ describe('parseTsConfigToJitiConfig', () => { }); }); }); + +describe('toFileUrl', () => { + it('returns a file:// URL for an absolute path', () => { + const absolutePath = path.resolve('some', 'config.ts'); + expect(toFileUrl(absolutePath)).toBe(pathToFileURL(absolutePath).href); + }); + + it('normalizes Windows absolute paths to file URLs for native import()', () => { + // Note: This is only needed for native import() calls, not for jiti.import() + // jiti handles Windows paths directly without URL conversion + // We should avoid native import calls in general + const windowsPath = path.win32.join('C:\\', 'Users', 'me', 'config.ts'); + expect(toFileUrl(windowsPath)).toBe('file:///C:/Users/me/config.ts'); + }); +}); diff --git a/packages/jiti-tsc/src/loader.ts b/packages/jiti-tsc/src/loader.ts new file mode 100644 index 0000000..8af6f4c --- /dev/null +++ b/packages/jiti-tsc/src/loader.ts @@ -0,0 +1,20 @@ +import { registerJitiTsconfig } from './lib/jiti/register.js'; + +// DX safety net: warn about incorrect usage with npm/npx +if (process.env['npm_execpath']) { + console.warn( + '[jiti-tsc] Detected npm/npx execution. ' + + 'Do not use --import @push-based/jiti-tsc/register with npm.', + ); +} + +try { + await registerJitiTsconfig(); +} catch (error) { + // If registration fails (e.g., when not loaded as --import), try to handle gracefully + console.warn( + '[jiti-tsc] Registration failed, continuing without TypeScript support:', + error instanceof Error ? error.message : String(error), + ); + // Don't throw - allow the process to continue +} From 6ff6e3dbbc2cbf1a59a6f96f8993cb42dc5ea46f Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Wed, 4 Feb 2026 00:13:57 +0100 Subject: [PATCH 06/10] fix: wip --- packages/jiti-tsc/src/lib/jiti/import-module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.ts b/packages/jiti-tsc/src/lib/jiti/import-module.ts index e752e9c..1c234ba 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { pathToFileURL } from 'node:url'; import type { CompilerOptions } from 'typescript'; import { loadTargetConfig } from './load-ts-config.js'; -import { settlePromise } from './promises.js'; +import { settlePromise } from '../utils/promises.js'; // For unknown reason, we can't import `JitiOptions` directly in this repository type JitiOptions = Exclude[1], undefined>; From c437a1133c6999c35d2af3d8e8af8897ca562913 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Wed, 4 Feb 2026 00:16:25 +0100 Subject: [PATCH 07/10] fix: wip --- .../jiti-tsc/src/lib/jiti/import-module.ts | 2 +- packages/jiti-tsc/tools/cli.ts | 33 ------------------- packages/jiti-tsc/tools/helper.ts | 3 -- packages/jiti-tsc/tools/test-app.ts | 8 ----- packages/jiti-tsc/tools/tsconfig.json | 10 ------ 5 files changed, 1 insertion(+), 55 deletions(-) delete mode 100644 packages/jiti-tsc/tools/cli.ts delete mode 100644 packages/jiti-tsc/tools/helper.ts delete mode 100644 packages/jiti-tsc/tools/test-app.ts delete mode 100644 packages/jiti-tsc/tools/tsconfig.json diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.ts b/packages/jiti-tsc/src/lib/jiti/import-module.ts index 1c234ba..4d3d962 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.ts @@ -178,7 +178,7 @@ export async function createTsJiti( ) { const { tsconfigPath, ...jitiOptions } = options; const validPath: null | string = - tsconfigPath != null ? path.resolve(process.cwd(), tsconfigPath) : null; + tsconfigPath == null ? null : path.resolve(process.cwd(), tsconfigPath); const tsDerivedJitiOptions: MappableJitiOptions = validPath ? await jitiOptionsFromTsConfig(validPath) : {}; diff --git a/packages/jiti-tsc/tools/cli.ts b/packages/jiti-tsc/tools/cli.ts deleted file mode 100644 index 8495ecb..0000000 --- a/packages/jiti-tsc/tools/cli.ts +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/env node -// Test script for jiti-tsc path aliases -console.log('Testing jiti-tsc path aliases:'); -console.log('NODE_OPTIONS:', process.env.NODE_OPTIONS); -console.log( - 'JITI_*:', - Object.entries(process.env).filter(([k]) => k.startsWith('JITI_')), -); - -// Test importing with path alias -try { - const { helper } = await import('@tools/helper'); - console.log('✓ Successfully imported @tools/helper'); - console.log('Result:', helper()); -} catch (e: unknown) { - console.log( - '✗ Failed to import @tools/helper:', - e instanceof Error ? e.message : String(e), - ); -} - -// Test importing jiti-tsc itself -try { - await import('@push-based/jiti-tsc'); - console.log('✓ jiti-tsc imported successfully'); -} catch (e: unknown) { - console.log( - '✗ Failed to import jiti-tsc:', - e instanceof Error ? e.message : String(e), - ); -} - -export {}; diff --git a/packages/jiti-tsc/tools/helper.ts b/packages/jiti-tsc/tools/helper.ts deleted file mode 100644 index 786698d..0000000 --- a/packages/jiti-tsc/tools/helper.ts +++ /dev/null @@ -1,3 +0,0 @@ -export function helper() { - return 'Hello from jiti-tsc tools!'; -} diff --git a/packages/jiti-tsc/tools/test-app.ts b/packages/jiti-tsc/tools/test-app.ts deleted file mode 100644 index 2328640..0000000 --- a/packages/jiti-tsc/tools/test-app.ts +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env node -import { helper } from '@tools/helper'; - -console.log('Testing jiti-tsc path aliases:'); -console.log('NODE_OPTIONS:', process.env.NODE_OPTIONS); -console.log('JITI_TSCONFIG_PATH:', process.env.JITI_TSCONFIG_PATH); -console.log('Result:', helper()); -console.log('✓ jiti-tsc working correctly!'); diff --git a/packages/jiti-tsc/tools/tsconfig.json b/packages/jiti-tsc/tools/tsconfig.json deleted file mode 100644 index 7d25004..0000000 --- a/packages/jiti-tsc/tools/tsconfig.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": "../../../tsconfig.base.json", - "compilerOptions": { - "baseUrl": ".", - "paths": { - "@tools/*": ["./*"] - } - }, - "include": ["*.ts"] -} From adf875d7af81a45b4ad8d239a8d73d48aeffd8aa Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Wed, 4 Feb 2026 00:41:13 +0100 Subject: [PATCH 08/10] fix: wip --- e2e/ts-jiti-e2e/mocks/README.md | 32 ++++ .../fixtures/path-aliases-basic/README.md | 15 ++ .../fixtures/path-aliases-basic/src/config.ts | 4 + .../fixtures/path-aliases-basic/src/index.ts | 8 + .../path-aliases-basic/src/utils/math.ts | 7 + .../path-aliases-basic/test-aliases.ts | 8 + .../fixtures/path-aliases-basic/tsconfig.json | 12 ++ .../fixtures/tsconfig-setup/import-alias.ts | 0 .../fixtures/tsconfig-setup/tsconfig.json | 0 .../mocks/fixtures/tsconfig-setup/utils.ts | 0 .../tests/cli-jiti-tsc.e2e.test.ts | 161 +++++++++++++++++ .../tests/register-jiti-tsc.e2e.test.ts | 165 ++++++++++++++++++ .../basic-setup/tsconfig-config-errors.json | 10 -- .../basic-setup/tsconfig.all-audits.json | 16 -- .../basic-setup/tsconfig.internal-errors.json | 8 - .../mocks/fixtures/basic-setup/tsconfig.json | 7 - .../basic-setup/tsconfig.no-files-match.json | 8 - .../mocks/fixtures/invalid-js-file.json | 1 - .../mocks/fixtures/no-default-export.mjs | 2 - .../fixtures/valid-cjs-module-export.cjs | 1 - .../mocks/fixtures/valid-es-module-export.mjs | 1 - .../mocks/fixtures/valid-ts-default-export.ts | 1 - packages/jiti-tsc/src/cli.ts | 37 ++-- .../src/lib/jiti/import-module.int.test.ts | 0 .../jiti-tsc/src/lib/jiti/import-module.ts | 7 +- packages/jiti-tsc/src/lib/jiti/register.ts | 48 +---- 26 files changed, 433 insertions(+), 126 deletions(-) create mode 100644 e2e/ts-jiti-e2e/mocks/README.md create mode 100644 e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/README.md create mode 100644 e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/config.ts create mode 100644 e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/index.ts create mode 100644 e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/utils/math.ts create mode 100644 e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/test-aliases.ts create mode 100644 e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/tsconfig.json rename {packages/jiti-tsc => e2e/ts-jiti-e2e}/mocks/fixtures/tsconfig-setup/import-alias.ts (100%) rename {packages/jiti-tsc => e2e/ts-jiti-e2e}/mocks/fixtures/tsconfig-setup/tsconfig.json (100%) rename {packages/jiti-tsc => e2e/ts-jiti-e2e}/mocks/fixtures/tsconfig-setup/utils.ts (100%) delete mode 100644 packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig-config-errors.json delete mode 100644 packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.all-audits.json delete mode 100644 packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.internal-errors.json delete mode 100644 packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.json delete mode 100644 packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.no-files-match.json delete mode 100644 packages/jiti-tsc/mocks/fixtures/invalid-js-file.json delete mode 100644 packages/jiti-tsc/mocks/fixtures/no-default-export.mjs delete mode 100644 packages/jiti-tsc/mocks/fixtures/valid-cjs-module-export.cjs delete mode 100644 packages/jiti-tsc/mocks/fixtures/valid-es-module-export.mjs delete mode 100644 packages/jiti-tsc/mocks/fixtures/valid-ts-default-export.ts delete mode 100644 packages/jiti-tsc/src/lib/jiti/import-module.int.test.ts diff --git a/e2e/ts-jiti-e2e/mocks/README.md b/e2e/ts-jiti-e2e/mocks/README.md new file mode 100644 index 0000000..9ed07c0 --- /dev/null +++ b/e2e/ts-jiti-e2e/mocks/README.md @@ -0,0 +1,32 @@ +# E2E Test Mocks + +This directory contains mock fixtures used in end-to-end tests for the jiti-tsc project. + +## Available Fixtures + +### `tsconfig-setup/` +Basic TypeScript compilation fixture with simple path alias setup: +- `@utils/*`: Maps to `./utils.ts` +- Minimal setup for testing basic TSC functionality + +### `path-aliases-basic/` +TypeScript with path aliases fixture: +- `@/*`: Maps to `./src/*` +- `@utils/*`: Maps to `./src/utils/*` +- Demonstrates path alias resolution capabilities + +## Usage + +These fixtures provide two levels of TypeScript testing: + +1. **Basic TSC**: Use `tsconfig-setup/` for testing fundamental TypeScript compilation +2. **TSC with Path Aliases**: Use `path-aliases-basic/` for testing path alias resolution + +Each fixture includes: +- `tsconfig.json`: TypeScript configuration +- Source files demonstrating the feature set +- Test files for verification + +## Testing + +Use these mocks to test jiti-tsc's ability to handle TypeScript compilation with and without path aliases. \ No newline at end of file diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/README.md b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/README.md new file mode 100644 index 0000000..4b6f485 --- /dev/null +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/README.md @@ -0,0 +1,15 @@ +# Path Aliases Basic Mock + +A simple mock fixture demonstrating basic TypeScript path aliases. + +## Path Aliases + +- `@/*`: Maps to `./src/*` +- `@utils/*`: Maps to `./src/utils/*` + +## Files + +- `tsconfig.json`: TypeScript configuration with basic path mappings +- `src/index.ts`: Main file using path aliases +- `src/utils/math.ts`: Utility functions +- `src/config.ts`: Configuration file \ No newline at end of file diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/config.ts b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/config.ts new file mode 100644 index 0000000..0063ef6 --- /dev/null +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/config.ts @@ -0,0 +1,4 @@ +export const config = { + baseValue: 10, + multiplier: 2 +} as const; \ No newline at end of file diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/index.ts b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/index.ts new file mode 100644 index 0000000..1e1ace5 --- /dev/null +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/index.ts @@ -0,0 +1,8 @@ +import { add, multiply } from '@utils/math'; +import { config } from '@/config'; + +export function calculate() { + const sum = add(config.baseValue, 5); + const product = multiply(sum, 2); + return { sum, product }; +} \ No newline at end of file diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/utils/math.ts b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/utils/math.ts new file mode 100644 index 0000000..88e0f4c --- /dev/null +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/utils/math.ts @@ -0,0 +1,7 @@ +export function add(a: number, b: number): number { + return a + b; +} + +export function multiply(a: number, b: number): number { + return a * b; +} \ No newline at end of file diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/test-aliases.ts b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/test-aliases.ts new file mode 100644 index 0000000..a7ca193 --- /dev/null +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/test-aliases.ts @@ -0,0 +1,8 @@ +// Test file to verify path aliases work +import { calculate } from './src/index'; +import { add } from '@utils/math'; +import { config } from '@/config'; + +console.log('Testing path aliases...'); +console.log('Direct import result:', calculate()); +console.log('Path alias import result:', add(config.baseValue, 3)); \ No newline at end of file diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/tsconfig.json b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/tsconfig.json new file mode 100644 index 0000000..ce5fa43 --- /dev/null +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@utils/*": ["./src/utils/*"] + } + }, + "include": ["**/*.ts"] +} \ No newline at end of file diff --git a/packages/jiti-tsc/mocks/fixtures/tsconfig-setup/import-alias.ts b/e2e/ts-jiti-e2e/mocks/fixtures/tsconfig-setup/import-alias.ts similarity index 100% rename from packages/jiti-tsc/mocks/fixtures/tsconfig-setup/import-alias.ts rename to e2e/ts-jiti-e2e/mocks/fixtures/tsconfig-setup/import-alias.ts diff --git a/packages/jiti-tsc/mocks/fixtures/tsconfig-setup/tsconfig.json b/e2e/ts-jiti-e2e/mocks/fixtures/tsconfig-setup/tsconfig.json similarity index 100% rename from packages/jiti-tsc/mocks/fixtures/tsconfig-setup/tsconfig.json rename to e2e/ts-jiti-e2e/mocks/fixtures/tsconfig-setup/tsconfig.json diff --git a/packages/jiti-tsc/mocks/fixtures/tsconfig-setup/utils.ts b/e2e/ts-jiti-e2e/mocks/fixtures/tsconfig-setup/utils.ts similarity index 100% rename from packages/jiti-tsc/mocks/fixtures/tsconfig-setup/utils.ts rename to e2e/ts-jiti-e2e/mocks/fixtures/tsconfig-setup/utils.ts diff --git a/e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts b/e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts index e44042a..ed0f8e3 100644 --- a/e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts +++ b/e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts @@ -55,6 +55,39 @@ const tsconfigPathContent = { compilerOptions: { baseUrl: '.', paths: { '@/*': ['./*'] } }, }; +const tsconfigMultiplePathsContent = { + compilerOptions: { + baseUrl: '.', + paths: { + '@/*': ['./*'], + '@components/*': ['./src/components/*'], + '@utils/*': ['./src/utils/*'], + '@lib': ['./src/lib/index.ts'], + '~/*': ['./src/*'], + }, + }, +}; + +const tsconfigPathUtilsContent = `import { helper } from '@utils/helpers'; console.log(helper); +console.log('args:', process.argv.slice(2));`; + +const tsconfigPathUtilsHelperContent = `export const helper = 'utils-helper';`; + +const tsconfigPathComponentsContent = `import { Button } from '@components/Button'; console.log(Button); +console.log('args:', process.argv.slice(2));`; + +const tsconfigPathButtonContent = `export const Button = 'Button-component';`; + +const tsconfigPathLibContent = `import lib from '@lib'; console.log(lib); +console.log('args:', process.argv.slice(2));`; + +const tsconfigPathLibIndexContent = `export default 'lib-index';`; + +const tsconfigPathTildeContent = `import { config } from '~/config/app'; console.log(config); +console.log('args:', process.argv.slice(2));`; + +const tsconfigPathAppConfigContent = `export const config = { app: 'config' };`; + describe('CLI jiti', () => { const envRoot = path.join(E2E_ENVIRONMENTS_DIR, nxTargetProject()); const describeName = 'CLI jiti'; @@ -227,6 +260,134 @@ describe('CLI jiti', () => { await cleanup(); }); + it('should exec .ts with multiple path aliases (@utils)', async () => { + const d = getTestDir('exec-ts-multiple-paths-utils'); + const cleanup = await fsFromJson({ + [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, + [path.join(d, 'a.ts')]: tsconfigPathUtilsContent, + [path.join(d, 'src', 'utils', 'helpers.ts')]: tsconfigPathUtilsHelperContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: 'npx', + args: [ + '@push-based/jiti-tsc', + path.relative(envRoot, path.join(d, 'a.ts')), + '--utils-arg=test', + ], + cwd: envRoot, + env: { + ...process.env, + JITI_TSCONFIG_PATH: path.resolve(path.join(d, 'tsconfig.json')), + }, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'utils-helper', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--utils-arg=test' ]", + ); + await cleanup(); + }); + + it('should exec .ts with multiple path aliases (@components)', async () => { + const d = getTestDir('exec-ts-multiple-paths-components'); + const cleanup = await fsFromJson({ + [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, + [path.join(d, 'a.ts')]: tsconfigPathComponentsContent, + [path.join(d, 'src', 'components', 'Button.ts')]: tsconfigPathButtonContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: 'npx', + args: [ + '@push-based/jiti-tsc', + path.relative(envRoot, path.join(d, 'a.ts')), + '--components-arg=test', + ], + cwd: envRoot, + env: { + ...process.env, + JITI_TSCONFIG_PATH: path.resolve(path.join(d, 'tsconfig.json')), + }, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'Button-component', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--components-arg=test' ]", + ); + await cleanup(); + }); + + it('should exec .ts with multiple path aliases (@lib exact match)', async () => { + const d = getTestDir('exec-ts-multiple-paths-lib'); + const cleanup = await fsFromJson({ + [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, + [path.join(d, 'a.ts')]: tsconfigPathLibContent, + [path.join(d, 'src', 'lib', 'index.ts')]: tsconfigPathLibIndexContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: 'npx', + args: [ + '@push-based/jiti-tsc', + path.relative(envRoot, path.join(d, 'a.ts')), + '--lib-arg=test', + ], + cwd: envRoot, + env: { + ...process.env, + JITI_TSCONFIG_PATH: path.resolve(path.join(d, 'tsconfig.json')), + }, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'lib-index', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--lib-arg=test' ]", + ); + await cleanup(); + }); + + it('should exec .ts with multiple path aliases (~ alias)', async () => { + const d = getTestDir('exec-ts-multiple-paths-tilde'); + const cleanup = await fsFromJson({ + [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, + [path.join(d, 'a.ts')]: tsconfigPathTildeContent, + [path.join(d, 'src', 'config', 'app.ts')]: tsconfigPathAppConfigContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: 'npx', + args: [ + '@push-based/jiti-tsc', + path.relative(envRoot, path.join(d, 'a.ts')), + '--tilde-arg=test', + ], + cwd: envRoot, + env: { + ...process.env, + JITI_TSCONFIG_PATH: path.resolve(path.join(d, 'tsconfig.json')), + }, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + '[object Object]', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--tilde-arg=test' ]", + ); + await cleanup(); + }); + it('should execute ts file with --import jiti-tsc', async () => { const baseFolder = getTestDir('exec-jiti-tsc'); const cleanup = await fsFromJson({ diff --git a/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts b/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts index dbd33c8..ac76985 100644 --- a/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts +++ b/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts @@ -50,6 +50,39 @@ const tsconfigPathContent = { compilerOptions: { baseUrl: '.', paths: { '@/*': ['./*'] } }, }; +const tsconfigMultiplePathsContent = { + compilerOptions: { + baseUrl: '.', + paths: { + '@/*': ['./*'], + '@components/*': ['./src/components/*'], + '@utils/*': ['./src/utils/*'], + '@lib': ['./src/lib/index.ts'], + '~/*': ['./src/*'], + }, + }, +}; + +const tsconfigPathUtilsContent = `import { helper } from '@utils/helpers'; console.log(helper); +console.log('args:', process.argv.slice(2));`; + +const tsconfigPathUtilsHelperContent = `export const helper = 'utils-helper-loader';`; + +const tsconfigPathComponentsContent = `import { Button } from '@components/Button'; console.log(Button); +console.log('args:', process.argv.slice(2));`; + +const tsconfigPathButtonContent = `export const Button = 'Button-component-loader';`; + +const tsconfigPathLibContent = `import lib from '@lib'; console.log(lib); +console.log('args:', process.argv.slice(2));`; + +const tsconfigPathLibIndexContent = `export default 'lib-index-loader';`; + +const tsconfigPathTildeContent = `import { config } from '~/config/app'; console.log(config); +console.log('args:', process.argv.slice(2));`; + +const tsconfigPathAppConfigContent = `export const config = { app: 'config-loader' };`; + describe('Loader hook (--import)', () => { const envRoot = path.join(E2E_ENVIRONMENTS_DIR, nxTargetProject()); const describeName = 'Loader hook (--import)'; @@ -243,6 +276,138 @@ describe('Loader hook (--import)', () => { await cleanup(); }); + it('should exec .ts with multiple path aliases (@utils) through loader hook', async () => { + const d = getTestDir('exec-ts-multiple-paths-utils-loader'); + const cleanup = await fsFromJson({ + [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, + [path.join(d, 'a.ts')]: tsconfigPathUtilsContent, + [path.join(d, 'src', 'utils', 'helpers.ts')]: tsconfigPathUtilsHelperContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + '--import', + '@push-based/jiti-tsc/register', + path.relative(envRoot, path.join(d, 'a.ts')), + '--utils-arg=test', + ], + cwd: envRoot, + env: { + ...process.env, + JITI_TSCONFIG_PATH: path.resolve(path.join(d, 'tsconfig.json')), + }, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'utils-helper-loader', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--utils-arg=test' ]", + ); + await cleanup(); + }); + + it('should exec .ts with multiple path aliases (@components) through loader hook', async () => { + const d = getTestDir('exec-ts-multiple-paths-components-loader'); + const cleanup = await fsFromJson({ + [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, + [path.join(d, 'a.ts')]: tsconfigPathComponentsContent, + [path.join(d, 'src', 'components', 'Button.ts')]: tsconfigPathButtonContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + '--import', + '@push-based/jiti-tsc/register', + path.relative(envRoot, path.join(d, 'a.ts')), + '--components-arg=test', + ], + cwd: envRoot, + env: { + ...process.env, + JITI_TSCONFIG_PATH: path.resolve(path.join(d, 'tsconfig.json')), + }, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'Button-component-loader', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--components-arg=test' ]", + ); + await cleanup(); + }); + + it('should exec .ts with multiple path aliases (@lib exact match) through loader hook', async () => { + const d = getTestDir('exec-ts-multiple-paths-lib-loader'); + const cleanup = await fsFromJson({ + [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, + [path.join(d, 'a.ts')]: tsconfigPathLibContent, + [path.join(d, 'src', 'lib', 'index.ts')]: tsconfigPathLibIndexContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + '--import', + '@push-based/jiti-tsc/register', + path.relative(envRoot, path.join(d, 'a.ts')), + '--lib-arg=test', + ], + cwd: envRoot, + env: { + ...process.env, + JITI_TSCONFIG_PATH: path.resolve(path.join(d, 'tsconfig.json')), + }, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'lib-index-loader', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--lib-arg=test' ]", + ); + await cleanup(); + }); + + it('should exec .ts with multiple path aliases (~ alias) through loader hook', async () => { + const d = getTestDir('exec-ts-multiple-paths-tilde-loader'); + const cleanup = await fsFromJson({ + [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, + [path.join(d, 'a.ts')]: tsconfigPathTildeContent, + [path.join(d, 'src', 'config', 'app.ts')]: tsconfigPathAppConfigContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: process.execPath, + args: [ + '--import', + '@push-based/jiti-tsc/register', + path.relative(envRoot, path.join(d, 'a.ts')), + '--tilde-arg=test', + ], + cwd: envRoot, + env: { + ...process.env, + JITI_TSCONFIG_PATH: path.resolve(path.join(d, 'tsconfig.json')), + }, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + '[object Object]', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--tilde-arg=test' ]", + ); + await cleanup(); + }); + it('should work with NODE_OPTIONS environment variable', async () => { const baseFolder = getTestDir('exec-node-options'); const cleanup = await fsFromJson({ diff --git a/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig-config-errors.json b/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig-config-errors.json deleted file mode 100644 index c68a4d4..0000000 --- a/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig-config-errors.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "compilerOptions": { - "rootDir": "./src", - "strict": true, - "verbatimModuleSyntax": false, - "target": "ES6", - "module": "CommonJS" - }, - "include": ["src/**/*.ts", "./out/**/*.ts", "nonexistent-file.ts"] -} diff --git a/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.all-audits.json b/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.all-audits.json deleted file mode 100644 index 2817c9d..0000000 --- a/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.all-audits.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "compilerOptions": { - "rootDir": "./src", - "strict": true, - "verbatimModuleSyntax": false, - "target": "ES6", - "module": "CommonJS" - }, - "exclude": ["exclude"], - "include": [ - "src/1-syntax-errors/**/*.ts", - "src/2-semantic-errors/**/*.ts", - "src/4-languale-service/**/*.ts", - "src/6-config-errors/**/*.ts" - ] -} diff --git a/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.internal-errors.json b/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.internal-errors.json deleted file mode 100644 index 1ee4ec6..0000000 --- a/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.internal-errors.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "rootDir": "./", - "module": "CommonJS", - "out": "./dist/bundle.js" - }, - "include": ["src/0-no-diagnostics/**/*.ts"] -} diff --git a/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.json b/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.json deleted file mode 100644 index 474196e..0000000 --- a/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "rootDir": "./", - "module": "CommonJS" - }, - "include": ["src/**/*.ts"] -} diff --git a/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.no-files-match.json b/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.no-files-match.json deleted file mode 100644 index 983a188..0000000 --- a/packages/jiti-tsc/mocks/fixtures/basic-setup/tsconfig.no-files-match.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "compilerOptions": { - "rootDir": "./src", - "strict": true, - "target": "ES6", - "module": "CommonJS" - } -} diff --git a/packages/jiti-tsc/mocks/fixtures/invalid-js-file.json b/packages/jiti-tsc/mocks/fixtures/invalid-js-file.json deleted file mode 100644 index 7ba1b79..0000000 --- a/packages/jiti-tsc/mocks/fixtures/invalid-js-file.json +++ /dev/null @@ -1 +0,0 @@ -{ "key": "value" } diff --git a/packages/jiti-tsc/mocks/fixtures/no-default-export.mjs b/packages/jiti-tsc/mocks/fixtures/no-default-export.mjs deleted file mode 100644 index 2ed8bca..0000000 --- a/packages/jiti-tsc/mocks/fixtures/no-default-export.mjs +++ /dev/null @@ -1,2 +0,0 @@ -export const exportedVar = 'exported-variable'; -const internalVar = 'internal-variable'; diff --git a/packages/jiti-tsc/mocks/fixtures/valid-cjs-module-export.cjs b/packages/jiti-tsc/mocks/fixtures/valid-cjs-module-export.cjs deleted file mode 100644 index 411718b..0000000 --- a/packages/jiti-tsc/mocks/fixtures/valid-cjs-module-export.cjs +++ /dev/null @@ -1 +0,0 @@ -module.exports = 'valid-cjs-module-export'; diff --git a/packages/jiti-tsc/mocks/fixtures/valid-es-module-export.mjs b/packages/jiti-tsc/mocks/fixtures/valid-es-module-export.mjs deleted file mode 100644 index 1335a16..0000000 --- a/packages/jiti-tsc/mocks/fixtures/valid-es-module-export.mjs +++ /dev/null @@ -1 +0,0 @@ -export default 'valid-es-module-export'; diff --git a/packages/jiti-tsc/mocks/fixtures/valid-ts-default-export.ts b/packages/jiti-tsc/mocks/fixtures/valid-ts-default-export.ts deleted file mode 100644 index 5243d58..0000000 --- a/packages/jiti-tsc/mocks/fixtures/valid-ts-default-export.ts +++ /dev/null @@ -1 +0,0 @@ -export default 'valid-ts-default-export'; diff --git a/packages/jiti-tsc/src/cli.ts b/packages/jiti-tsc/src/cli.ts index 38a03de..360d613 100644 --- a/packages/jiti-tsc/src/cli.ts +++ b/packages/jiti-tsc/src/cli.ts @@ -1,34 +1,21 @@ #!/usr/bin/env node import { spawn } from 'node:child_process'; -import fs from 'node:fs'; -const [, , cmd, ...rest] = process.argv; +const [_, __, cmd, ...rest] = process.argv; if (!cmd) { console.error('Usage: jiti-tsc [...args]'); + // eslint-ignore-next-line n/no-process-exit process.exit(1); } -// Detect if the first argument is a TypeScript file or a command -// This allows the CLI to work in two modes: -// 1. jiti-tsc file.ts → runs TypeScript files with jiti support -// 2. jiti-tsc command args → runs shell commands (npm scripts, binaries, etc.) -const isFile = - fs.existsSync(cmd) && - fs.statSync(cmd).isFile() && - /\.(ts|tsx|mts|cts)$/.test(cmd); - -if (isFile) { - const child = spawn( - process.execPath, - ['--import', '@push-based/jiti-tsc/register', cmd, ...rest], - { stdio: 'inherit' }, - ); - child.on('exit', code => process.exit(code ?? 1)); -} else { - const child = spawn(cmd, rest, { - stdio: 'inherit', - shell: true, // important for npm-installed binaries on windows - }); - child.on('exit', code => process.exit(code ?? 1)); -} +// Always use the register entry point for consistent loader hook behavior +const child = spawn( + process.execPath, + ['--import', '@push-based/jiti-tsc/register', cmd, ...rest], + { stdio: 'inherit' }, +); +child.on('exit', code => { + // eslint-ignore-next-line n/no-process-exit + process.exit(code ?? 1); +}); diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.int.test.ts b/packages/jiti-tsc/src/lib/jiti/import-module.int.test.ts deleted file mode 100644 index e69de29..0000000 diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.ts b/packages/jiti-tsc/src/lib/jiti/import-module.ts index 4d3d962..649219e 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.ts @@ -5,6 +5,8 @@ import { pathToFileURL } from 'node:url'; import type { CompilerOptions } from 'typescript'; import { loadTargetConfig } from './load-ts-config.js'; import { settlePromise } from '../utils/promises.js'; +import * as process from 'node:process'; +import { JITI_TSCONFIG_PATH_ENV_VAR } from './constants'; // For unknown reason, we can't import `JitiOptions` directly in this repository type JitiOptions = Exclude[1], undefined>; @@ -169,7 +171,7 @@ export function parseTsConfigToJitiConfig( * Used instead of direct jiti.createJiti to allow tsconfig integration. * @param id * @param options - * @param jiti + * @param createJiti */ export async function createTsJiti( id: string, @@ -177,8 +179,9 @@ export async function createTsJiti( createJiti: (typeof import('jiti'))['createJiti'] = createJitiSource, ) { const { tsconfigPath, ...jitiOptions } = options; + const parsedTsconfigPath = process.env[JITI_TSCONFIG_PATH_ENV_VAR] ?? tsconfigPath; const validPath: null | string = - tsconfigPath == null ? null : path.resolve(process.cwd(), tsconfigPath); + parsedTsconfigPath == null ? null : path.resolve(process.cwd(), parsedTsconfigPath); const tsDerivedJitiOptions: MappableJitiOptions = validPath ? await jitiOptionsFromTsConfig(validPath) : {}; diff --git a/packages/jiti-tsc/src/lib/jiti/register.ts b/packages/jiti-tsc/src/lib/jiti/register.ts index 27370f5..4603bb2 100644 --- a/packages/jiti-tsc/src/lib/jiti/register.ts +++ b/packages/jiti-tsc/src/lib/jiti/register.ts @@ -1,47 +1,7 @@ -// src/core/register.ts -import path from 'node:path'; -import { JITI_TSCONFIG_PATH_ENV_VAR } from './constants.js'; -import { jitiOptionsFromTsConfig } from './import-module.js'; -import { jitiOptionsToEnv } from './jiti.schema.js'; +import { createTsJiti } from './import-module.js'; export async function registerJitiTsconfig() { - const tsconfigPathEnv = process.env[JITI_TSCONFIG_PATH_ENV_VAR]; - const tsconfigPath = tsconfigPathEnv - ? path.isAbsolute(tsconfigPathEnv) - ? tsconfigPathEnv - : path.resolve(process.cwd(), tsconfigPathEnv) - : path.resolve(process.cwd(), 'tsconfig.json'); - - try { - const jitiOptions = await jitiOptionsFromTsConfig(tsconfigPath); - // Add semver and other problematic modules to nativeModules to avoid Node.js v24 issues - jitiOptions.nativeModules = [ - ...(jitiOptions.nativeModules || []), - 'semver', - 'semver/classes/range', - 'semver/classes/comparator', - ]; - const envVars = jitiOptionsToEnv(jitiOptions); - - Object.entries(envVars).forEach( - // eslint-disable-next-line functional/immutable-data - ([k, v]) => v != null && (process.env[k] = v), - ); - - // Debug logging when JITI_DEBUG is enabled - if (process.env['JITI_DEBUG']) { - console.log('[jiti-tsc] NODE_OPTIONS:', process.env['NODE_OPTIONS']); - console.log('[jiti-tsc] JITI_* env vars:'); - Object.entries(process.env) - .filter(([k]) => k.startsWith('JITI_')) - .forEach(([k, v]) => console.log(k, v)); - } - } catch { - console.warn( - `[jiti-tsc] Failed to load tsconfig from ${tsconfigPath}, continuing without tsconfig`, - ); - } - - // @ts-expect-error - jiti/register is a side-effect import - await import('jiti/register'); + const jitiInstance = await createTsJiti(import.meta.url); + // @ts-expect-error - register method exists but is not in types + return jitiInstance.register(); } From 894b4ff2a863a883cb0ff62ef30d8765fd444276 Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Wed, 4 Feb 2026 00:56:46 +0100 Subject: [PATCH 09/10] fix: wip --- .../tests/cli-jiti-tsc.e2e.test.ts | 8 ++-- .../tests/register-jiti-tsc.e2e.test.ts | 8 ++-- packages/jiti-tsc/README.md | 40 ++++++++++++++++--- packages/jiti-tsc/src/cli.ts | 4 +- packages/jiti-tsc/src/lib/jiti/env-vars.ts | 28 +++++++++++++ .../jiti-tsc/src/lib/jiti/import-module.ts | 34 +++++----------- .../src/lib/jiti/import-module.unit.test.ts | 18 --------- packages/jiti-tsc/src/lib/jiti/jiti.schema.ts | 33 --------------- packages/jiti-tsc/src/lib/jiti/register.ts | 24 +++++++++-- packages/jiti-tsc/src/lib/utils/promises.ts | 10 ----- 10 files changed, 104 insertions(+), 103 deletions(-) create mode 100644 packages/jiti-tsc/src/lib/jiti/env-vars.ts delete mode 100644 packages/jiti-tsc/src/lib/jiti/jiti.schema.ts delete mode 100644 packages/jiti-tsc/src/lib/utils/promises.ts diff --git a/e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts b/e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts index ed0f8e3..c14f7ce 100644 --- a/e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts +++ b/e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts @@ -265,7 +265,8 @@ describe('CLI jiti', () => { const cleanup = await fsFromJson({ [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, [path.join(d, 'a.ts')]: tsconfigPathUtilsContent, - [path.join(d, 'src', 'utils', 'helpers.ts')]: tsconfigPathUtilsHelperContent, + [path.join(d, 'src', 'utils', 'helpers.ts')]: + tsconfigPathUtilsHelperContent, }); const { code, stdout, stderr } = await executeProcess({ command: 'npx', @@ -297,7 +298,8 @@ describe('CLI jiti', () => { const cleanup = await fsFromJson({ [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, [path.join(d, 'a.ts')]: tsconfigPathComponentsContent, - [path.join(d, 'src', 'components', 'Button.ts')]: tsconfigPathButtonContent, + [path.join(d, 'src', 'components', 'Button.ts')]: + tsconfigPathButtonContent, }); const { code, stdout, stderr } = await executeProcess({ command: 'npx', @@ -380,7 +382,7 @@ describe('CLI jiti', () => { expect(code).toBe(0); expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( - '[object Object]', + "{ app: 'config' }", ); expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( "args: [ '--tilde-arg=test' ]", diff --git a/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts b/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts index ac76985..0fc606d 100644 --- a/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts +++ b/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts @@ -281,7 +281,8 @@ describe('Loader hook (--import)', () => { const cleanup = await fsFromJson({ [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, [path.join(d, 'a.ts')]: tsconfigPathUtilsContent, - [path.join(d, 'src', 'utils', 'helpers.ts')]: tsconfigPathUtilsHelperContent, + [path.join(d, 'src', 'utils', 'helpers.ts')]: + tsconfigPathUtilsHelperContent, }); const { code, stdout, stderr } = await executeProcess({ command: process.execPath, @@ -314,7 +315,8 @@ describe('Loader hook (--import)', () => { const cleanup = await fsFromJson({ [path.join(d, 'tsconfig.json')]: tsconfigMultiplePathsContent, [path.join(d, 'a.ts')]: tsconfigPathComponentsContent, - [path.join(d, 'src', 'components', 'Button.ts')]: tsconfigPathButtonContent, + [path.join(d, 'src', 'components', 'Button.ts')]: + tsconfigPathButtonContent, }); const { code, stdout, stderr } = await executeProcess({ command: process.execPath, @@ -400,7 +402,7 @@ describe('Loader hook (--import)', () => { expect(code).toBe(0); expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( - '[object Object]', + "{ app: 'config-loader' }", ); expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( "args: [ '--tilde-arg=test' ]", diff --git a/packages/jiti-tsc/README.md b/packages/jiti-tsc/README.md index 1557130..4da2d2e 100644 --- a/packages/jiti-tsc/README.md +++ b/packages/jiti-tsc/README.md @@ -4,14 +4,44 @@ ## Features -This library does one simple thing: it converts TypeScript path aliases to jiti-compatible alias options and wrapd the original jiti API to make it easier to use in monorepo environments. +This library does one simple thing: it converts TypeScript path aliases to jiti-compatible alias options and wraps the original jiti API to make it easier to use in monorepo environments. + +## Architecture Overview + +```mermaid +graph TD + A[tsconfig.json] --> B[TypeScript Config Parser] + B --> C[TS Options → Jiti Options] + C --> D[Enhanced Jiti Instance] + + E[Original Jiti] --> F[createTsJiti Wrapper] + F --> D + + D --> G[CLI Tool] + D --> H[Loader Hook] + D --> I[Programmatic API] + + G --> J[npx jiti-tsc file.ts] + H --> K[--import jiti-tsc/register] + I --> L[importModule/createTsJiti] +``` + +**How it works:** +1. **Parse**: Reads and parses `tsconfig.json` using TypeScript's config utilities +2. **Map**: Converts TypeScript compiler options to jiti-compatible options: + - `paths` → `alias` (with absolute path resolution) + - `esModuleInterop` → `interopDefault` + - `sourceMap` → `sourceMaps` + - `jsx` → `jsx` (boolean) +3. **Wrap**: Creates jiti instance with merged options (TS-derived + user-provided) +4. **Enhance**: Adds environment variable configuration for global registration **✅ Full TypeScript configuration support:** -- ✅ FAutomatically detected from the `tsconfig.json` file in the current working directory -- ✅ FAccepts custom tsconfig path via the `JITI_TSCONFIG_PATH` -- ✅ FCLI tool supporting tsconfig options -- ✅ FProgrammatic API supporting tsconfig options +- ✅ Automatically detected from the `tsconfig.json` file in the current working directory +- ✅ Accepts custom tsconfig path via the `JITI_TSCONFIG_PATH` +- ✅ CLI tool supporting tsconfig options +- ✅ Programmatic API supporting tsconfig options ## Installation diff --git a/packages/jiti-tsc/src/cli.ts b/packages/jiti-tsc/src/cli.ts index 360d613..6632a7d 100644 --- a/packages/jiti-tsc/src/cli.ts +++ b/packages/jiti-tsc/src/cli.ts @@ -5,7 +5,7 @@ const [_, __, cmd, ...rest] = process.argv; if (!cmd) { console.error('Usage: jiti-tsc [...args]'); - // eslint-ignore-next-line n/no-process-exit + // eslint-disable-next-line n/no-process-exit process.exit(1); } @@ -16,6 +16,6 @@ const child = spawn( { stdio: 'inherit' }, ); child.on('exit', code => { - // eslint-ignore-next-line n/no-process-exit + // eslint-disable-next-line n/no-process-exit process.exit(code ?? 1); }); diff --git a/packages/jiti-tsc/src/lib/jiti/env-vars.ts b/packages/jiti-tsc/src/lib/jiti/env-vars.ts new file mode 100644 index 0000000..ae2877b --- /dev/null +++ b/packages/jiti-tsc/src/lib/jiti/env-vars.ts @@ -0,0 +1,28 @@ +import type { MappableJitiOptions } from './import-module.js'; + +/** + * Sets jiti environment variables based on the provided jiti configuration. + * These environment variables are used by jiti/register when loaded. + * @param jitiConfig The jiti configuration options to convert to environment variables + */ +export function setJitiEnvVars(jitiConfig: MappableJitiOptions): void { + if (jitiConfig.alias) { + // eslint-disable-next-line functional/immutable-data + process.env['JITI_ALIAS'] = JSON.stringify(jitiConfig.alias); + } + + if (jitiConfig.sourceMaps !== undefined) { + // eslint-disable-next-line functional/immutable-data + process.env['JITI_SOURCEMAPS'] = jitiConfig.sourceMaps.toString(); + } + + if (jitiConfig.interopDefault !== undefined) { + // eslint-disable-next-line functional/immutable-data + process.env['JITI_INTEROPDEFAULT'] = jitiConfig.interopDefault.toString(); + } + + if (jitiConfig.jsx !== undefined) { + // eslint-disable-next-line functional/immutable-data + process.env['JITI_JSX'] = jitiConfig.jsx.toString(); + } +} diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.ts b/packages/jiti-tsc/src/lib/jiti/import-module.ts index 649219e..2160599 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.ts @@ -1,12 +1,10 @@ import { createJiti as createJitiSource } from 'jiti'; -import { stat } from 'node:fs/promises'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; +import * as process from 'node:process'; import type { CompilerOptions } from 'typescript'; +import { fileExists } from '../utils/file-system.js'; +import { JITI_TSCONFIG_PATH_ENV_VAR } from './constants.js'; import { loadTargetConfig } from './load-ts-config.js'; -import { settlePromise } from '../utils/promises.js'; -import * as process from 'node:process'; -import { JITI_TSCONFIG_PATH_ENV_VAR } from './constants'; // For unknown reason, we can't import `JitiOptions` directly in this repository type JitiOptions = Exclude[1], undefined>; @@ -28,19 +26,6 @@ export type ImportModuleOptions = JitiOptions & { tsconfig?: string; }; -export function toFileUrl(filepath: string): string { - // Handle Windows absolute paths (C:\Users\... or C:/Users/...) on all platforms - // pathToFileURL on non-Windows systems treats Windows paths as relative paths - const windowsAbsolutePathMatch = filepath.match(/^([A-Za-z]:)([\\/].*)$/); - if (windowsAbsolutePathMatch) { - const [, drive, rest] = windowsAbsolutePathMatch; - // Normalize backslashes to forward slashes and construct file URL manually - const normalizedPath = `${drive}${rest?.replace(/\\/g, '/')}`; - return `file:///${normalizedPath}`; - } - return pathToFileURL(filepath).href; -} - export async function importModule( options: ImportModuleOptions, ): Promise { @@ -53,13 +38,9 @@ export async function importModule( } const absoluteFilePath = path.resolve(process.cwd(), filepath); - const resolvedStats = await settlePromise(stat(absoluteFilePath)); - if (resolvedStats.status === 'rejected') { + if (!(await fileExists(absoluteFilePath))) { throw new Error(`File '${absoluteFilePath}' does not exist`); } - if (!resolvedStats.value.isFile()) { - throw new Error(`Expected '${filepath}' to be a file`); - } const jitiInstance = await createTsJiti(import.meta.url, { ...jitiOptions, @@ -179,9 +160,12 @@ export async function createTsJiti( createJiti: (typeof import('jiti'))['createJiti'] = createJitiSource, ) { const { tsconfigPath, ...jitiOptions } = options; - const parsedTsconfigPath = process.env[JITI_TSCONFIG_PATH_ENV_VAR] ?? tsconfigPath; + const parsedTsconfigPath = + process.env[JITI_TSCONFIG_PATH_ENV_VAR] ?? tsconfigPath; const validPath: null | string = - parsedTsconfigPath == null ? null : path.resolve(process.cwd(), parsedTsconfigPath); + parsedTsconfigPath == null + ? null + : path.resolve(process.cwd(), parsedTsconfigPath); const tsDerivedJitiOptions: MappableJitiOptions = validPath ? await jitiOptionsFromTsConfig(validPath) : {}; diff --git a/packages/jiti-tsc/src/lib/jiti/import-module.unit.test.ts b/packages/jiti-tsc/src/lib/jiti/import-module.unit.test.ts index 6d2e5d0..a5e8e89 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.unit.test.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.unit.test.ts @@ -1,11 +1,8 @@ -import path from 'node:path'; -import { pathToFileURL } from 'node:url'; import type { CompilerOptions } from 'typescript'; import { describe, expect, it } from 'vitest'; import { mapTsPathsToJitiAlias, parseTsConfigToJitiConfig, - toFileUrl, } from './import-module.js'; describe('mapTsPathsToJitiAlias', () => { @@ -110,18 +107,3 @@ describe('parseTsConfigToJitiConfig', () => { }); }); }); - -describe('toFileUrl', () => { - it('returns a file:// URL for an absolute path', () => { - const absolutePath = path.resolve('some', 'config.ts'); - expect(toFileUrl(absolutePath)).toBe(pathToFileURL(absolutePath).href); - }); - - it('normalizes Windows absolute paths to file URLs for native import()', () => { - // Note: This is only needed for native import() calls, not for jiti.import() - // jiti handles Windows paths directly without URL conversion - // We should avoid native import calls in general - const windowsPath = path.win32.join('C:\\', 'Users', 'me', 'config.ts'); - expect(toFileUrl(windowsPath)).toBe('file:///C:/Users/me/config.ts'); - }); -}); diff --git a/packages/jiti-tsc/src/lib/jiti/jiti.schema.ts b/packages/jiti-tsc/src/lib/jiti/jiti.schema.ts deleted file mode 100644 index a28acfb..0000000 --- a/packages/jiti-tsc/src/lib/jiti/jiti.schema.ts +++ /dev/null @@ -1,33 +0,0 @@ -import type { JitiOptions } from 'jiti'; - -/** - * Converts jiti options to environment variables for the jiti CLI - * @param options Jiti options to convert - * @returns Environment variables object - */ -export function jitiOptionsToEnv(options: JitiOptions): Record { - return { - ...(options.alias && { JITI_ALIAS: JSON.stringify(options.alias) }), - ...(options.interopDefault !== undefined && { - JITI_INTEROP_DEFAULT: options.interopDefault ? '1' : '0', - }), - ...(options.sourceMaps !== undefined && { - JITI_SOURCE_MAPS: options.sourceMaps ? '1' : '0', - }), - ...(options.jsx !== undefined && { JITI_JSX: options.jsx ? '1' : '0' }), - }; -} - -/** - * - * @param env - */ -export function filterJitiEnvVars( - env: Record, -): Record { - return Object.fromEntries( - Object.entries(env) - .filter(([key]) => key.startsWith('JITI_')) - .map(([key, value]) => [key.replace('JITI_', ''), value]), - ) as Record; -} diff --git a/packages/jiti-tsc/src/lib/jiti/register.ts b/packages/jiti-tsc/src/lib/jiti/register.ts index 4603bb2..c92130c 100644 --- a/packages/jiti-tsc/src/lib/jiti/register.ts +++ b/packages/jiti-tsc/src/lib/jiti/register.ts @@ -1,7 +1,23 @@ -import { createTsJiti } from './import-module.js'; +import { JITI_TSCONFIG_PATH_ENV_VAR } from './constants.js'; +import { setJitiEnvVars } from './env-vars.js'; +import { parseTsConfigToJitiConfig } from './import-module.js'; +import { loadTargetConfig } from './load-ts-config.js'; export async function registerJitiTsconfig() { - const jitiInstance = await createTsJiti(import.meta.url); - // @ts-expect-error - register method exists but is not in types - return jitiInstance.register(); + const tsconfigPath = process.env[JITI_TSCONFIG_PATH_ENV_VAR]; + + if (tsconfigPath) { + try { + const { options } = loadTargetConfig(tsconfigPath); + const jitiConfig = parseTsConfigToJitiConfig(options, tsconfigPath); + setJitiEnvVars(jitiConfig); + } catch (error) { + console.warn('[jiti-tsc] Failed to load TypeScript config:', error); + } + } + + // Always import jiti/register for basic TypeScript support + // It will use the environment variables we set above + // @ts-expect-error - jiti/register is a side-effect import + await import('jiti/register'); } diff --git a/packages/jiti-tsc/src/lib/utils/promises.ts b/packages/jiti-tsc/src/lib/utils/promises.ts deleted file mode 100644 index 0261dec..0000000 --- a/packages/jiti-tsc/src/lib/utils/promises.ts +++ /dev/null @@ -1,10 +0,0 @@ -export async function settlePromise( - promise: Promise, -): Promise> { - try { - const value = await promise; - return { status: 'fulfilled', value }; - } catch (error) { - return { status: 'rejected', reason: error }; - } -} From d1dbe6037628459fe34c05b94a6002f1fde8c88d Mon Sep 17 00:00:00 2001 From: Michael Hladky Date: Wed, 4 Feb 2026 00:56:53 +0100 Subject: [PATCH 10/10] fix: wip --- e2e/ts-jiti-e2e/mocks/README.md | 7 ++++++- .../mocks/fixtures/path-aliases-basic/README.md | 2 +- .../mocks/fixtures/path-aliases-basic/src/config.ts | 4 ++-- .../mocks/fixtures/path-aliases-basic/src/index.ts | 4 ++-- .../mocks/fixtures/path-aliases-basic/src/utils/math.ts | 2 +- .../mocks/fixtures/path-aliases-basic/test-aliases.ts | 6 +++--- .../mocks/fixtures/path-aliases-basic/tsconfig.json | 2 +- packages/jiti-tsc/README.md | 1 + 8 files changed, 17 insertions(+), 11 deletions(-) diff --git a/e2e/ts-jiti-e2e/mocks/README.md b/e2e/ts-jiti-e2e/mocks/README.md index 9ed07c0..c800bf2 100644 --- a/e2e/ts-jiti-e2e/mocks/README.md +++ b/e2e/ts-jiti-e2e/mocks/README.md @@ -5,12 +5,16 @@ This directory contains mock fixtures used in end-to-end tests for the jiti-tsc ## Available Fixtures ### `tsconfig-setup/` + Basic TypeScript compilation fixture with simple path alias setup: + - `@utils/*`: Maps to `./utils.ts` - Minimal setup for testing basic TSC functionality ### `path-aliases-basic/` + TypeScript with path aliases fixture: + - `@/*`: Maps to `./src/*` - `@utils/*`: Maps to `./src/utils/*` - Demonstrates path alias resolution capabilities @@ -23,10 +27,11 @@ These fixtures provide two levels of TypeScript testing: 2. **TSC with Path Aliases**: Use `path-aliases-basic/` for testing path alias resolution Each fixture includes: + - `tsconfig.json`: TypeScript configuration - Source files demonstrating the feature set - Test files for verification ## Testing -Use these mocks to test jiti-tsc's ability to handle TypeScript compilation with and without path aliases. \ No newline at end of file +Use these mocks to test jiti-tsc's ability to handle TypeScript compilation with and without path aliases. diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/README.md b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/README.md index 4b6f485..8d2c4e4 100644 --- a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/README.md +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/README.md @@ -12,4 +12,4 @@ A simple mock fixture demonstrating basic TypeScript path aliases. - `tsconfig.json`: TypeScript configuration with basic path mappings - `src/index.ts`: Main file using path aliases - `src/utils/math.ts`: Utility functions -- `src/config.ts`: Configuration file \ No newline at end of file +- `src/config.ts`: Configuration file diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/config.ts b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/config.ts index 0063ef6..178885a 100644 --- a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/config.ts +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/config.ts @@ -1,4 +1,4 @@ export const config = { baseValue: 10, - multiplier: 2 -} as const; \ No newline at end of file + multiplier: 2, +} as const; diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/index.ts b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/index.ts index 1e1ace5..77c2f6d 100644 --- a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/index.ts +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/index.ts @@ -1,8 +1,8 @@ -import { add, multiply } from '@utils/math'; import { config } from '@/config'; +import { add, multiply } from '@utils/math'; export function calculate() { const sum = add(config.baseValue, 5); const product = multiply(sum, 2); return { sum, product }; -} \ No newline at end of file +} diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/utils/math.ts b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/utils/math.ts index 88e0f4c..341e651 100644 --- a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/utils/math.ts +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/utils/math.ts @@ -4,4 +4,4 @@ export function add(a: number, b: number): number { export function multiply(a: number, b: number): number { return a * b; -} \ No newline at end of file +} diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/test-aliases.ts b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/test-aliases.ts index a7ca193..1ce63ce 100644 --- a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/test-aliases.ts +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/test-aliases.ts @@ -1,8 +1,8 @@ // Test file to verify path aliases work -import { calculate } from './src/index'; -import { add } from '@utils/math'; import { config } from '@/config'; +import { add } from '@utils/math'; +import { calculate } from './src/index'; console.log('Testing path aliases...'); console.log('Direct import result:', calculate()); -console.log('Path alias import result:', add(config.baseValue, 3)); \ No newline at end of file +console.log('Path alias import result:', add(config.baseValue, 3)); diff --git a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/tsconfig.json b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/tsconfig.json index ce5fa43..9018d3b 100644 --- a/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/tsconfig.json +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/tsconfig.json @@ -9,4 +9,4 @@ } }, "include": ["**/*.ts"] -} \ No newline at end of file +} diff --git a/packages/jiti-tsc/README.md b/packages/jiti-tsc/README.md index 4da2d2e..4c07405 100644 --- a/packages/jiti-tsc/README.md +++ b/packages/jiti-tsc/README.md @@ -27,6 +27,7 @@ graph TD ``` **How it works:** + 1. **Parse**: Reads and parses `tsconfig.json` using TypeScript's config utilities 2. **Map**: Converts TypeScript compiler options to jiti-compatible options: - `paths` → `alias` (with absolute path resolution)