Skip to content

Commit 8ed1b9f

Browse files
Doctor-wusxzz
andauthored
feat(entry): support glob negation patterns in object entry (#662)
Co-authored-by: Kevin Deng <sxzz@sxzz.moe>
1 parent 0430b57 commit 8ed1b9f

File tree

4 files changed

+137
-9
lines changed

4 files changed

+137
-9
lines changed

src/config/options.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ export async function resolveUserConfig(
120120
outDir = path.resolve(cwd, outDir)
121121
clean = resolveClean(clean, outDir, cwd)
122122

123-
entry = await resolveEntry(logger, entry, cwd, color, nameLabel)
123+
const resolvedEntry = await resolveEntry(logger, entry, cwd, color, nameLabel)
124124
if (dts == null) {
125125
dts = !!(pkg?.types || pkg?.typings || hasExportsTypes(pkg))
126126
}
@@ -227,7 +227,7 @@ export async function resolveUserConfig(
227227
cwd,
228228
debug,
229229
dts,
230-
entry,
230+
entry: resolvedEntry,
231231
env,
232232
exports,
233233
external,

src/config/types.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import type { Hookable } from 'hookable'
2929
import type { Options as PublintOptions } from 'publint'
3030
import type {
3131
ExternalOption,
32-
InputOption,
3332
InputOptions,
3433
InternalModuleFormat,
3534
MinifyOptions,
@@ -44,6 +43,29 @@ import type { Options as UnusedOptions } from 'unplugin-unused'
4443
export type Sourcemap = boolean | 'inline' | 'hidden'
4544
export type Format = ModuleFormat
4645
export type NormalizedFormat = InternalModuleFormat
46+
47+
/**
48+
* Extended input option that supports glob negation patterns.
49+
*
50+
* When using object form, values can be:
51+
* - A single glob pattern string
52+
* - An array of glob patterns, including negation patterns (prefixed with `!`)
53+
*
54+
* @example
55+
* ```ts
56+
* entry: {
57+
* // Single pattern
58+
* "utils/*": "./src/utils/*.ts",
59+
* // Array with negation pattern to exclude files
60+
* "hooks/*": ["./src/hooks/*.ts", "!./src/hooks/index.ts"],
61+
* }
62+
* ```
63+
*/
64+
export type TsdownInputOption =
65+
| string
66+
| string[]
67+
| Record<string, string | string[]>
68+
4769
export type {
4870
AttwOptions,
4971
BuildContext,
@@ -113,8 +135,16 @@ export interface UserConfig {
113135
// #region Input Options
114136
/**
115137
* Defaults to `'src/index.ts'` if it exists.
138+
*
139+
* Supports glob patterns with negation to exclude files:
140+
* @example
141+
* ```ts
142+
* entry: {
143+
* "hooks/*": ["./src/hooks/*.ts", "!./src/hooks/index.ts"],
144+
* }
145+
* ```
116146
*/
117-
entry?: InputOption
147+
entry?: TsdownInputOption
118148

119149
external?: ExternalOption
120150
noExternal?: Arrayable<string | RegExp> | NoExternalFn
@@ -553,6 +583,8 @@ export type ResolvedConfig = Overwrite<
553583
| 'footer'
554584
>,
555585
{
586+
/** Resolved entry map (after glob expansion) */
587+
entry: Record<string, string>
556588
nameLabel: string | undefined
557589
format: NormalizedFormat
558590
target?: string[]

src/features/entry.test.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,76 @@ describe('toObjectEntry', () => {
113113
'src/bar': path.join(testDir, 'src/bar.ts'),
114114
})
115115
})
116+
117+
// #660
118+
test('object entry with glob negation pattern', async (context) => {
119+
const { testDir } = await writeFixtures(context, {
120+
'src/hooks/index.ts': '',
121+
'src/hooks/useAuth.ts': '',
122+
'src/hooks/useUser.ts': '',
123+
})
124+
const result = await toObjectEntry(
125+
{
126+
'hooks/*': ['src/hooks/*.ts', '!src/hooks/index.ts'],
127+
},
128+
testDir,
129+
)
130+
expect(result).toEqual({
131+
'hooks/useAuth': path.join(testDir, 'src/hooks/useAuth.ts'),
132+
'hooks/useUser': path.join(testDir, 'src/hooks/useUser.ts'),
133+
})
134+
expect(Object.keys(result)).not.toContain('hooks/index')
135+
})
136+
137+
test('object entry with multiple negation patterns', async (context) => {
138+
const { testDir } = await writeFixtures(context, {
139+
'src/utils/index.ts': '',
140+
'src/utils/internal.ts': '',
141+
'src/utils/helper.ts': '',
142+
'src/utils/format.ts': '',
143+
})
144+
const result = await toObjectEntry(
145+
{
146+
'utils/*': [
147+
'src/utils/*.ts',
148+
'!src/utils/index.ts',
149+
'!src/utils/internal.ts',
150+
],
151+
},
152+
testDir,
153+
)
154+
expect(result).toEqual({
155+
'utils/helper': path.join(testDir, 'src/utils/helper.ts'),
156+
'utils/format': path.join(testDir, 'src/utils/format.ts'),
157+
})
158+
})
159+
160+
test('object entry with multiple positive patterns should throw', async (context) => {
161+
const { testDir } = await writeFixtures(context, {
162+
'src/hooks/useAuth.ts': '',
163+
'src/utils/helper.ts': '',
164+
})
165+
await expect(
166+
toObjectEntry(
167+
{
168+
'lib/*': ['src/hooks/*.ts', 'src/utils/*.ts'],
169+
},
170+
testDir,
171+
),
172+
).rejects.toThrow(/multiple positive patterns/)
173+
})
174+
175+
test('object entry with no positive pattern should throw', async (context) => {
176+
const { testDir } = await writeFixtures(context, {
177+
'src/hooks/useAuth.ts': '',
178+
})
179+
await expect(
180+
toObjectEntry(
181+
{
182+
'hooks/*': ['!src/hooks/index.ts'],
183+
},
184+
testDir,
185+
),
186+
).rejects.toThrow(/no positive pattern/)
187+
})
116188
})

src/features/entry.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'node:path'
22
import picomatch from 'picomatch'
33
import { glob, isDynamicPattern } from 'tinyglobby'
44
import { fsExists, lowestCommonAncestor, stripExtname } from '../utils/fs.ts'
5-
import { slash } from '../utils/general.ts'
5+
import { slash, toArray } from '../utils/general.ts'
66
import type { UserConfig } from '../config/index.ts'
77
import type { Logger } from '../utils/logger.ts'
88
import type { Ansis } from 'ansis'
@@ -39,7 +39,7 @@ export async function resolveEntry(
3939
}
4040

4141
export async function toObjectEntry(
42-
entry: string | string[] | Record<string, string>,
42+
entry: string | string[] | Record<string, string | string[]>,
4343
cwd: string,
4444
): Promise<Record<string, string>> {
4545
if (typeof entry === 'string') {
@@ -51,10 +51,34 @@ export async function toObjectEntry(
5151
(
5252
await Promise.all(
5353
Object.entries(entry).map(async ([key, value]) => {
54-
if (!key.includes('*')) return [[key, value]]
54+
if (!key.includes('*')) {
55+
if (Array.isArray(value)) {
56+
throw new TypeError(
57+
`Object entry "${key}" cannot have an array value when the key is not a glob pattern.`,
58+
)
59+
}
5560

56-
const valueGlob = picomatch.scan(value)
57-
const files = await glob(value, {
61+
return [[key, value]]
62+
}
63+
64+
const patterns = toArray(value)
65+
66+
const positivePatterns = patterns.filter((p) => !p.startsWith('!'))
67+
if (positivePatterns.length === 0) {
68+
throw new TypeError(
69+
`Object entry "${key}" has no positive pattern. At least one positive pattern is required.`,
70+
)
71+
}
72+
73+
if (positivePatterns.length > 1) {
74+
throw new TypeError(
75+
`Object entry "${key}" has multiple positive patterns: ${positivePatterns.join(', ')}. ` +
76+
`Only one positive pattern is allowed. Use negation patterns (prefixed with "!") to exclude files.`,
77+
)
78+
}
79+
80+
const valueGlob = picomatch.scan(positivePatterns[0])
81+
const files = await glob(patterns, {
5882
cwd,
5983
expandDirectories: false,
6084
})

0 commit comments

Comments
 (0)