diff --git a/packages/typescript/src/index.ts b/packages/typescript/src/index.ts index f3b263435..dd027c46c 100644 --- a/packages/typescript/src/index.ts +++ b/packages/typescript/src/index.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { createFilter } from '@rollup/pluginutils'; import { Plugin, RollupOptions, SourceDescription } from 'rollup'; -import type { Watch } from 'typescript'; +import type { ResolvedModuleFull, Watch } from 'typescript'; import { RollupTypescriptOptions } from '../types'; @@ -17,6 +17,11 @@ import { preflight } from './preflight'; import createWatchProgram, { WatchProgramHelper } from './watchProgram'; import TSCache from './tscache'; +/** + * The glob pattern used to test if a given file extension corresponds to a TypeScript file. + */ +const defaultTsExtensionsGlob = '?(m|c)ts+(|x)'; + export default function typescript(options: RollupTypescriptOptions = {}): Plugin { const { cacheDir, @@ -36,9 +41,13 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi const watchProgramHelper = new WatchProgramHelper(); const parsedOptions = parseTypescriptConfig(ts, tsconfig, compilerOptions, noForceEmit); - const filter = createFilter(include || ['*.ts+(|x)', '**/*.ts+(|x)'], exclude, { - resolve: filterRoot ?? parsedOptions.options.rootDir - }); + const filter = createFilter( + include || [`*.${defaultTsExtensionsGlob}`, `**/*.${defaultTsExtensionsGlob}`], + exclude, + { + resolve: filterRoot ?? parsedOptions.options.rootDir + } + ); parsedOptions.fileNames = parsedOptions.fileNames.filter(filter); const formatHost = createFormattingHost(ts, parsedOptions.options); @@ -110,7 +119,7 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi const resolved = resolveModule(importee, containingFile); if (resolved) { - if (resolved.extension === '.d.ts') return null; + if (isTypeDeclarationFile(resolved)) return null; return path.normalize(resolved.resolvedFileName); } @@ -173,3 +182,16 @@ export default function typescript(options: RollupTypescriptOptions = {}): Plugi } }; } + +/** + * Check if the given resolved TypeScript module is a type declaration. + */ +function isTypeDeclarationFile(resolved: ResolvedModuleFull) { + const lastDotDDot = resolved.extension.lastIndexOf('.d.'); + if (lastDotDDot < 0) return false; + + const lastDot = resolved.extension.lastIndexOf('.'); + + // Ensure the dot is the second one in `.d.` + return lastDot === lastDotDDot + 2; +} diff --git a/packages/typescript/test/fixtures/cts/main.mts b/packages/typescript/test/fixtures/cts/main.mts new file mode 100644 index 000000000..ccf559ebb --- /dev/null +++ b/packages/typescript/test/fixtures/cts/main.mts @@ -0,0 +1,3 @@ +const answer = 42; +// eslint-disable-next-line no-console +console.log(`the answer is ${answer}`); diff --git a/packages/typescript/test/fixtures/cts/tsconfig.json b/packages/typescript/test/fixtures/cts/tsconfig.json new file mode 100644 index 000000000..3fc681555 --- /dev/null +++ b/packages/typescript/test/fixtures/cts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16" + } +} diff --git a/packages/typescript/test/fixtures/dcts/main.cts b/packages/typescript/test/fixtures/dcts/main.cts new file mode 100644 index 000000000..e82d43df2 --- /dev/null +++ b/packages/typescript/test/fixtures/dcts/main.cts @@ -0,0 +1,5 @@ +/* eslint-disable */ +// @ts-ignore +import { foo } from 'an-import'; + +foo(); diff --git a/packages/typescript/test/fixtures/dcts/tsconfig.json b/packages/typescript/test/fixtures/dcts/tsconfig.json new file mode 100644 index 000000000..3fc681555 --- /dev/null +++ b/packages/typescript/test/fixtures/dcts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16" + } +} diff --git a/packages/typescript/test/fixtures/dmts/main.mts b/packages/typescript/test/fixtures/dmts/main.mts new file mode 100644 index 000000000..e82d43df2 --- /dev/null +++ b/packages/typescript/test/fixtures/dmts/main.mts @@ -0,0 +1,5 @@ +/* eslint-disable */ +// @ts-ignore +import { foo } from 'an-import'; + +foo(); diff --git a/packages/typescript/test/fixtures/dmts/tsconfig.json b/packages/typescript/test/fixtures/dmts/tsconfig.json new file mode 100644 index 000000000..3fc681555 --- /dev/null +++ b/packages/typescript/test/fixtures/dmts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16" + } +} diff --git a/packages/typescript/test/fixtures/mts/main.mts b/packages/typescript/test/fixtures/mts/main.mts new file mode 100644 index 000000000..ccf559ebb --- /dev/null +++ b/packages/typescript/test/fixtures/mts/main.mts @@ -0,0 +1,3 @@ +const answer = 42; +// eslint-disable-next-line no-console +console.log(`the answer is ${answer}`); diff --git a/packages/typescript/test/fixtures/mts/tsconfig.json b/packages/typescript/test/fixtures/mts/tsconfig.json new file mode 100644 index 000000000..3fc681555 --- /dev/null +++ b/packages/typescript/test/fixtures/mts/tsconfig.json @@ -0,0 +1,6 @@ +{ + "compilerOptions": { + "module": "Node16", + "moduleResolution": "Node16" + } +} diff --git a/packages/typescript/test/test.js b/packages/typescript/test/test.js index d72fea497..506354b87 100644 --- a/packages/typescript/test/test.js +++ b/packages/typescript/test/test.js @@ -16,7 +16,7 @@ test.beforeEach(() => process.chdir(__dirname)); const outputOptions = { format: 'esm' }; -test.serial('runs code through typescript', async (t) => { +test.serial('runs ts code through typescript', async (t) => { const bundle = await rollup({ input: 'fixtures/basic/main.ts', plugins: [typescript({ tsconfig: 'fixtures/basic/tsconfig.json', target: 'es5' })], @@ -28,6 +28,30 @@ test.serial('runs code through typescript', async (t) => { t.false(code.includes('const'), code); }); +test.serial('runs mts code through typescript', async (t) => { + const bundle = await rollup({ + input: 'fixtures/mts/main.ts', + plugins: [typescript({ tsconfig: 'fixtures/mts/tsconfig.json', target: 'es5' })], + onwarn + }); + const code = await getCode(bundle, outputOptions); + + t.false(code.includes('number'), code); + t.false(code.includes('const'), code); +}); + +test.serial('runs cts code through typescript', async (t) => { + const bundle = await rollup({ + input: 'fixtures/cts/main.ts', + plugins: [typescript({ tsconfig: 'fixtures/cts/tsconfig.json', target: 'es5' })], + onwarn + }); + const code = await getCode(bundle, outputOptions); + + t.false(code.includes('number'), code); + t.false(code.includes('const'), code); +}); + test.serial('runs code through typescript with compilerOptions', async (t) => { const bundle = await rollup({ input: 'fixtures/basic/main.ts', @@ -375,6 +399,28 @@ test.serial('should not resolve .d.ts files', async (t) => { t.deepEqual(imports, ['an-import']); }); +test.serial('should not resolve .d.cts files', async (t) => { + const bundle = await rollup({ + input: 'fixtures/dts/main.cts', + plugins: [typescript({ tsconfig: 'fixtures/dcts/tsconfig.json' })], + onwarn, + external: ['an-import'] + }); + const imports = bundle.cache.modules[0].dependencies; + t.deepEqual(imports, ['an-import']); +}); + +test.serial('should not resolve .d.mts files', async (t) => { + const bundle = await rollup({ + input: 'fixtures/dts/main.mts', + plugins: [typescript({ tsconfig: 'fixtures/dmts/tsconfig.json' })], + onwarn, + external: ['an-import'] + }); + const imports = bundle.cache.modules[0].dependencies; + t.deepEqual(imports, ['an-import']); +}); + test.serial('should transpile JSX if enabled', async (t) => { process.chdir('fixtures/jsx');