Skip to content

Commit a808345

Browse files
committed
feat(entry): support mixed array and object entries
closes #708
1 parent be1b60c commit a808345

File tree

3 files changed

+119
-64
lines changed

3 files changed

+119
-64
lines changed

src/config/types.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,10 +63,9 @@ export type NormalizedFormat = InternalModuleFormat
6363
* }
6464
* ```
6565
*/
66-
export type TsdownInputOption =
67-
| string
68-
| string[]
69-
| Record<string, string | string[]>
66+
export type TsdownInputOption = Arrayable<
67+
string | Record<string, Arrayable<string>>
68+
>
7069

7170
export type {
7271
AttwOptions,

src/features/entry.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,4 +190,28 @@ describe('toObjectEntry', () => {
190190
),
191191
).rejects.toThrow('Cannot find files')
192192
})
193+
194+
test('mixed object and array entry', async (context) => {
195+
const { testDir } = await writeFixtures(context, {
196+
'index.ts': '',
197+
'src/foo.ts': '',
198+
'src/bar.ts': '',
199+
})
200+
const result = await toObjectEntry(
201+
[
202+
'src/*',
203+
'!src/foo.ts',
204+
205+
{ main: 'index.ts' },
206+
{ 'lib/*': ['src/*.ts', '!src/bar.ts'] },
207+
{ bar: 'override by object' },
208+
],
209+
testDir,
210+
)
211+
expect(result).toEqual({
212+
main: 'index.ts',
213+
bar: 'override by object',
214+
'lib/foo': path.join(testDir, 'src/foo.ts'),
215+
})
216+
})
193217
})

src/features/entry.ts

Lines changed: 92 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import picomatch from 'picomatch'
33
import { glob, isDynamicPattern } from 'tinyglobby'
44
import { fsExists, lowestCommonAncestor, stripExtname } from '../utils/fs.ts'
55
import { slash, toArray } from '../utils/general.ts'
6-
import type { UserConfig } from '../config/index.ts'
6+
import type { TsdownInputOption, UserConfig } from '../config/types.ts'
77
import type { Logger } from '../utils/logger.ts'
8+
import type { Arrayable } from '../utils/types.ts'
89
import type { Ansis } from 'ansis'
910

1011
export async function resolveEntry(
@@ -38,94 +39,125 @@ export async function resolveEntry(
3839
return entryMap
3940
}
4041

41-
export async function toObjectEntry(
42-
entry: string | string[] | Record<string, string | string[]>,
42+
export function toObjectEntry(
43+
entry: TsdownInputOption,
4344
cwd: string,
4445
): Promise<Record<string, string>> {
4546
if (typeof entry === 'string') {
4647
entry = [entry]
4748
}
48-
if (!Array.isArray(entry)) {
49-
// resolve object entry with globs
50-
return Object.fromEntries(
51-
(
52-
await Promise.all(
53-
Object.entries(entry).map(async ([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-
}
6049

61-
return [[key, value]]
62-
}
50+
if (!Array.isArray(entry)) {
51+
return resolveObjectEntry(entry, cwd)
52+
}
53+
return resolveArrayEntry(entry, cwd)
54+
}
6355

64-
const patterns = toArray(value)
65-
const files = await glob(patterns, {
66-
cwd,
67-
expandDirectories: false,
68-
})
69-
if (!files.length) {
70-
throw new Error(
71-
`Cannot find files for entry key "${key}" with patterns: ${JSON.stringify(
72-
patterns,
73-
)}`,
56+
async function resolveObjectEntry(
57+
entries: Record<string, string | string[]>,
58+
cwd: string,
59+
) {
60+
return Object.fromEntries(
61+
(
62+
await Promise.all(
63+
Object.entries(entries).map(async ([key, value]) => {
64+
if (!key.includes('*')) {
65+
if (Array.isArray(value)) {
66+
throw new TypeError(
67+
`Object entry "${key}" cannot have an array value when the key is not a glob pattern.`,
7468
)
7569
}
7670

77-
let valueGlobBase: string | undefined
78-
for (const pattern of patterns) {
79-
if (pattern.startsWith('!')) continue
80-
const base = picomatch.scan(pattern).base
81-
if (valueGlobBase === undefined) {
82-
valueGlobBase = base
83-
} else if (valueGlobBase !== base) {
84-
throw new Error(
85-
`When using object entry with glob pattern key "${key}", all value glob patterns must have the same base directory.`,
86-
)
87-
}
88-
}
71+
return [[key, value]]
72+
}
73+
74+
const patterns = toArray(value)
75+
const files = await glob(patterns, {
76+
cwd,
77+
expandDirectories: false,
78+
})
79+
if (!files.length) {
80+
throw new Error(
81+
`Cannot find files for entry key "${key}" with patterns: ${JSON.stringify(
82+
patterns,
83+
)}`,
84+
)
85+
}
86+
87+
let valueGlobBase: string | undefined
88+
for (const pattern of patterns) {
89+
if (pattern.startsWith('!')) continue
90+
const base = picomatch.scan(pattern).base
8991
if (valueGlobBase === undefined) {
92+
valueGlobBase = base
93+
} else if (valueGlobBase !== base) {
9094
throw new Error(
91-
`Cannot determine base directory for value glob patterns of key "${key}".`,
95+
`When using object entry with glob pattern key "${key}", all value glob patterns must have the same base directory.`,
9296
)
9397
}
98+
}
99+
if (valueGlobBase === undefined) {
100+
throw new Error(
101+
`Cannot determine base directory for value glob patterns of key "${key}".`,
102+
)
103+
}
94104

95-
return files.map((file) => [
96-
slash(
97-
key.replaceAll(
98-
'*',
99-
stripExtname(path.relative(valueGlobBase, file)),
100-
),
105+
return files.map((file) => [
106+
slash(
107+
key.replaceAll(
108+
'*',
109+
stripExtname(path.relative(valueGlobBase, file)),
101110
),
102-
path.resolve(cwd, file),
103-
])
104-
}),
105-
)
106-
).flat(),
107-
)
111+
),
112+
path.resolve(cwd, file),
113+
])
114+
}),
115+
)
116+
).flat(),
117+
)
118+
}
119+
120+
async function resolveArrayEntry(
121+
entries: (string | Record<string, Arrayable<string>>)[],
122+
cwd: string,
123+
) {
124+
const stringEntries: string[] = []
125+
const objectEntries: Record<string, Arrayable<string>>[] = []
126+
for (const e of entries) {
127+
if (typeof e === 'string') {
128+
stringEntries.push(e)
129+
} else {
130+
objectEntries.push(e)
131+
}
108132
}
109133

110-
const isGlob = entry.some((e) => isDynamicPattern(e))
111-
let resolvedEntry: string[]
134+
const isGlob = stringEntries.some((e) => isDynamicPattern(e))
135+
let resolvedEntries: string[]
112136
if (isGlob) {
113-
resolvedEntry = (
114-
await glob(entry, {
137+
resolvedEntries = (
138+
await glob(stringEntries, {
115139
cwd,
116140
expandDirectories: false,
117141
absolute: true,
118142
})
119143
).map((file) => path.resolve(file))
120144
} else {
121-
resolvedEntry = entry
145+
resolvedEntries = stringEntries
122146
}
123147

124-
const base = lowestCommonAncestor(...resolvedEntry)
125-
return Object.fromEntries(
126-
resolvedEntry.map((file) => {
148+
const base = lowestCommonAncestor(...resolvedEntries)
149+
const arrayEntryMap = Object.fromEntries(
150+
resolvedEntries.map((file) => {
127151
const relative = path.relative(base, file)
128152
return [slash(stripExtname(relative)), file]
129153
}),
130154
)
155+
156+
return Object.assign(
157+
{},
158+
arrayEntryMap,
159+
...(await Promise.all(
160+
objectEntries.map((entry) => resolveObjectEntry(entry, cwd)),
161+
)),
162+
)
131163
}

0 commit comments

Comments
 (0)