diff --git a/src/index.ts b/src/index.ts index b0cdfa06..0561ae1f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -108,7 +108,9 @@ const normalizeOptions = async ( } if (Array.isArray(entry)) { - options.entry = await glob(entry) + // Normalize Windows paths before passing to glob + const normalizedEntry = entry.map(slash) + options.entry = await glob(normalizedEntry) // Ensure entry exists if (!options.entry || options.entry.length === 0) { throw new PrettyError(`Cannot find ${entry}`) @@ -116,14 +118,18 @@ const normalizeOptions = async ( logger.info('CLI', `Building entry: ${options.entry.join(', ')}`) } } else { + const normalizedEntry: Record = {} Object.keys(entry).forEach((alias) => { const filename = entry[alias]! - if (!fs.existsSync(filename)) { + // Normalize Windows paths for each entry + const normalizedFilename = slash(filename) + if (!fs.existsSync(normalizedFilename)) { throw new PrettyError(`Cannot find ${alias}: ${filename}`) } + normalizedEntry[alias] = normalizedFilename }) - options.entry = entry - logger.info('CLI', `Building entry: ${JSON.stringify(entry)}`) + options.entry = normalizedEntry + logger.info('CLI', `Building entry: ${JSON.stringify(normalizedEntry)}`) } const tsconfig = loadTsConfig(process.cwd(), options.tsconfig) diff --git a/test/index.test.ts b/test/index.test.ts index 5b42572e..a8c7a4a4 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -217,7 +217,7 @@ test('onSuccess: use a function from config file', async () => { await new Promise((resolve) => { setTimeout(() => { console.log('world') - resolve('') + resolve('') }, 1_000) }) } @@ -601,7 +601,7 @@ test('use rollup for treeshaking --format cjs', async () => { }`, 'input.tsx': ` import ReactSelect from 'react-select' - + export const Component = (props: {}) => { return }; @@ -925,3 +925,102 @@ test('generate sourcemap with --treeshake', async () => { }), ) }) + +test('windows: complex path handling', async () => { + const { outFiles } = await run( + getTestName(), + { + 'src\\nested\\input.ts': `export const foo = 1`, + 'src\\other\\path\\test.ts': `export const bar = 2`, + }, + { + entry: [ + String.raw`src\nested\input.ts`, + String.raw`src\other\path\test.ts` + ], + }, + ) + expect(outFiles.sort()).toEqual(['nested/input.js', 'other/path/test.js']) +}) + +test('windows: object entry with backslashes', async () => { + const { outFiles } = await run( + getTestName(), + { + 'src\\nested\\input.ts': `export const foo = 1`, + }, + { + entry: { + 'custom-name': String.raw`src\nested\input.ts`, + }, + }, + ) + expect(outFiles).toEqual(['custom-name.js']) +}) + +test('windows: mixed path separators', async () => { + const { outFiles } = await run( + getTestName(), + { + 'src/nested\\input.ts': `export const foo = 1`, + 'src\\other/test.ts': `export const bar = 2`, + }, + { + entry: [ + 'src/nested\\input.ts', + String.raw`src\other/test.ts` + ], + }, + ) + expect(outFiles.sort()).toEqual(['nested/input.js', 'other/test.js']) +}) + +test('path normalization handles mixed path styles', async () => { + const { outFiles } = await run( + getTestName(), + { + 'src\\foo\\input.ts': `export const foo = 1`, + 'src/bar/input.ts': `export const bar = 2`, + 'src\\baz/input.ts': `export const baz = 3`, + }, + { + entry: [ + 'src\\foo\\input.ts', + 'src/bar/input.ts', + 'src\\baz/input.ts' + ], + flags: ['--format', 'cjs'], + }, + ) + expect(outFiles.sort()).toEqual([ + 'bar/input.js', + 'baz/input.js', + 'foo/input.js', + ]) +}) + +test('path normalization with glob patterns', async () => { + const { outFiles } = await run( + getTestName(), + { + 'src\\types\\foo.ts': `export type Foo = string`, + 'src/types/bar.ts': `export type Bar = number`, + 'src\\types\\baz.ts': `export type Baz = boolean`, + 'tsup.config.ts': ` + export default { + entry: ['src/**/*.ts'], + format: ['cjs'], + } + `, + }, + { + entry: ['src/**/*.ts'], + flags: ['--format', 'cjs'], + }, + ) + expect(outFiles.sort()).toEqual([ + 'bar.js', + 'baz.js', + 'foo.js', + ]) +}) diff --git a/test/utils.ts b/test/utils.ts index 1bce118f..ac44fb3e 100644 --- a/test/utils.ts +++ b/test/utils.ts @@ -23,32 +23,75 @@ export function getTestName() { return name } -export async function run( - title: string, - files: { [name: string]: string }, +export const run = async ( + name: string, + files: Record, options: { - entry?: string[] - flags?: string[] + entry?: string[] | Record; + flags?: string[]; env?: Record } = {}, -) { - const testDir = path.resolve(cacheDir, filenamify(title)) +) => { + const testDir = path.resolve(cacheDir, filenamify(name)) // Write entry files on disk await Promise.all( Object.keys(files).map(async (name) => { - const filePath = path.resolve(testDir, name) + // Normalize all paths to forward slashes for consistency + const normalizedName = name.replace(/\\/g, '/') + const filePath = path.resolve(testDir, normalizedName) const parentDir = path.dirname(filePath) - // Thanks to `recursive: true`, this doesn't fail even if the directory already exists. await fsp.mkdir(parentDir, { recursive: true }) - return fsp.writeFile(filePath, files[name], 'utf8') + await fsp.writeFile(filePath, files[name], 'utf8') }), ) - const entry = options.entry || ['input.ts'] + const normalizeEntry = (entry: string) => { + // Always normalize to forward slashes for consistency across platforms + // This handles Windows paths, Unix paths, and preserves glob patterns + const normalized = entry.replace(/\\/g, '/') + + // If it's a glob pattern, return as is + if (normalized.includes('*')) { + return normalized + } + + // For non-glob entries, just normalize slashes but preserve the path structure + return normalized + } + + let entryArgs: string[] = [] + let flagArgs: string[] = [] + + // Handle entries first + if (!options.entry) { + // Default to input.ts if it exists + const defaultEntry = path.resolve(testDir, 'input.ts') + if (fs.existsSync(defaultEntry)) { + entryArgs.push('input.ts') + } + } else if (Array.isArray(options.entry)) { + // For array entries, normalize each entry + entryArgs.push(...options.entry.map(normalizeEntry)) + } else { + // For object entries, normalize values + flagArgs.push( + ...Object.entries(options.entry).map( + ([key, value]) => `--entry.${key}=${normalizeEntry(value)}` + ) + ) + } + + // Add other flags after entries + if (options.flags) { + flagArgs.push(...options.flags) + } + + // Combine args with entries first, then flags + const args = [...entryArgs, ...flagArgs] // Run tsup cli - const processPromise = exec(bin, [...entry, ...(options.flags || [])], { + const processPromise = exec(bin, args, { nodeOptions: { cwd: testDir, env: { ...process.env, ...options.env },