Skip to content

Commit 5c71f67

Browse files
authored
feat(exports): add inlinedDependencies field to package.json (#785)
1 parent 0205fa0 commit 5c71f67

File tree

10 files changed

+282
-99
lines changed

10 files changed

+282
-99
lines changed

dts.snapshot.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,11 +100,11 @@
100100
"CopyOptions": "type CopyOptions = Arrayable<string | CopyEntry>",
101101
"CopyOptionsFn": "type CopyOptionsFn = (_: ResolvedConfig) => Awaitable<CopyOptions>",
102102
"CssOptions": "interface CssOptions {\n splitting?: boolean\n fileName?: string\n}",
103-
"DepPlugin": "declare function DepPlugin(_: ResolvedConfig): Plugin",
103+
"DepPlugin": "declare function DepPlugin(_: ResolvedConfig, _: TsdownBundle): Plugin",
104104
"DepsConfig": "interface DepsConfig {\n neverBundle?: ExternalOption\n alwaysBundle?: Arrayable<string | RegExp> | NoExternalFn\n onlyAllowBundle?: Arrayable<string | RegExp> | false\n skipNodeModulesBundle?: boolean\n}",
105105
"DevtoolsOptions": "interface DevtoolsOptions extends NonNullable<InputOptions['devtools']> {\n ui?: boolean | Partial<StartOptions>\n clean?: boolean\n}",
106106
"ExeOptions": "interface ExeOptions {\n seaConfig?: Omit<SeaConfig, 'main' | 'output' | 'mainFormat'>\n fileName?: string | ((_: RolldownChunk) => string)\n}",
107-
"ExportsOptions": "interface ExportsOptions {\n devExports?: boolean | string\n packageJson?: boolean\n all?: boolean\n exclude?: (RegExp | string)[]\n legacy?: boolean\n customExports?: Record<string, any> | ((_: Record<string, any>, _: { pkg: PackageJson; chunks: ChunksByFormat; isPublish: boolean }) => Awaitable<Record<string, any>>)\n}",
107+
"ExportsOptions": "interface ExportsOptions {\n devExports?: boolean | string\n packageJson?: boolean\n all?: boolean\n exclude?: (RegExp | string)[]\n legacy?: boolean\n customExports?: Record<string, any> | ((_: Record<string, any>, _: { pkg: PackageJson; chunks: ChunksByFormat; isPublish: boolean }) => Awaitable<Record<string, any>>)\n inlinedDependencies?: boolean\n}",
108108
"Format": "type Format = ModuleFormat",
109109
"globalLogger": "Logger",
110110
"InlineConfig": "interface InlineConfig extends UserConfig {\n config?: boolean | string\n configLoader?: 'auto' | 'native' | 'unrun'\n filter?: RegExp | Arrayable<string>\n}",
@@ -142,7 +142,7 @@
142142
"RolldownContext": "interface RolldownContext {\n buildOptions: BuildOptions\n}",
143143
"SeaConfig": "interface SeaConfig {\n main?: string\n executable?: string\n output?: string\n mainFormat?: 'commonjs' | 'module'\n disableExperimentalSEAWarning?: boolean\n useSnapshot?: boolean\n useCodeCache?: boolean\n execArgv?: string[]\n execArgvExtension?: 'none' | 'env' | 'cli'\n assets?: Record<string, string>\n}",
144144
"Sourcemap": "type Sourcemap = boolean | 'inline' | 'hidden'",
145-
"TsdownBundle": "interface TsdownBundle extends AsyncDisposable {\n chunks: RolldownChunk[]\n config: ResolvedConfig\n}",
145+
"TsdownBundle": "interface TsdownBundle extends AsyncDisposable {\n chunks: RolldownChunk[]\n config: ResolvedConfig\n inlinedDeps: Map<string, Set<string>>\n}",
146146
"TsdownHooks": "interface TsdownHooks {\n 'build:prepare': (_: BuildContext) => void | Promise<void>\n 'build:before': (_: BuildContext & RolldownContext) => void | Promise<void>\n 'build:done': (_: BuildContext & { chunks: RolldownChunk[] }) => void | Promise<void>\n}",
147147
"TsdownInputOption": "type TsdownInputOption = Arrayable<string | Record<string, Arrayable<string>>>",
148148
"UserConfig": "interface UserConfig {\n entry?: TsdownInputOption\n deps?: DepsConfig\n external?: ExternalOption\n noExternal?: Arrayable<string | RegExp> | NoExternalFn\n inlineOnly?: Arrayable<string | RegExp> | false\n skipNodeModulesBundle?: boolean\n alias?: Record<string, string>\n tsconfig?: string | boolean\n platform?: 'node' | 'neutral' | 'browser'\n target?: string | string[] | false\n env?: Record<string, any>\n envFile?: string\n envPrefix?: string | string[]\n define?: Record<string, string>\n shims?: boolean\n treeshake?: boolean | TreeshakingOptions\n loader?: ModuleTypes\n removeNodeProtocol?: boolean\n nodeProtocol?: 'strip' | boolean\n checks?: ChecksOptions & { legacyCjs?: boolean }\n plugins?: InputOptions['plugins']\n inputOptions?: InputOptions | ((_: InputOptions, _: NormalizedFormat, _: { cjsDts: boolean }) => Awaitable<InputOptions | void | null>)\n format?: Format | Format[] | Partial<Record<Format, Partial<ResolvedConfig>>>\n globalName?: string\n outDir?: string\n write?: boolean\n sourcemap?: Sourcemap\n clean?: boolean | string[]\n minify?: boolean | 'dce-only' | MinifyOptions\n footer?: ChunkAddon\n banner?: ChunkAddon\n unbundle?: boolean\n bundle?: boolean\n fixedExtension?: boolean\n outExtensions?: OutExtensionFactory\n hash?: boolean\n cjsDefault?: boolean\n outputOptions?: OutputOptions | ((_: OutputOptions, _: NormalizedFormat, _: { cjsDts: boolean }) => Awaitable<OutputOptions | void | null>)\n cwd?: string\n name?: string\n logLevel?: LogLevel\n failOnWarn?: boolean | CIOption\n customLogger?: Logger\n fromVite?: boolean | 'vitest'\n watch?: boolean | Arrayable<string>\n ignoreWatch?: Arrayable<string | RegExp>\n devtools?: WithEnabled<DevtoolsOptions>\n onSuccess?: string | ((_: ResolvedConfig, _: AbortSignal) => void | Promise<void>)\n dts?: WithEnabled<DtsOptions>\n unused?: WithEnabled<UnusedOptions>\n publint?: WithEnabled<PublintOptions>\n attw?: WithEnabled<AttwOptions>\n report?: WithEnabled<ReportOptions>\n globImport?: boolean\n exports?: WithEnabled<ExportsOptions>\n css?: CssOptions\n publicDir?: CopyOptions | CopyOptionsFn\n copy?: CopyOptions | CopyOptionsFn\n hooks?: Partial<TsdownHooks> | ((_: Hookable<TsdownHooks>) => Awaitable<void>)\n exe?: WithEnabled<ExeOptions>\n workspace?: Workspace | Arrayable<string> | true\n}",

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,5 +141,11 @@
141141
"vite": "catalog:docs",
142142
"vitest": "catalog:dev"
143143
},
144+
"inlinedDependencies": {
145+
"package-manager-detector": "1.6.0",
146+
"@publint/pack": "0.1.4",
147+
"is-in-ci": "2.0.0",
148+
"pkg-types": "2.3.0"
149+
},
144150
"prettier": "@sxzz/prettier-config"
145151
}

src/build.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ async function buildSingle(
156156
const bundle: TsdownBundle = {
157157
chunks,
158158
config,
159+
inlinedDeps: new Map(),
159160
async [asyncDispose]() {
160161
debouncedPostBuild.cancel()
161162
ab?.abort()

src/features/deps.ts

Lines changed: 118 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { readFile } from 'node:fs/promises'
12
import { isBuiltin } from 'node:module'
3+
import path from 'node:path'
24
import { blue, underline, yellow } from 'ansis'
35
import { createDebug } from 'obug'
46
import { RE_DTS, RE_NODE_MODULES } from 'rolldown-plugin-dts/filename'
@@ -12,6 +14,7 @@ import {
1214
} from '../utils/general.ts'
1315
import { shimFile } from './shims.ts'
1416
import type { ResolvedConfig, UserConfig } from '../config/types.ts'
17+
import type { TsdownBundle } from '../utils/chunks.ts'
1518
import type { Logger } from '../utils/logger.ts'
1619
import type { Arrayable } from '../utils/types.ts'
1720
import type { PackageJson } from 'pkg-types'
@@ -146,12 +149,45 @@ export function resolveDepsConfig(
146149
}
147150
}
148151

149-
export function DepPlugin({
150-
pkg,
151-
deps: { alwaysBundle, onlyAllowBundle, skipNodeModulesBundle },
152-
logger,
153-
nameLabel,
154-
}: ResolvedConfig): Plugin {
152+
async function parseBundledDep(
153+
moduleId: string,
154+
): Promise<{ name: string; pkgName: string; version: string } | undefined> {
155+
const slashed = slash(moduleId)
156+
const lastNmIdx = slashed.lastIndexOf('/node_modules/')
157+
if (lastNmIdx === -1) return
158+
159+
const afterNm = slashed.slice(lastNmIdx + 14 /* '/node_modules/'.length */)
160+
const parts = afterNm.split('/')
161+
162+
let name: string
163+
if (parts[0][0] === '@') {
164+
name = `${parts[0]}/${parts[1]}`
165+
} else {
166+
name = parts[0]
167+
}
168+
169+
const root = slashed.slice(
170+
0,
171+
lastNmIdx + 14 /* '/node_modules/'.length */ + name.length,
172+
)
173+
174+
try {
175+
const json = JSON.parse(
176+
await readFile(path.join(root, 'package.json'), 'utf8'),
177+
)
178+
return { name, pkgName: json.name, version: json.version }
179+
} catch {}
180+
}
181+
182+
export function DepPlugin(
183+
{
184+
pkg,
185+
deps: { alwaysBundle, onlyAllowBundle, skipNodeModulesBundle },
186+
logger,
187+
nameLabel,
188+
}: ResolvedConfig,
189+
tsdownBundle: TsdownBundle,
190+
): Plugin {
155191
const deps = pkg && Array.from(getProductionDeps(pkg))
156192

157193
return {
@@ -182,93 +218,82 @@ export function DepPlugin({
182218
},
183219
},
184220

185-
generateBundle:
186-
onlyAllowBundle === false
187-
? undefined
188-
: {
189-
order: 'post',
190-
handler(options, bundle) {
191-
const deps = new Set<string>()
192-
const importers = new Map<string, Set<string>>()
193-
194-
for (const chunk of Object.values(bundle)) {
195-
if (chunk.type === 'asset') continue
196-
197-
for (const id of chunk.moduleIds) {
198-
if (!RE_NODE_MODULES.test(id)) continue
199-
200-
const parts = slash(id)
201-
.split('/node_modules/')
202-
.at(-1)
203-
?.split('/')
204-
if (!parts) continue
205-
206-
let dep: string
207-
if (parts[0][0] === '@') {
208-
dep = `${parts[0]}/${parts[1]}`
209-
} else {
210-
dep = parts[0]
211-
}
212-
deps.add(dep)
213-
214-
const module = this.getModuleInfo(id)
215-
if (module) {
216-
importers.set(
217-
dep,
218-
new Set([
219-
...module.importers,
220-
...(importers.get(dep) || []),
221-
]),
222-
)
223-
}
224-
}
225-
}
226-
227-
debug('found deps in bundle: %o', deps)
228-
229-
if (onlyAllowBundle) {
230-
const errors = Array.from(deps)
231-
.filter((dep) => !matchPattern(dep, onlyAllowBundle))
232-
.map(
233-
(dep) =>
234-
`${yellow(dep)} is located in ${blue`node_modules`} but is not included in ${blue`deps.onlyAllowBundle`} option.\n` +
235-
`To fix this, either add it to ${blue`deps.onlyAllowBundle`}, declare it as a production or peer dependency in your package.json, or externalize it manually.\n` +
236-
`Imported by\n${[...(importers.get(dep) || [])]
237-
.map((s) => `- ${underline(s)}`)
238-
.join('\n')}`,
239-
)
240-
if (errors.length) {
241-
this.error(errors.join('\n\n'))
242-
}
243-
244-
const unusedPatterns = onlyAllowBundle.filter(
245-
(pattern) =>
246-
!Array.from(deps).some((dep) =>
247-
matchPattern(dep, [pattern]),
248-
),
249-
)
250-
if (unusedPatterns.length) {
251-
logger.info(
252-
nameLabel,
253-
`The following entries in ${blue`deps.onlyAllowBundle`} are not used in the bundle:\n${unusedPatterns
254-
.map((pattern) => `- ${yellow(pattern)}`)
255-
.join(
256-
'\n',
257-
)}\nConsider removing them to keep your configuration clean.`,
258-
)
259-
}
260-
} else if (deps.size) {
261-
logger.info(
262-
nameLabel,
263-
`Hint: consider adding ${blue`deps.onlyAllowBundle`} option to avoid unintended bundling of dependencies, or set ${blue`deps.onlyAllowBundle: false`} to disable this hint.\n` +
264-
`See more at ${underline`https://tsdown.dev/options/dependencies#deps-onlyallowbundle`}\n` +
265-
`Detected dependencies in bundle:\n${Array.from(deps)
266-
.map((dep) => `- ${blue(dep)}`)
267-
.join('\n')}`,
268-
)
269-
}
270-
},
271-
},
221+
generateBundle: {
222+
order: 'post',
223+
async handler(options, bundle) {
224+
const deps = new Set<string>()
225+
const importers = new Map<string, Set<string>>()
226+
227+
for (const chunk of Object.values(bundle)) {
228+
if (chunk.type === 'asset') continue
229+
230+
for (const id of chunk.moduleIds) {
231+
const parsed = await parseBundledDep(id)
232+
if (!parsed) continue
233+
234+
deps.add(parsed.name)
235+
236+
if (!tsdownBundle.inlinedDeps.has(parsed.pkgName)) {
237+
tsdownBundle.inlinedDeps.set(parsed.pkgName, new Set())
238+
}
239+
tsdownBundle.inlinedDeps.get(parsed.pkgName)!.add(parsed.version)
240+
241+
const module = this.getModuleInfo(id)
242+
if (module) {
243+
importers.set(
244+
parsed.name,
245+
new Set([
246+
...module.importers,
247+
...(importers.get(parsed.name) || []),
248+
]),
249+
)
250+
}
251+
}
252+
}
253+
254+
debug('found deps in bundle: %o', deps)
255+
256+
if (onlyAllowBundle) {
257+
const errors = Array.from(deps)
258+
.filter((dep) => !matchPattern(dep, onlyAllowBundle))
259+
.map(
260+
(dep) =>
261+
`${yellow(dep)} is located in ${blue`node_modules`} but is not included in ${blue`deps.onlyAllowBundle`} option.\n` +
262+
`To fix this, either add it to ${blue`deps.onlyAllowBundle`}, declare it as a production or peer dependency in your package.json, or externalize it manually.\n` +
263+
`Imported by\n${[...(importers.get(dep) || [])]
264+
.map((s) => `- ${underline(s)}`)
265+
.join('\n')}`,
266+
)
267+
if (errors.length) {
268+
this.error(errors.join('\n\n'))
269+
}
270+
271+
const unusedPatterns = onlyAllowBundle.filter(
272+
(pattern) =>
273+
!Array.from(deps).some((dep) => matchPattern(dep, [pattern])),
274+
)
275+
if (unusedPatterns.length) {
276+
logger.info(
277+
nameLabel,
278+
`The following entries in ${blue`deps.onlyAllowBundle`} are not used in the bundle:\n${unusedPatterns
279+
.map((pattern) => `- ${yellow(pattern)}`)
280+
.join(
281+
'\n',
282+
)}\nConsider removing them to keep your configuration clean.`,
283+
)
284+
}
285+
} else if (onlyAllowBundle == null && deps.size) {
286+
logger.info(
287+
nameLabel,
288+
`Hint: consider adding ${blue`deps.onlyAllowBundle`} option to avoid unintended bundling of dependencies, or set ${blue`deps.onlyAllowBundle: false`} to disable this hint.\n` +
289+
`See more at ${underline`https://tsdown.dev/options/dependencies#deps-onlyallowbundle`}\n` +
290+
`Detected dependencies in bundle:\n${Array.from(deps)
291+
.map((dep) => `- ${blue(dep)}`)
292+
.join('\n')}`,
293+
)
294+
}
295+
},
296+
},
272297
}
273298

274299
/**

0 commit comments

Comments
 (0)