diff --git a/src/loaders-deprecated.ts b/src/loaders-deprecated.ts index f8a2afd..18ae34f 100644 --- a/src/loaders-deprecated.ts +++ b/src/loaders-deprecated.ts @@ -81,6 +81,7 @@ const _transformSource: transformSource = async function ( if ( url.endsWith('.json') + || url.endsWith('.jsx') || tsExtensionsPattern.test(url) ) { const transformed = await transform( diff --git a/src/loaders.ts b/src/loaders.ts index 386023f..b5a1893 100644 --- a/src/loaders.ts +++ b/src/loaders.ts @@ -154,7 +154,13 @@ export const resolve: resolve = async function ( /** * Typescript gives .ts, .cts, or .mts priority over actual .js, .cjs, or .mjs extensions */ - if (tsExtensionsPattern.test(context.parentURL!)) { + if ( + context.parentURL + && ( + tsExtensionsPattern.test(context.parentURL) + || fileMatcher?.(fileURLToPath(context.parentURL)) + ) + ) { const tsPath = resolveTsPath(specifier); if (tsPath) { @@ -242,6 +248,7 @@ export const load: LoadHook = async function ( if ( // Support named imports in JSON modules loaded.format === 'json' + || url.endsWith('.jsx') || tsExtensionsPattern.test(url) ) { const transformed = await transform( diff --git a/src/utils.ts b/src/utils.ts index 7b0e8be..fc7026e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -17,7 +17,13 @@ const tsconfig = ( path: path.resolve(process.env.ESBK_TSCONFIG_PATH), config: parseTsconfig(process.env.ESBK_TSCONFIG_PATH), } - : getTsconfig() + : ( + getTsconfig() + ?? { + path: process.cwd(), + config: {}, + } + ) ); export const fileMatcher = tsconfig && createFilesMatcher(tsconfig); @@ -25,7 +31,7 @@ export const tsconfigPathsMatcher = tsconfig && createPathsMatcher(tsconfig); export const fileProtocol = 'file://'; -export const tsExtensionsPattern = /\.([cm]?ts|[tj]sx)$/; +export const tsExtensionsPattern = /\.([cm]?ts|tsx)$/; const getFormatFromExtension = (fileUrl: string): ModuleFormat | undefined => { const extension = path.extname(fileUrl); @@ -43,6 +49,8 @@ const getFormatFromExtension = (fileUrl: string): ModuleFormat | undefined => { } }; +export const knownExtensions = /\.[tj]sx?$/; + export const getFormatFromFileUrl = (fileUrl: string) => { const format = getFormatFromExtension(fileUrl); @@ -50,8 +58,12 @@ export const getFormatFromFileUrl = (fileUrl: string) => { return format; } - // ts, tsx, jsx - if (tsExtensionsPattern.test(fileUrl)) { + /** + * Since this is only called when Node.js can't figure + * out the type, we only need to implement it for the + * extensions it can't handle (e.g. ts, tsx, jsx) + */ + if (knownExtensions.test(fileUrl)) { return getPackageType(fileUrl); } }; diff --git a/tests/fixtures/package-module/lib/cjs-ext-cjs/index.cjs b/tests/fixtures/package-module/lib/cjs-ext-cjs/index.cjs index 95d7ee0..1b9d890 100644 --- a/tests/fixtures/package-module/lib/cjs-ext-cjs/index.cjs +++ b/tests/fixtures/package-module/lib/cjs-ext-cjs/index.cjs @@ -31,9 +31,25 @@ test( 'sourcemaps', () => { const { stack } = new Error(); - const pathIndex = stack.indexOf(__filename + ':33:'); - const previousCharacter = stack[pathIndex - 1]; - return pathIndex > -1 && previousCharacter !== ':'; + const errorPosition = ':33:'; + const isWindows = process.platform === 'win32'; + let pathname = __filename; + if (isWindows) { + // Remove drive letter + pathname = pathname.slice(2); + } + + let pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + if ( + pathIndex === -1 + && isWindows + ) { + // Convert backslash to slash + pathname = pathname.replace(/\\/g, '/'); + pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + } + + return pathIndex > -1; }, ); diff --git a/tests/fixtures/package-module/lib/cjs-ext-js/index.js b/tests/fixtures/package-module/lib/cjs-ext-js/index.js index f70040d..ba668c6 100644 --- a/tests/fixtures/package-module/lib/cjs-ext-js/index.js +++ b/tests/fixtures/package-module/lib/cjs-ext-js/index.js @@ -1,3 +1,5 @@ +import { fileURLToPath } from 'node:url'; + async function test(description, testFunction) { try { const result = await testFunction(); @@ -31,16 +33,25 @@ test( 'sourcemaps', () => { const { stack } = new Error(); - let { pathname } = new URL(import.meta.url); - if (process.platform === 'win32') { - pathname = pathname.slice(1); + const errorPosition = ':35:'; + const isWindows = process.platform === 'win32'; + let pathname = fileURLToPath(import.meta.url); + if (isWindows) { + // Remove drive letter + pathname = pathname.slice(2); } - let pathIndex = stack.indexOf(`${pathname}:33:`); - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${pathname.toLowerCase()}:33:`); + + let pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + if ( + pathIndex === -1 + && isWindows + ) { + // Convert backslash to slash + pathname = pathname.replace(/\\/g, '/'); + pathIndex = stack.indexOf(`${pathname}${errorPosition}`); } - const previousCharacter = stack[pathIndex - 1]; - return pathIndex > -1 && previousCharacter !== ':'; + + return pathIndex > -1; }, ); diff --git a/tests/fixtures/package-module/lib/esm-ext-js/index.js b/tests/fixtures/package-module/lib/esm-ext-js/index.js index 985123d..d7844be 100644 --- a/tests/fixtures/package-module/lib/esm-ext-js/index.js +++ b/tests/fixtures/package-module/lib/esm-ext-js/index.js @@ -1,3 +1,5 @@ +import { fileURLToPath } from 'node:url'; + async function test(description, testFunction) { try { const result = await testFunction(); @@ -31,16 +33,25 @@ test( 'sourcemaps', () => { const { stack } = new Error(); - let { pathname } = new URL(import.meta.url); - if (process.platform === 'win32') { - pathname = pathname.slice(1); + const errorPosition = ':35:'; + const isWindows = process.platform === 'win32'; + let pathname = fileURLToPath(import.meta.url); + if (isWindows) { + // Remove drive letter + pathname = pathname.slice(2); } - let pathIndex = stack.indexOf(`${pathname}:33:`); - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${pathname.toLowerCase()}:33:`); + + let pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + if ( + pathIndex === -1 + && isWindows + ) { + // Convert backslash to slash + pathname = pathname.replace(/\\/g, '/'); + pathIndex = stack.indexOf(`${pathname}${errorPosition}`); } - const previousCharacter = stack[pathIndex - 1]; - return pathIndex > -1 && previousCharacter !== ':'; + + return pathIndex > -1; }, ); diff --git a/tests/fixtures/package-module/lib/esm-ext-mjs/index.mjs b/tests/fixtures/package-module/lib/esm-ext-mjs/index.mjs index bf0a6b5..b2c8a0f 100644 --- a/tests/fixtures/package-module/lib/esm-ext-mjs/index.mjs +++ b/tests/fixtures/package-module/lib/esm-ext-mjs/index.mjs @@ -1,3 +1,5 @@ +import { fileURLToPath } from 'node:url'; + async function test(description, testFunction) { try { const result = await testFunction(); @@ -31,16 +33,25 @@ test( 'sourcemaps', () => { const { stack } = new Error(); - let { pathname } = new URL(import.meta.url); - if (process.platform === 'win32') { - pathname = pathname.slice(1); + const errorPosition = ':35:'; + const isWindows = process.platform === 'win32'; + let pathname = fileURLToPath(import.meta.url); + if (isWindows) { + // Remove drive letter + pathname = pathname.slice(2); } - let pathIndex = stack.indexOf(pathname + ':33:'); - if (pathIndex === -1) { - pathIndex = stack.indexOf(pathname.toLowerCase() + ':33:'); + + let pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + if ( + pathIndex === -1 + && isWindows + ) { + // Convert backslash to slash + pathname = pathname.replace(/\\/g, '/'); + pathIndex = stack.indexOf(`${pathname}${errorPosition}`); } - const previousCharacter = stack[pathIndex - 1]; - return pathIndex > -1 && previousCharacter !== ':'; + + return pathIndex > -1; }, ); diff --git a/tests/fixtures/package-module/lib/ts-ext-cts/index.cts b/tests/fixtures/package-module/lib/ts-ext-cts/index.cts index 2dfd1fb..b75f9ab 100644 --- a/tests/fixtures/package-module/lib/ts-ext-cts/index.cts +++ b/tests/fixtures/package-module/lib/ts-ext-cts/index.cts @@ -1,3 +1,5 @@ +const { fileURLToPath } = require('node:url'); + async function test(description: string, testFunction: () => any | Promise) { try { const result = await testFunction(); @@ -31,9 +33,25 @@ test( 'sourcemaps', () => { const stack = (new Error()).stack!; - const pathIndex = stack.indexOf(`${__filename}:33:`); - const previousCharacter = stack[pathIndex - 1]; - return pathIndex > -1 && previousCharacter !== ':'; + const errorPosition = ':35:'; + const isWindows = process.platform === 'win32'; + let pathname = fileURLToPath(import.meta.url); + if (isWindows) { + // Remove drive letter + pathname = pathname.slice(2); + } + + let pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + if ( + pathIndex === -1 + && isWindows + ) { + // Convert backslash to slash + pathname = pathname.replace(/\\/g, '/'); + pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + } + + return pathIndex > -1; }, ); diff --git a/tests/fixtures/package-module/lib/ts-ext-jsx/index.jsx b/tests/fixtures/package-module/lib/ts-ext-jsx/index.jsx index 00a98c3..dd25728 100644 --- a/tests/fixtures/package-module/lib/ts-ext-jsx/index.jsx +++ b/tests/fixtures/package-module/lib/ts-ext-jsx/index.jsx @@ -33,19 +33,25 @@ test( 'sourcemaps', () => { const { stack } = new Error(); - let { pathname } = new URL(import.meta.url); - if (process.platform === 'win32') { - pathname = pathname.slice(1); + const errorPosition = ':35:'; + const isWindows = process.platform === 'win32'; + let pathname = fileURLToPath(import.meta.url); + if (isWindows) { + // Remove drive letter + pathname = pathname.slice(2); } - let pathIndex = stack.indexOf(`${pathname}:35:`); - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${pathname.toLowerCase()}:35:`); - } - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${fileURLToPath(import.meta.url).toLowerCase()}:35:`); + + let pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + if ( + pathIndex === -1 + && isWindows + ) { + // Convert backslash to slash + pathname = pathname.replace(/\\/g, '/'); + pathIndex = stack.indexOf(`${pathname}${errorPosition}`); } - const previousCharacter = stack[pathIndex - 1]; - return pathIndex > -1 && previousCharacter !== ':'; + + return pathIndex > -1; }, ); diff --git a/tests/fixtures/package-module/lib/ts-ext-mts/index.mts b/tests/fixtures/package-module/lib/ts-ext-mts/index.mts index 1ada674..3c1b8ec 100644 --- a/tests/fixtures/package-module/lib/ts-ext-mts/index.mts +++ b/tests/fixtures/package-module/lib/ts-ext-mts/index.mts @@ -33,19 +33,25 @@ test( 'sourcemaps', () => { const stack = (new Error()).stack!; - let { pathname } = new URL(import.meta.url); - if (process.platform === 'win32') { - pathname = pathname.slice(1); + const errorPosition = ':35:'; + const isWindows = process.platform === 'win32'; + let pathname = fileURLToPath(import.meta.url); + if (isWindows) { + // Remove drive letter + pathname = pathname.slice(2); } - let pathIndex = stack.indexOf(`${pathname}:35:`); - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${pathname.toLowerCase()}:35:`); - } - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${fileURLToPath(import.meta.url).toLowerCase()}:35:`); + + let pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + if ( + pathIndex === -1 + && isWindows + ) { + // Convert backslash to slash + pathname = pathname.replace(/\\/g, '/'); + pathIndex = stack.indexOf(`${pathname}${errorPosition}`); } - const previousCharacter = stack[pathIndex - 1]; - return pathIndex > -1 && previousCharacter !== ':'; + + return pathIndex > -1; }, ); diff --git a/tests/fixtures/package-module/lib/ts-ext-ts/index.ts b/tests/fixtures/package-module/lib/ts-ext-ts/index.ts index d2077e0..d052019 100644 --- a/tests/fixtures/package-module/lib/ts-ext-ts/index.ts +++ b/tests/fixtures/package-module/lib/ts-ext-ts/index.ts @@ -33,19 +33,25 @@ test( 'sourcemaps', () => { const stack = (new Error()).stack!; - let { pathname } = new URL(import.meta.url); - if (process.platform === 'win32') { - pathname = pathname.slice(1); + const errorPosition = ':35:'; + const isWindows = process.platform === 'win32'; + let pathname = fileURLToPath(import.meta.url); + if (isWindows) { + // Remove drive letter + pathname = pathname.slice(2); } - let pathIndex = stack.indexOf(`${pathname}:35:`); - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${pathname.toLowerCase()}:35:`); - } - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${fileURLToPath(import.meta.url).toLowerCase()}:35:`); + + let pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + if ( + pathIndex === -1 + && isWindows + ) { + // Convert backslash to slash + pathname = pathname.replace(/\\/g, '/'); + pathIndex = stack.indexOf(`${pathname}${errorPosition}`); } - const previousCharacter = stack[pathIndex - 1]; - return pathIndex > -1 && previousCharacter !== ':'; + + return pathIndex > -1; }, ); diff --git a/tests/fixtures/package-module/lib/ts-ext-ts/index.tsx.ts b/tests/fixtures/package-module/lib/ts-ext-ts/index.tsx.ts index 4f9f752..f0baf47 100644 --- a/tests/fixtures/package-module/lib/ts-ext-ts/index.tsx.ts +++ b/tests/fixtures/package-module/lib/ts-ext-ts/index.tsx.ts @@ -33,19 +33,25 @@ test( 'sourcemaps', () => { const stack = (new Error()).stack!; - let { pathname } = new URL(import.meta.url); - if (process.platform === 'win32') { - pathname = pathname.slice(1); + const errorPosition = ':35:'; + const isWindows = process.platform === 'win32'; + let pathname = fileURLToPath(import.meta.url); + if (isWindows) { + // Remove drive letter + pathname = pathname.slice(2); } - let pathIndex = stack.indexOf(`${pathname}:35:`); - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${pathname.toLowerCase()}:35:`); - } - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${fileURLToPath(import.meta.url).toLowerCase()}:35:`); + + let pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + if ( + pathIndex === -1 + && isWindows + ) { + // Convert backslash to slash + pathname = pathname.replace(/\\/g, '/'); + pathIndex = stack.indexOf(`${pathname}${errorPosition}`); } - const previousCharacter = stack[pathIndex - 1]; - return pathIndex > -1 && previousCharacter !== ':'; + + return pathIndex > -1; }, ); diff --git a/tests/fixtures/package-module/lib/ts-ext-tsx/index.tsx b/tests/fixtures/package-module/lib/ts-ext-tsx/index.tsx index 07b9e64..4121325 100644 --- a/tests/fixtures/package-module/lib/ts-ext-tsx/index.tsx +++ b/tests/fixtures/package-module/lib/ts-ext-tsx/index.tsx @@ -33,19 +33,25 @@ test( 'sourcemaps', () => { const stack = (new Error()).stack!; - let { pathname } = new URL(import.meta.url); - if (process.platform === 'win32') { - pathname = pathname.slice(1); + const errorPosition = ':35:'; + const isWindows = process.platform === 'win32'; + let pathname = fileURLToPath(import.meta.url); + if (isWindows) { + // Remove drive letter + pathname = pathname.slice(2); } - let pathIndex = stack.indexOf(`${pathname}:35:`); - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${pathname.toLowerCase()}:35:`); - } - if (pathIndex === -1) { - pathIndex = stack.indexOf(`${fileURLToPath(import.meta.url).toLowerCase()}:35:`); + + let pathIndex = stack.indexOf(`${pathname}${errorPosition}`); + if ( + pathIndex === -1 + && isWindows + ) { + // Convert backslash to slash + pathname = pathname.replace(/\\/g, '/'); + pathIndex = stack.indexOf(`${pathname}${errorPosition}`); } - const previousCharacter = stack[pathIndex - 1]; - return pathIndex > -1 && previousCharacter !== ':'; + + return pathIndex > -1; }, ); diff --git a/tests/specs/typescript/cts.ts b/tests/specs/typescript/cts.ts index 527e08d..0fc825a 100644 --- a/tests/specs/typescript/cts.ts +++ b/tests/specs/typescript/cts.ts @@ -1,6 +1,10 @@ +import fs from 'fs/promises'; +import path from 'path'; import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; import type { NodeApis } from '../../utils/node-with-loader.js'; import { assertNotFound } from '../../utils/assertions.js'; +import { importAndLog, tsconfigJson } from '../../utils/fixtures.js'; export default testSuite(async ({ describe }, node: NodeApis) => { describe('.cts extension', ({ describe }) => { @@ -24,17 +28,165 @@ export default testSuite(async ({ describe }, node: NodeApis) => { }); }); - describe('full path via .cjs', ({ test }) => { - const importPath = './lib/ts-ext-cts/index.cjs'; + describe('full path via .cjs', async ({ describe }) => { + const ctsFile = await fs.readFile('./tests/fixtures/package-module/lib/ts-ext-cts/index.cts', 'utf8'); - test('Load - should not work', async () => { - const nodeProcess = await node.load(importPath); - assertNotFound(nodeProcess.stderr, importPath); + describe('From JavaScript file', ({ describe }) => { + describe('with allowJs', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mjs': importAndLog('./file.cjs'), + 'file.cts': ctsFile, + 'tsconfig.json': tsconfigJson({ + compilerOptions: { + allowJs: true, + }, + }), + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.cjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.cjs')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.mjs', { + cwd: fixture.path, + }); + expect(nodeProcess.stderr).toMatch('SyntaxError: Unexpected token \':\''); + }); + }); + + describe('without allowJs', ({ describe }) => { + describe('empty tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mjs': importAndLog('./file.cjs'), + 'file.cts': ctsFile, + 'tsconfig.json': '{}', + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.cjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.cjs')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.mjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.cjs')); + }); + }); + + describe('no tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mjs': importAndLog('./file.cjs'), + 'file.cts': ctsFile, + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.cjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.cjs')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.mjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.cjs')); + }); + }); + }); }); - test('Import', async () => { - const nodeProcess = await node.import(importPath, { typescript: true }); - expect(nodeProcess.stderr).toMatch('SyntaxError: Unexpected token \':\''); + describe('From TypeScript file', ({ describe }) => { + describe('with allowJs', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mts': importAndLog('./file.cjs'), + 'file.cts': ctsFile, + 'tsconfig.json': tsconfigJson({ + compilerOptions: { + allowJs: true, + }, + }), + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.cjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.cjs')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.mts', { + cwd: fixture.path, + }); + expect(nodeProcess.stderr).toMatch('SyntaxError: Unexpected token \':\''); + }); + }); + + describe('without allowJs', ({ describe }) => { + describe('empty tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mts': importAndLog('./file.cjs'), + 'file.cts': ctsFile, + 'tsconfig.json': '{}', + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.cjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.cjs')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.mts', { + cwd: fixture.path, + }); + expect(nodeProcess.stderr).toMatch('SyntaxError: Unexpected token \':\''); + }); + }); + + describe('no tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mts': importAndLog('./file.cjs'), + 'file.cts': ctsFile, + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.cjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.cjs')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.mts', { + cwd: fixture.path, + }); + expect(nodeProcess.stderr).toMatch('SyntaxError: Unexpected token \':\''); + }); + }); + }); }); }); diff --git a/tests/specs/typescript/mts.ts b/tests/specs/typescript/mts.ts index 95b91f4..4863bcc 100644 --- a/tests/specs/typescript/mts.ts +++ b/tests/specs/typescript/mts.ts @@ -1,8 +1,12 @@ +import fs from 'fs/promises'; +import path from 'path'; import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; import semver from 'semver'; import type { NodeApis } from '../../utils/node-with-loader.js'; import nodeSupports from '../../utils/node-supports.js'; import { assertNotFound } from '../../utils/assertions.js'; +import { importAndLog, tsconfigJson } from '../../utils/fixtures.js'; export default testSuite(async ({ describe }, node: NodeApis) => { describe('.mts extension', ({ describe }) => { @@ -35,18 +39,169 @@ export default testSuite(async ({ describe }, node: NodeApis) => { }); }); - describe('full path via .mjs', ({ test }) => { - const importPath = './lib/ts-ext-mts/index.mjs'; + describe('full path via .mjs', async ({ describe }) => { + const mtsFile = await fs.readFile('./tests/fixtures/package-module/lib/ts-ext-mts/index.mts', 'utf8'); - test('Load - should not work', async () => { - const nodeProcess = await node.load(importPath); - assertNotFound(nodeProcess.stderr, importPath); + describe('From JavaScript file', ({ describe }) => { + describe('with allowJs', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mjs': importAndLog('./file.mjs'), + 'file.mts': mtsFile, + 'tsconfig.json': tsconfigJson({ + compilerOptions: { + allowJs: true, + }, + }), + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.mjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.mjs')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.mjs', { + cwd: fixture.path, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + }); + }); + + describe('without allowJs', ({ describe }) => { + describe('empty tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mjs': importAndLog('./file.mjs'), + 'file.mts': mtsFile, + 'tsconfig.json': '{}', + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.mjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.mjs')); + }); + + test('Import - should not work', async () => { + const nodeProcess = await node.load('import.mjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.mjs')); + }); + }); + + describe('no tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mjs': importAndLog('./file.mjs'), + 'file.mts': mtsFile, + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.mjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.mjs')); + }); + + test('Import - should not work', async () => { + const nodeProcess = await node.load('import.mjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.mjs')); + }); + }); + }); }); - test('Import', async () => { - const nodeProcess = await node.import(importPath, { typescript: true }); - assertResults(nodeProcess.stdout); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); + describe('From TypeScript file', ({ describe }) => { + describe('with allowJs', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mts': importAndLog('./file.mjs'), + 'file.mts': mtsFile, + 'tsconfig.json': tsconfigJson({ + compilerOptions: { + allowJs: true, + }, + }), + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.mjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.mjs')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.mts', { + cwd: fixture.path, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + }); + }); + + describe('without allowJs', ({ describe }) => { + describe('empty tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mts': importAndLog('./file.mjs'), + 'file.mts': mtsFile, + 'tsconfig.json': '{}', + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.mjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.mjs')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.mts', { + cwd: fixture.path, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + }); + }); + + describe('no tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'import.mts': importAndLog('./file.mjs'), + 'file.mts': mtsFile, + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.mjs', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.mjs')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.mts', { + cwd: fixture.path, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + }); + }); + }); }); }); diff --git a/tests/specs/typescript/ts.ts b/tests/specs/typescript/ts.ts index 7d9ba59..49f5734 100644 --- a/tests/specs/typescript/ts.ts +++ b/tests/specs/typescript/ts.ts @@ -1,8 +1,12 @@ +import fs from 'fs/promises'; +import path from 'path'; import { testSuite, expect } from 'manten'; +import { createFixture } from 'fs-fixture'; import semver from 'semver'; import type { NodeApis } from '../../utils/node-with-loader.js'; import nodeSupports from '../../utils/node-supports.js'; import { assertNotFound } from '../../utils/assertions.js'; +import { importAndLog, packageJson, tsconfigJson } from '../../utils/fixtures.js'; export default testSuite(async ({ describe }, node: NodeApis) => { describe('.ts extension', ({ describe }) => { @@ -45,18 +49,213 @@ export default testSuite(async ({ describe }, node: NodeApis) => { }); }); - describe('full path via .js', ({ test }) => { - const importPath = './lib/ts-ext-ts/index.js'; + describe('full path via .js', async ({ describe }) => { + const tsFile = await fs.readFile('./tests/fixtures/package-module/lib/ts-ext-ts/index.ts', 'utf8'); - test('Load - should not work', async () => { - const nodeProcess = await node.load(importPath); - assertNotFound(nodeProcess.stderr, importPath); + describe('From JavaScript file', ({ describe }) => { + describe('with allowJs', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'package.json': packageJson({ type: 'module' }), + 'import.js': importAndLog('./file.js'), + 'file.ts': tsFile, + 'tsconfig.json': tsconfigJson({ + compilerOptions: { + allowJs: true, + }, + }), + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.js', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.js')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.js', { + cwd: fixture.path, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + }); + }); + + describe('without allowJs', ({ describe }) => { + describe('empty tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'package.json': packageJson({ type: 'module' }), + 'import.js': importAndLog('./file.js'), + 'file.ts': tsFile, + 'tsconfig.json': tsconfigJson({}), + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.js', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.js')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.js', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.js')); + }); + }); + + describe('no tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'package.json': packageJson({ type: 'module' }), + 'import.js': importAndLog('./file.js'), + 'file.ts': tsFile, + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.js', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.js')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.js', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.js')); + }); + }); + }); }); - test('Import', async () => { - const nodeProcess = await node.import(importPath, { typescript: true }); - assertResults(nodeProcess.stdout); - expect(nodeProcess.stdout).toMatch('{"default":1234}'); + describe('From TypeScript file', ({ describe }) => { + describe('with allowJs', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'package.json': packageJson({ type: 'module' }), + 'import.ts': importAndLog('./file.js'), + 'file.ts': tsFile, + 'tsconfig.json': tsconfigJson({ + compilerOptions: { + allowJs: true, + }, + }), + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.js', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.js')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.ts', { + cwd: fixture.path, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + }); + }); + + describe('without allowJs', ({ describe }) => { + describe('excluded by tsconfig.json', async ({ test, onFinish }) => { + /** + * file.ts is technically excluded from tsconfig.json, but it should work + * becaue it's clearly from a TypeScript file + * + * In the future, we'll probably want to lookup a matching tsconfig for each file + * and not just pick one in cwd + */ + const fixture = await createFixture({ + 'package.json': packageJson({ type: 'module' }), + 'import.ts': importAndLog('./file.js'), + 'file.ts': tsFile, + 'tsconfig.json': tsconfigJson({ + compilerOptions: { + // TODO: add some configs that shouldnt get applied + }, + exclude: ['*.ts'], + }), + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.js', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.js')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.ts', { + cwd: fixture.path, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + }); + }); + + describe('empty tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'package.json': packageJson({ type: 'module' }), + 'import.ts': importAndLog('./file.js'), + 'file.ts': tsFile, + 'tsconfig.json': tsconfigJson({}), + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.js', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.js')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.ts', { + cwd: fixture.path, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + }); + }); + + describe('no tsconfig.json', async ({ test, onFinish }) => { + const fixture = await createFixture({ + 'package.json': packageJson({ type: 'module' }), + 'import.ts': importAndLog('./file.js'), + 'file.ts': tsFile, + }); + + onFinish(async () => await fixture.rm()); + + test('Load - should not work', async () => { + const nodeProcess = await node.load('./file.js', { + cwd: fixture.path, + }); + assertNotFound(nodeProcess.stderr, path.join(fixture.path, 'file.js')); + }); + + test('Import', async () => { + const nodeProcess = await node.load('import.ts', { + cwd: fixture.path, + }); + assertResults(nodeProcess.stdout); + expect(nodeProcess.stdout).toMatch('{"default":1234}'); + }); + }); + }); }); }); diff --git a/tests/utils/fixtures.ts b/tests/utils/fixtures.ts index 789a83a..5c81523 100644 --- a/tests/utils/fixtures.ts +++ b/tests/utils/fixtures.ts @@ -7,3 +7,7 @@ export const packageJson = ( export const tsconfigJson = ( tsconfigJsonObject: TsConfigJson, ) => JSON.stringify(tsconfigJsonObject); + +export const importAndLog = ( + specifier: string, +) => `import("${specifier}").then(m => console.log(JSON.stringify(m)))`;