diff --git a/e2e/ts-jiti-e2e/mocks/README.md b/e2e/ts-jiti-e2e/mocks/README.md new file mode 100644 index 0000000..c800bf2 --- /dev/null +++ b/e2e/ts-jiti-e2e/mocks/README.md @@ -0,0 +1,37 @@ +# 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. 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..8d2c4e4 --- /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 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..178885a --- /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; 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..77c2f6d --- /dev/null +++ b/e2e/ts-jiti-e2e/mocks/fixtures/path-aliases-basic/src/index.ts @@ -0,0 +1,8 @@ +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 }; +} 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..341e651 --- /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; +} 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..1ce63ce --- /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 { 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)); 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..9018d3b --- /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"] +} 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 new file mode 100644 index 0000000..c14f7ce --- /dev/null +++ b/e2e/ts-jiti-e2e/tests/cli-jiti-tsc.e2e.test.ts @@ -0,0 +1,422 @@ +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 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: { '@/*': ['./*'] } }, +}; + +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'; + const describeSlug = toSlug(describeName); + + const getTestDir = (itName: string) => + path.join( + envRoot, + TEST_OUTPUT_DIR, + testFileName, + describeSlug, + toSlug(itName), + ); + + it('should execute ts file with jiti-tsc', async () => { + const baseFolder = getTestDir('exec-jiti-tsc'); + const cleanup = await fsFromJson({ + [path.join(baseFolder, 'bin.ts')]: binJitiTscContent, + }); + + const { code, stdout, stderr } = await executeProcess({ + command: 'npx', + args: [ + '@push-based/jiti-tsc', + path.relative(envRoot, path.join(baseFolder, 'bin.ts')), + '--test-arg=123', + ], + cwd: envRoot, + silent: true, + }); + + expect(code).toBe(0); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + 'Executed over jiti-tsc', + ); + expect(removeColorCodes(stdout) + removeColorCodes(stderr)).toContain( + "args: [ '--test-arg=123' ]", + ); + + await cleanup(); + }); + + it('should load .ts', async () => { + const d = getTestDir('load-ts'); + const cleanup = await fsFromJson({ + [path.join(d, 'a.ts')]: loadTsAContent, + [path.join(d, 'b.ts')]: loadTsBContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: 'npx', + args: [ + '@push-based/jiti-tsc', + 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( + "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')]: execTsAContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: 'npx', + args: [ + '@push-based/jiti-tsc', + 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( + "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')]: execTsLoadTsAContent, + [path.join(d, 'b.ts')]: execTsLoadTsBContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: 'npx', + args: [ + '@push-based/jiti-tsc', + 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( + "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')]: tsconfigPathContent, + [path.join(d, 'a.ts')]: tsconfigPathAContent, + [path.join(d, 'b.ts')]: tsconfigPathBContent, + }); + const { code, stdout, stderr } = await executeProcess({ + command: 'npx', + args: [ + '@push-based/jiti-tsc', + 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( + "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')]: 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: 'npx', + args: [ + '@push-based/jiti-tsc', + 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( + "args: [ '--complex-arg=multi-file' ]", + ); + 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( + "{ app: 'config' }", + ); + 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({ + [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: { + NODE_OPTIONS: '--import @push-based/jiti-tsc', + }, + }); + + expect(code).toBe(0); + 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/e2e/ts-jiti-e2e/tests/jiti.e2e.test.ts b/e2e/ts-jiti-e2e/tests/jiti.e2e.test.ts deleted file mode 100644 index d3f7a08..0000000 --- a/e2e/ts-jiti-e2e/tests/jiti.e2e.test.ts +++ /dev/null @@ -1,221 +0,0 @@ -import { nxTargetProject } from '@push-based/test-nx-utils'; -import { - E2E_ENVIRONMENTS_DIR, - TEST_OUTPUT_DIR, - executeProcess, - fsFromJson, -} 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); -const testFileName = path.basename(__filename); - -const toSlug = (str: string): string => - str - .toLowerCase() - .replace(/\s+/g, '-') - .replace(/[^a-z\d-]/g, ''); - -describe('CLI jiti', () => { - const envRoot = path.join(E2E_ENVIRONMENTS_DIR, nxTargetProject()); - const describeName = 'CLI jiti'; - const describeSlug = toSlug(describeName); - - const getTestDir = (itName: string) => - path.join( - envRoot, - TEST_OUTPUT_DIR, - testFileName, - describeSlug, - toSlug(itName), - ); - - 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)); -`, - }); - - const { code, stdout } = await executeProcess({ - command: 'npx', - args: [ - '@push-based/jiti-tsc', - path.relative(envRoot, path.join(baseFolder, 'bin.ts')), - '--test-arg=123', - ], - cwd: envRoot, - silent: true, - }); - - expect(code).toBe(0); - expect(stripAnsi(stdout)).toContain('Executed over jiti-tsc'); - expect(stripAnsi(stdout)).toContain("args: [ '--test-arg=123' ]"); - - await cleanup(); - }); - - 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';`, - }); - const { code, stdout } = await executeProcess({ - command: 'npx', - args: [ - '@push-based/jiti-tsc', - path.relative(envRoot, path.join(d, 'a.js')), - '--load-arg=test', - ], - cwd: envRoot, - silent: true, - }); - - expect(code).toBe(0); - expect(stripAnsi(stdout)).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));`, - }); - const { code, stdout } = await executeProcess({ - command: 'npx', - args: [ - '@push-based/jiti-tsc', - path.relative(envRoot, path.join(d, 'a.ts')), - '--exec-arg=value', - ], - cwd: envRoot, - silent: true, - }); - - expect(code).toBe(0); - expect(stripAnsi(stdout)).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';`, - }); - const { code, stdout } = await executeProcess({ - command: 'npx', - args: [ - '@push-based/jiti-tsc', - path.relative(envRoot, path.join(d, 'a.ts')), - '--load-ts-arg=hello', - ], - cwd: envRoot, - silent: true, - }); - - expect(code).toBe(0); - expect(stripAnsi(stdout)).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';`, - }); - const { code, stdout } = await executeProcess({ - command: 'npx', - args: [ - '@push-based/jiti-tsc', - 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(stripAnsi(stdout)).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';`, - }); - const { code, stdout } = await executeProcess({ - command: 'npx', - args: [ - '@push-based/jiti-tsc', - 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(stripAnsi(stdout)).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)); -`, - }); - - const { code, stdout } = await executeProcess({ - command: process.execPath, - args: [ - path.relative(envRoot, path.join(baseFolder, 'bin.ts')), - '--myArg=42', - ], - cwd: envRoot, - silent: true, - env: { - NODE_OPTIONS: '--import @push-based/jiti-tsc', - }, - }); - - expect(code).toBe(0); - expect(stripAnsi(stdout)).toContain('Executed over --import jiti-tsc'); - expect(stripAnsi(stdout)).toContain("args: [ '--myArg=42' ]"); - - await cleanup(); - }); -}); 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..0fc606d --- /dev/null +++ b/e2e/ts-jiti-e2e/tests/register-jiti-tsc.e2e.test.ts @@ -0,0 +1,443 @@ +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: { '@/*': ['./*'] } }, +}; + +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)'; + 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 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( + "{ app: 'config-loader' }", + ); + 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({ + [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/README.md b/packages/jiti-tsc/README.md index 1557130..4c07405 100644 --- a/packages/jiti-tsc/README.md +++ b/packages/jiti-tsc/README.md @@ -4,14 +4,45 @@ ## 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/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/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/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..6632a7d 100644 --- a/packages/jiti-tsc/src/cli.ts +++ b/packages/jiti-tsc/src/cli.ts @@ -1,22 +1,21 @@ #!/usr/bin/env node import { spawn } from 'node:child_process'; -const [_nodePath, _scriptPath, file, ...rest] = process.argv; +const [_, __, cmd, ...rest] = process.argv; -if (!file) { - console.error('Usage: jiti-tsc '); +if (!cmd) { + console.error('Usage: jiti-tsc [...args]'); // eslint-disable-next-line n/no-process-exit 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(' '), - }, +// 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-disable-next-line n/no-process-exit + process.exit(code ?? 1); }); -// 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..0278d5d 100644 --- a/packages/jiti-tsc/src/index.ts +++ b/packages/jiti-tsc/src/index.ts @@ -1,8 +1,12 @@ -import { registerJitiTsconfig } from './lib/jiti/register.js'; - -try { - await registerJitiTsconfig(); -} catch (error) { - console.error('[jiti-tsc] Failed to register:', error); - throw error; -} +// 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/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.int.test.ts b/packages/jiti-tsc/src/lib/jiti/import-module.int.test.ts deleted file mode 100644 index 3d5f4db..0000000 --- a/packages/jiti-tsc/src/lib/jiti/import-module.int.test.ts +++ /dev/null @@ -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 20c228e..2160599 100644 --- a/packages/jiti-tsc/src/lib/jiti/import-module.ts +++ b/packages/jiti-tsc/src/lib/jiti/import-module.ts @@ -1,18 +1,31 @@ import { createJiti as createJitiSource } from 'jiti'; -import { stat } from 'node:fs/promises'; import path from 'node:path'; +import * as process from 'node:process'; import type { CompilerOptions } from 'typescript'; import { fileExists } from '../utils/file-system.js'; -import { settlePromise } from '../utils/promises.js'; +import { JITI_TSCONFIG_PATH_ENV_VAR } from './constants.js'; import { loadTargetConfig } from './load-ts-config.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 async function importModule( options: ImportModuleOptions, ): Promise { @@ -25,20 +38,18 @@ 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, tsconfigPath: tsconfig, }); - return (await jitiInstance.import(absoluteFilePath, { default: true })) as T; + return (await jitiInstance.import(absoluteFilePath, { + default: true, + })) as T; } /** @@ -91,9 +102,13 @@ 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 + Pick< + JitiOptions, + 'alias' | 'interopDefault' | 'sourceMaps' | 'jsx' | 'nativeModules' + > >; /** * Parse TypeScript compiler options to mappable jiti options @@ -137,7 +152,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, @@ -145,21 +160,31 @@ export async function createTsJiti( createJiti: (typeof import('jiti'))['createJiti'] = createJitiSource, ) { const { tsconfigPath, ...jitiOptions } = options; - - const fallbackTsconfigPath = path.resolve(process.cwd(), 'tsconfig.json'); - + const parsedTsconfigPath = + process.env[JITI_TSCONFIG_PATH_ENV_VAR] ?? tsconfigPath; const validPath: null | string = - tsconfigPath == null - ? (await fileExists(fallbackTsconfigPath)) - ? fallbackTsconfigPath - : null - : path.resolve(process.cwd(), tsconfigPath); - + parsedTsconfigPath == null + ? null + : path.resolve(process.cwd(), parsedTsconfigPath); 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/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 b732885..c92130c 100644 --- a/packages/jiti-tsc/src/lib/jiti/register.ts +++ b/packages/jiti-tsc/src/lib/jiti/register.ts @@ -1,31 +1,23 @@ -// 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 { setJitiEnvVars } from './env-vars.js'; +import { parseTsConfigToJitiConfig } from './import-module.js'; +import { loadTargetConfig } from './load-ts-config.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'); + const tsconfigPath = process.env[JITI_TSCONFIG_PATH_ENV_VAR]; - try { - const jitiOptions = await jitiOptionsFromTsConfig(tsconfigPath); - const envVars = jitiOptionsToEnv(jitiOptions); - - Object.entries(envVars).forEach( - // eslint-disable-next-line functional/immutable-data - ([k, v]) => v != null && (process.env[k] = v), - ); - } catch { - console.warn( - `[jiti-tsc] Failed to load tsconfig from ${tsconfigPath}, continuing without tsconfig`, - ); + 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 }; - } -} 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 +}