Skip to content
Merged
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
78 changes: 78 additions & 0 deletions src/features/pkg/exports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -866,6 +866,84 @@ describe('generateExports', () => {
})
})

test('bin: implicit auto-detect with single shebang', async ({ expect }) => {
const results = generateExports(
{
es: [
genChunk(
'cli.js',
true,
undefined,
'#!/usr/bin/env node\nconsole.log("hello")',
),
],
},
{ exports: {} },
)
await expect(results).resolves.toMatchObject({
bin: { 'fake-pkg': './cli.js' },
})
})

test('bin: implicit auto-detect with multiple shebangs warns', async ({
expect,
}) => {
const warnings: string[] = []
const logger = {
...globalLogger,
warn: (...msgs: any[]) => {
warnings.push(msgs.join(' '))
},
}
const results = await generateExports(
{
es: [
genChunk('cli.js', true, undefined, '#!/usr/bin/env node\n'),
genChunk('tool.js', true, undefined, '#!/usr/bin/env node\n'),
],
},
{ exports: {}, logger },
)
expect(results.bin).toBeUndefined()
expect(
warnings.some((w) =>
w.includes('Multiple entry chunks with shebangs found'),
),
).toBe(true)
})

test('bin: implicit auto-detect with no shebangs silently skips', async ({
expect,
}) => {
const warnings: string[] = []
const logger = {
...globalLogger,
warn: (...msgs: any[]) => {
warnings.push(msgs.join(' '))
},
}
const results = await generateExports(
{
es: [genChunk('index.js', true, undefined, 'console.log("hello")')],
},
{ exports: {}, logger },
)
expect(results.bin).toBeUndefined()
expect(warnings.filter((w) => w.includes('bin'))).toHaveLength(0)
})

test('bin: false disables auto-detection', async ({ expect }) => {
const results = generateExports(
{
es: [genChunk('cli.js', true, undefined, '#!/usr/bin/env node\n')],
},
{ exports: { bin: false } },
)
await expect(results).resolves.toMatchObject({
bin: undefined,
})
})

test('generate css publish exports', async ({ expect }) => {
const results = generateExports(
{ es: [genChunk('index.js'), genAsset('style.css')] },
Expand Down
33 changes: 23 additions & 10 deletions src/features/pkg/exports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,13 @@ export interface ExportsOptions {
/**
* Auto-generate the `bin` field in package.json.
*
* - `true`: Auto-detect entry chunks with shebangs. Uses package name (without scope) as bin name.
* Errors if multiple shebang entries are found.
* By default, tsdown auto-detects entry chunks with shebangs.
* If exactly one is found, it is used as the bin entry.
* If multiple are found, a warning is shown.
* Set to `false` to disable auto-detection.
*
* - `true`: Auto-detect with strict behavior (errors if multiple shebang entries are found).
* - `false`: Disable bin auto-detection.
* - `string`: Source file path to use as the bin entry. Bin name defaults to package name (without scope).
* - `Record<string, string>`: Map of bin command names to source file paths.
*
Expand Down Expand Up @@ -435,17 +440,17 @@ function generateBin(
logger: Logger,
cwd: string,
): string | Record<string, string> | undefined {
if (!bin) return
if (bin === false) return

if (bin === true || typeof bin === 'string') {
if (bin === true || bin === undefined || typeof bin === 'string') {
if (!pkg.name)
throw new Error(
'Package name is required when using string form for `bin`',
)

const binName = pkg.name[0] === '@' ? pkg.name.split('/', 2)[1] : pkg.name

if (bin === true) {
if (bin === true || bin === undefined) {
let detected: string | undefined
const seen = new Set<string>()

Expand All @@ -460,18 +465,26 @@ function generateBin(
seen.add(chunk.facadeModuleId)

if (detected) {
throw new Error(
'Multiple entry chunks with shebangs found. Use `exports.bin: { name: "./src/file.ts" }` to specify which one to use.',
if (bin === true) {
throw new Error(
'Multiple entry chunks with shebangs found. Use `exports.bin: { name: "./src/file.ts" }` to specify which one to use.',
)
}
logger.warn(
'Multiple entry chunks with shebangs found. Use `exports.bin: true` or `exports.bin: { name: "./src/file.ts" }` to configure explicitly.',
)
return
}
detected = join(pkgRoot, chunk.outDir, slash(chunk.fileName))
}
}

if (detected == null) {
logger.warn(
'`exports.bin` is true but no entry chunks with shebangs were found',
)
if (bin === true) {
logger.warn(
'`exports.bin` is true but no entry chunks with shebangs were found',
)
}
return
}
return { [binName]: detected }
Expand Down
Loading