Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { terserPlugin } from './plugins/terser'
import { runTypeScriptCompiler } from './tsc'
import { runDtsRollup } from './api-extractor'
import { cjsInterop } from './plugins/cjs-interop'
import { rewriteImportExtensions } from './plugins/rewrite-import-extensions'
import type { Format, KILL_SIGNAL, NormalizedOptions, Options } from './options'

export type { Format, Options, NormalizedOptions }
Expand Down Expand Up @@ -313,6 +314,7 @@ export async function build(_options: Options) {
...options.format.map(async (format, index) => {
const pluginContainer = new PluginContainer([
shebang(),
rewriteImportExtensions(),
...(options.plugins || []),
treeShakingPlugin({
treeshake: options.treeshake,
Expand Down
32 changes: 32 additions & 0 deletions src/plugins/rewrite-dts-import-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import type { Plugin } from 'rollup'

const RELATIVE_TS_IMPORT_PATTERN =
/(?<=(?:from\s+|import\s*\(|require\s*\()['"])(\.\.?\/[^'"]*)(\.(?:ts|tsx|mts|cts))(\?[^'"]*)?(?=['"])/g

const getOutputExtension = (tsExt: string, outputPath: string): string => {
if (tsExt === '.mts') return '.mjs'
if (tsExt === '.cts') return '.cjs'
if (outputPath.endsWith('.mjs') || outputPath.endsWith('.d.mts'))
return '.mjs'
if (outputPath.endsWith('.cjs') || outputPath.endsWith('.d.cts'))
return '.cjs'
return '.js'
}

export const rewriteDtsImportExtensionsPlugin = (): Plugin => ({
name: 'tsup:rewrite-dts-import-extensions',
renderChunk(code, chunk) {
let touched = false
const rewritten = code.replace(
RELATIVE_TS_IMPORT_PATTERN,
(_, pathWithoutExt, tsExt, query = '') => {
touched = true
return (
pathWithoutExt + getOutputExtension(tsExt, chunk.fileName) + query
)
},
)
if (!touched) return null
return { code: rewritten, map: null }
},
})
36 changes: 36 additions & 0 deletions src/plugins/rewrite-import-extensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Plugin } from '../plugin'

const getOutputExtension = (tsExt: string, outputPath: string): string => {
if (tsExt === '.mts') return '.mjs'
if (tsExt === '.cts') return '.cjs'
if (outputPath.endsWith('.mjs')) return '.mjs'
if (outputPath.endsWith('.cjs')) return '.cjs'
return '.js'
}

const RELATIVE_IMPORT_PATTERN =
/(?<=(?:from\s+|import\s*\(|require\s*\()['"])(\.\.?\/[^'"]*)(\.(?:ts|tsx|mts|cts))(\?[^'"]*)?(?=['"])/g

export const rewriteImportExtensions = (): Plugin => {
return {
name: 'rewrite-import-extensions',

renderChunk(code, info) {
if (!/\.(js|mjs|cjs)$/.test(info.path)) return

let touched = false

const rewritten = code.replace(
RELATIVE_IMPORT_PATTERN,
(_, pathWithoutExt, tsExt, query = '') => {
touched = true
return pathWithoutExt + getOutputExtension(tsExt, info.path) + query
},
)

if (!touched) return

return { code: rewritten }
},
}
}
2 changes: 2 additions & 0 deletions src/rollup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { reportSize } from './lib/report-size'
import type { NormalizedOptions } from './'
import type { InputOptions, OutputOptions, Plugin } from 'rollup'
import { FixDtsDefaultCjsExportsPlugin } from 'fix-dts-default-cjs-exports/rollup'
import { rewriteDtsImportExtensionsPlugin } from './plugins/rewrite-dts-import-extensions'

const logger = createLogger()

Expand Down Expand Up @@ -149,6 +150,7 @@ const getRollupConfig = async (
entryFileNames: `[name]${outputExtension}`,
chunkFileNames: `[name]-[hash]${outputExtension}`,
plugins: [
rewriteDtsImportExtensionsPlugin(),
format === 'cjs' &&
options.cjsInterop &&
FixDtsDefaultCjsExportsPlugin(),
Expand Down
71 changes: 71 additions & 0 deletions test/dts.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -480,3 +480,74 @@ test('declaration files with multiple entrypoints #316', async () => {
'dist/bar/index.d.ts',
).toMatchSnapshot()
})

test('dts should rewrite .ts import extensions to .mjs for esm format', async () => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'src/input.ts': `export type { Foo } from './types.ts'`,
'src/types.ts': `export type Foo = string`,
'tsup.config.ts': `
export default {
entry: ['src/input.ts'],
format: ['esm'],
dts: { only: true },
external: [/\\.\\/types/]
}
`,
},
{ entry: [] },
)
expect(outFiles).toContain('input.d.mts')
const dts = await getFileContent('dist/input.d.mts')
expect(dts).toContain('./types.mjs')
expect(dts).not.toContain('./types.ts')
})

test('dts should rewrite .ts import extensions to .cjs for cjs format with type:module', async () => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'src/input.ts': `export type { Foo } from './types.ts'`,
'src/types.ts': `export type Foo = string`,
'package.json': `{ "type": "module" }`,
'tsup.config.ts': `
export default {
entry: ['src/input.ts'],
format: ['cjs'],
dts: { only: true },
external: [/\\.\\/types/]
}
`,
},
{ entry: [] },
)
expect(outFiles).toContain('input.d.cts')
const dts = await getFileContent('dist/input.d.cts')
expect(dts).toContain('./types.cjs')
expect(dts).not.toContain('./types.ts')
})

test('dts should rewrite .ts import extensions to .js for esm format with type:module', async () => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'src/input.ts': `export type { Foo } from './types.ts'`,
'src/types.ts': `export type Foo = string`,
'package.json': `{ "type": "module" }`,
'tsup.config.ts': `
export default {
entry: ['src/input.ts'],
format: ['esm'],
dts: { only: true },
external: [/\\.\\/types/]
}
`,
},
{ entry: [] },
)
expect(outFiles).toContain('input.d.ts')
const dts = await getFileContent('dist/input.d.ts')
expect(dts).toContain('./types.js')
expect(dts).not.toContain('./types.ts')
})
133 changes: 133 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -925,3 +925,136 @@ test('generate sourcemap with --treeshake', async () => {
}),
)
})

test('rewrite .ts import extensions with bundle:false', async () => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'input.ts': `import { foo } from './foo.ts';\nexport { foo };`,
'foo.ts': `export const foo = 'foo'`,
'tsup.config.ts': `export default { bundle: false }`,
},
{
entry: ['input.ts', 'foo.ts'],
},
)
expect(outFiles).toContain('input.js')
expect(outFiles).toContain('foo.js')
const output = await getFileContent('dist/input.js')
expect(output).toContain('./foo.js')
expect(output).not.toContain('./foo.ts')
})

test('rewrite .ts import extensions to .mjs for esm format', async () => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'input.ts': `import { foo } from './foo.ts';\nexport { foo };`,
'foo.ts': `export const foo = 'foo'`,
'tsup.config.ts': `export default { bundle: false }`,
},
{
entry: ['input.ts', 'foo.ts'],
flags: ['--format', 'esm'],
},
)
expect(outFiles).toContain('input.mjs')
const output = await getFileContent('dist/input.mjs')
expect(output).toContain('./foo.mjs')
expect(output).not.toContain('./foo.ts')
})

test('rewrite .ts import extensions to .cjs for cjs format with type:module', async () => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'input.ts': `import { foo } from './foo.ts';\nexport { foo };`,
'foo.ts': `export const foo = 'foo'`,
'package.json': JSON.stringify({ type: 'module' }),
'tsup.config.ts': `export default { bundle: false }`,
},
{
entry: ['input.ts', 'foo.ts'],
flags: ['--format', 'cjs'],
},
)
expect(outFiles).toContain('input.cjs')
const output = await getFileContent('dist/input.cjs')
expect(output).toContain('./foo.cjs')
expect(output).not.toContain('./foo.ts')
})

test('rewrite .mts and .cts import extensions', async () => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'input.ts': `import { foo } from './foo.mts';\nimport { bar } from './bar.cts';\nexport { foo, bar };`,
'foo.mts': `export const foo = 'foo'`,
'bar.cts': `export const bar = 'bar'`,
'tsup.config.ts': `export default { bundle: false }`,
},
{
entry: ['input.ts', 'foo.mts', 'bar.cts'],
},
)
expect(outFiles).toContain('input.js')
const output = await getFileContent('dist/input.js')
expect(output).toContain('./foo.mjs')
expect(output).toContain('./bar.cjs')
expect(output).not.toContain('.mts')
expect(output).not.toContain('.cts')
})

test('rewrite export from with .ts extension', async () => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'input.ts': `export { foo } from './foo.ts';`,
'foo.ts': `export const foo = 'foo'`,
'tsup.config.ts': `export default { bundle: false }`,
},
{
entry: ['input.ts', 'foo.ts'],
},
)
expect(outFiles).toContain('input.js')
const output = await getFileContent('dist/input.js')
expect(output).toContain('./foo.js')
expect(output).not.toContain('./foo.ts')
})

test('rewrite dynamic import with .ts extension', async () => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'input.ts': `export const load = () => import('./foo.ts');`,
'foo.ts': `export const foo = 'foo'`,
'tsup.config.ts': `export default { bundle: false }`,
},
{
entry: ['input.ts', 'foo.ts'],
},
)
expect(outFiles).toContain('input.js')
const output = await getFileContent('dist/input.js')
expect(output).toContain('./foo.js')
expect(output).not.toContain('./foo.ts')
})

test('rewrite .ts import extensions with query string', async () => {
const { getFileContent, outFiles } = await run(
getTestName(),
{
'input.ts': `import data from './data.ts?raw';\nexport { data };`,
'data.ts': `export default 'data'`,
'tsup.config.ts': `export default { bundle: false }`,
},
{
entry: ['input.ts', 'data.ts'],
},
)
expect(outFiles).toContain('input.js')
const output = await getFileContent('dist/input.js')
expect(output).toContain('./data.js?raw')
expect(output).not.toContain('./data.ts')
})