这是indexloc提供的服务,不要输入任何密码
Skip to content

fix: normalize Windows paths in entry points to handle backslashes correctly #1300

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
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
14 changes: 10 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,22 +108,28 @@ 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}`)
} else {
logger.info('CLI', `Building entry: ${options.entry.join(', ')}`)
}
} else {
const normalizedEntry: Record<string, string> = {}
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)
Expand Down
103 changes: 101 additions & 2 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
Expand Down Expand Up @@ -601,7 +601,7 @@ test('use rollup for treeshaking --format cjs', async () => {
}`,
'input.tsx': `
import ReactSelect from 'react-select'

export const Component = (props: {}) => {
return <ReactSelect {...props} />
};
Expand Down Expand Up @@ -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',
])
})
67 changes: 55 additions & 12 deletions test/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string>,
options: {
entry?: string[]
flags?: string[]
entry?: string[] | Record<string, string>;
flags?: string[];
env?: Record<string, string>
} = {},
) {
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 },
Expand Down