From 582b4ae2316f922a39c6389e0338cf8d6cb6e43d Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Fri, 30 Jan 2026 03:32:05 +0800 Subject: [PATCH 1/6] feat(pkg): optimize publint and attw packing with shared tarball - Add @publint/pack to deps and package-manager-detector for smart pack detection - Create tarball once and share buffer between publint and attw - Remove duplicate npm pack logic from attw, use createPackageFromTarballData - Detect package manager to optimize packing for npm, yarn, pnpm, bun --- dts.snapshot.json | 2 +- package.json | 2 ++ pnpm-lock.yaml | 9 +++++++++ pnpm-workspace.yaml | 1 + src/features/pkg/attw.ts | 25 ++++------------------- src/features/pkg/index.ts | 40 +++++++++++++++++++++++++++++++++---- src/features/pkg/publint.ts | 11 ++++++---- tsdown.config.ts | 2 ++ 8 files changed, 62 insertions(+), 30 deletions(-) diff --git a/dts.snapshot.json b/dts.snapshot.json index 528a28e61..e8f300554 100644 --- a/dts.snapshot.json +++ b/dts.snapshot.json @@ -126,7 +126,7 @@ "PackageJsonScriptWithPreAndPost": "type PackageJsonScriptWithPreAndPost = S | `${'pre' | 'post'}${S}`", "PackageJsonWithPath": "interface PackageJsonWithPath extends PackageJson {\n packageJsonPath: string\n}", "PackageType": "type PackageType = 'module' | 'commonjs' | undefined", - "PublintOptions": "interface PublintOptions extends Options {}", + "PublintOptions": "interface PublintOptions extends Omit {}", "ReportOptions": "interface ReportOptions {\n gzip?: boolean\n brotli?: boolean\n maxCompressSize?: number\n}", "ReportPlugin": "declare function ReportPlugin(_: ReportOptions, _: Logger, _: string, _: boolean, _: string, _: boolean): Plugin", "ResolvedConfig": "type ResolvedConfig = Overwrite, 'globalName' | 'inputOptions' | 'outputOptions' | 'minify' | 'define' | 'alias' | 'external' | 'onSuccess' | 'outExtensions' | 'hooks' | 'copy' | 'loader' | 'name' | 'banner' | 'footer' | 'checks'>, { entry: Record; nameLabel: string | undefined; format: NormalizedFormat; target?: string[]; clean: string[]; pkg?: PackageJsonWithPath; nodeProtocol: 'strip' | boolean; logger: Logger; ignoreWatch: Array; noExternal?: NoExternalFn; inlineOnly?: Array | false; css: Required; dts: false | DtsOptions; report: false | ReportOptions; tsconfig: false | string; exports: false | ExportsOptions; devtools: false | DevtoolsOptions; publint: false | PublintOptions; attw: false | AttwOptions; unused: false | UnusedOptions }>", diff --git a/package.json b/package.json index 4c650299c..b60b721de 100644 --- a/package.json +++ b/package.json @@ -108,6 +108,7 @@ }, "devDependencies": { "@arethetypeswrong/core": "catalog:peer", + "@publint/pack": "catalog:dev", "@sxzz/eslint-config": "catalog:dev", "@sxzz/prettier-config": "catalog:dev", "@sxzz/test-utils": "catalog:dev", @@ -126,6 +127,7 @@ "is-in-ci": "catalog:prod", "lightningcss": "catalog:dev", "memfs": "catalog:dev", + "package-manager-detector": "catalog:prod", "pkg-types": "catalog:dev", "prettier": "catalog:dev", "publint": "catalog:peer", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4473ff3b9..a08746299 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,6 +13,9 @@ catalogs: specifier: ^3.1.1 version: 3.1.1 dev: + '@publint/pack': + specifier: ^0.1.3 + version: 0.1.3 '@sxzz/eslint-config': specifier: ^7.5.1 version: 7.5.1 @@ -262,6 +265,9 @@ importers: '@arethetypeswrong/core': specifier: catalog:peer version: 0.18.2 + '@publint/pack': + specifier: catalog:dev + version: 0.1.3 '@sxzz/eslint-config': specifier: catalog:dev version: 7.5.1(@typescript-eslint/eslint-plugin@8.53.1(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@typescript-eslint/parser@8.53.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@unocss/eslint-plugin@66.6.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(prettier@3.8.1)(typescript@5.9.3) @@ -316,6 +322,9 @@ importers: memfs: specifier: catalog:dev version: 4.56.10(tslib@2.8.1) + package-manager-detector: + specifier: catalog:prod + version: 1.6.0 pkg-types: specifier: catalog:dev version: 2.3.0 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 173ad62e5..2ed964077 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -12,6 +12,7 @@ catalogs: '@clack/prompts': ^0.11.0 giget: ^3.1.1 dev: + '@publint/pack': ^0.1.3 '@sxzz/eslint-config': ^7.5.1 '@sxzz/prettier-config': ^2.2.6 '@sxzz/test-utils': ^0.5.15 diff --git a/src/features/pkg/attw.ts b/src/features/pkg/attw.ts index 514a4e2a7..8b7e72b04 100644 --- a/src/features/pkg/attw.ts +++ b/src/features/pkg/attw.ts @@ -1,10 +1,6 @@ -import { mkdtemp, readFile } from 'node:fs/promises' -import { tmpdir } from 'node:os' import path from 'node:path' import { dim } from 'ansis' import { createDebug } from 'obug' -import { exec } from 'tinyexec' -import { fsRemove } from '../../utils/fs.ts' import { importWithError, slash } from '../../utils/general.ts' import type { ResolvedConfig } from '../../config/index.ts' import type { @@ -96,7 +92,10 @@ const profiles: Record['profile'], string[]> = { 'esm-only': ['node10', 'node16-cjs'], } -export async function attw(options: ResolvedConfig): Promise { +export async function attw( + options: ResolvedConfig, + tarball: Uint8Array, +): Promise { if (!options.attw) return if (!options.pkg) { options.logger.warn('attw is enabled but package.json is not found') @@ -121,33 +120,17 @@ export async function attw(options: ResolvedConfig): Promise { const t = performance.now() debug('Running attw check') - const tempDir = await mkdtemp(path.join(tmpdir(), 'tsdown-attw-')) - const attwCore = await importWithError< typeof import('@arethetypeswrong/core') >('@arethetypeswrong/core', options.attw.resolvePaths) let checkResult: CheckResult try { - const { stdout: tarballInfo } = await exec( - 'npm', - ['pack', '--json', '--ignore-scripts', '--pack-destination', tempDir], - { nodeOptions: { cwd: options.cwd } }, - ) - const parsed = JSON.parse(tarballInfo) - if (!Array.isArray(parsed) || !parsed[0]?.filename) { - throw new Error('Invalid npm pack output format') - } - const tarballPath = path.join(tempDir, parsed[0].filename as string) - const tarball = await readFile(tarballPath) - const pkg = attwCore.createPackageFromTarballData(tarball) checkResult = await attwCore.checkPackage(pkg, attwOptions) } catch (error) { options.logger.error('ATTW check failed:', error) return - } finally { - await fsRemove(tempDir) } let errorMessage: string | undefined diff --git a/src/features/pkg/index.ts b/src/features/pkg/index.ts index 0df3c4d7f..b08e78fa9 100644 --- a/src/features/pkg/index.ts +++ b/src/features/pkg/index.ts @@ -1,10 +1,15 @@ +import { mkdtemp, readFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import path from 'node:path' import { formatWithOptions } from 'node:util' +import { fsRemove } from '../../utils/fs.ts' import { promiseWithResolvers } from '../../utils/general.ts' import { attw } from './attw.ts' import { writeExports } from './exports.ts' import { publint } from './publint.ts' import type { ResolvedConfig } from '../../config/types.ts' import type { ChunksByFormat, TsdownBundle } from '../../utils/chunks.ts' +import type { Buffer } from 'node:buffer' export type BundleByPkg = Record< string, // pkgPath @@ -91,14 +96,41 @@ export async function bundleDone( ) } - await Promise.all([ - ...publintConfigs.map((config) => publint(config)), - ...attwConfigs.map((config) => attw(config)), - ]) + if (publintConfigs.length || attwConfigs.length) { + const tarball = await packTarball(pkg.packageJsonPath) + await Promise.all([ + ...publintConfigs.map((config) => publint(config, tarball)), + ...attwConfigs.map((config) => attw(config, tarball)), + ]) + } ctx.resolve() } +async function packTarball( + packageJsonPath: string, +): Promise> { + const pkgDir = path.dirname(packageJsonPath) + const destination = await mkdtemp(path.join(tmpdir(), 'tsdown-pack-')) + const [{ detect }, { pack }] = await Promise.all([ + import('package-manager-detector/detect'), + import('@publint/pack'), + ]) + try { + const detected = await detect({ cwd: pkgDir }) + if (detected?.name === 'deno') { + throw new Error(`Cannot pack tarball for Deno projects at ${pkgDir}`) + } + const tarballPath = await pack(pkgDir, { + destination, + packageManager: detected?.name, + }) + return readFile(tarballPath) + } finally { + await fsRemove(destination) + } +} + function dedupeConfigs( configs: Array, key: K, diff --git a/src/features/pkg/publint.ts b/src/features/pkg/publint.ts index 497731f21..c90f3d850 100644 --- a/src/features/pkg/publint.ts +++ b/src/features/pkg/publint.ts @@ -1,19 +1,22 @@ -import path from 'node:path' import { dim } from 'ansis' import { createDebug } from 'obug' import { importWithError } from '../../utils/general.ts' import type { ResolvedConfig } from '../../config/index.ts' +import type { Buffer } from 'node:buffer' import type { Options } from 'publint' const debug = createDebug('tsdown:publint') const label = dim`[publint]` -export interface PublintOptions extends Options { +export interface PublintOptions extends Omit { /** @internal */ resolvePaths?: string[] } -export async function publint(options: ResolvedConfig): Promise { +export async function publint( + options: ResolvedConfig, + tarball: Buffer, +): Promise { if (!options.publint) return if (!options.pkg) { options.logger.warn( @@ -36,7 +39,7 @@ export async function publint(options: ResolvedConfig): Promise { const { messages } = await publint({ ...options.publint, - pkgDir: path.dirname(options.pkg.packageJsonPath), + pack: { tarball: tarball.buffer }, }) debug('Found %d issues', messages.length) diff --git a/tsdown.config.ts b/tsdown.config.ts index 9cdaed96a..da68360e9 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -11,6 +11,8 @@ export default defineConfig([ name: 'tsdown', inlineOnly: [ 'is-in-ci', + 'package-manager-detector', + '@publint/pack', 'pkg-types', // type-only ], platform: 'node', From 2a2b3842af0b725d8674c753f45153e58ee00f61 Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Fri, 30 Jan 2026 03:37:15 +0800 Subject: [PATCH 2/6] feat(pkg): optimize attw and publint packing with @publint/pack - Use @publint/pack instead of npm pack for consistent packing across package managers - Detect package manager with package-manager-detector and pass to pack - Pack tarball once when both publint and attw are enabled, share buffer between them - Simplify attw implementation by removing temporary npm pack logic - Update publint to accept pre-packed tarball instead of pkgDir Co-Authored-By: Claude Haiku 4.5 --- tsdown.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsdown.config.ts b/tsdown.config.ts index da68360e9..2005c1f9e 100644 --- a/tsdown.config.ts +++ b/tsdown.config.ts @@ -10,9 +10,9 @@ export default defineConfig([ entry: ['./src/{index,run,plugins,config}.ts'], name: 'tsdown', inlineOnly: [ + '@publint/pack', 'is-in-ci', 'package-manager-detector', - '@publint/pack', 'pkg-types', // type-only ], platform: 'node', From 5f4554be1936f427972d7f5db47d738451720868 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 19:39:32 +0000 Subject: [PATCH 3/6] [autofix.ci] apply automated fixes --- dts.snapshot.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dts.snapshot.json b/dts.snapshot.json index e8f300554..347f78b3c 100644 --- a/dts.snapshot.json +++ b/dts.snapshot.json @@ -1,5 +1,5 @@ { - "config-!~{00i}~.d.mts": { + "config-!~{00m}~.d.mts": { "defineConfig": "declare function defineConfig(_: UserConfigExport): UserConfigExport", "mergeConfig": "declare function mergeConfig(_: InlineConfig, _: InlineConfig): InlineConfig", "resolveUserConfig": "declare function resolveUserConfig(_: UserConfig, _: InlineConfig): Promise" @@ -83,7 +83,7 @@ "run.d.mts": { "#exports": [] }, - "types-!~{00g}~.d.mts": { + "types-!~{00k}~.d.mts": { "Arrayable": "type Arrayable = T | T[]", "AttwOptions": "interface AttwOptions extends CheckPackageOptions {\n profile?: 'strict' | 'node16' | 'esm-only'\n level?: 'error' | 'warn'\n ignoreRules?: string[]\n}", "Awaitable": "type Awaitable = T | Promise", From 2cdb4a07777ebac56338323fa9080fb8805b8317 Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Fri, 30 Jan 2026 03:43:20 +0800 Subject: [PATCH 4/6] fix(pkg): wrap pack+lint block in try/catch to prevent build hang If packTarball throws, ctx.resolve() must still run so other bundles awaiting ctx.promise don't hang indefinitely. --- src/features/pkg/index.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/features/pkg/index.ts b/src/features/pkg/index.ts index b08e78fa9..30d05714e 100644 --- a/src/features/pkg/index.ts +++ b/src/features/pkg/index.ts @@ -96,12 +96,16 @@ export async function bundleDone( ) } - if (publintConfigs.length || attwConfigs.length) { - const tarball = await packTarball(pkg.packageJsonPath) - await Promise.all([ - ...publintConfigs.map((config) => publint(config, tarball)), - ...attwConfigs.map((config) => attw(config, tarball)), - ]) + try { + if (publintConfigs.length || attwConfigs.length) { + const tarball = await packTarball(pkg.packageJsonPath) + await Promise.all([ + ...publintConfigs.map((config) => publint(config, tarball)), + ...attwConfigs.map((config) => attw(config, tarball)), + ]) + } + } catch (error) { + configs[0].logger.error('Pack failed:', error) } ctx.resolve() From 5f8f3c6737fe5293fbb31fa083acb3716c7495bf Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Fri, 30 Jan 2026 03:45:06 +0800 Subject: [PATCH 5/6] refactor(attw): remove redundant try/catch around checkPackage Error is now caught by the outer try/catch in bundleDone. --- src/features/pkg/attw.ts | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/features/pkg/attw.ts b/src/features/pkg/attw.ts index 8b7e72b04..2ecb4685b 100644 --- a/src/features/pkg/attw.ts +++ b/src/features/pkg/attw.ts @@ -5,7 +5,6 @@ import { importWithError, slash } from '../../utils/general.ts' import type { ResolvedConfig } from '../../config/index.ts' import type { CheckPackageOptions, - CheckResult, Problem, ProblemKind, } from '@arethetypeswrong/core' @@ -123,15 +122,9 @@ export async function attw( const attwCore = await importWithError< typeof import('@arethetypeswrong/core') >('@arethetypeswrong/core', options.attw.resolvePaths) - let checkResult: CheckResult - try { - const pkg = attwCore.createPackageFromTarballData(tarball) - checkResult = await attwCore.checkPackage(pkg, attwOptions) - } catch (error) { - options.logger.error('ATTW check failed:', error) - return - } + const pkg = attwCore.createPackageFromTarballData(tarball) + const checkResult = await attwCore.checkPackage(pkg, attwOptions) let errorMessage: string | undefined if (checkResult.types) { From 513fa68b15dae7a4b0bbbd0e5d69315122acbaf5 Mon Sep 17 00:00:00 2001 From: Kevin Deng Date: Fri, 30 Jan 2026 03:46:26 +0800 Subject: [PATCH 6/6] chore(attw): align tarball type with Buffer --- src/features/pkg/attw.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/features/pkg/attw.ts b/src/features/pkg/attw.ts index 2ecb4685b..080b666c0 100644 --- a/src/features/pkg/attw.ts +++ b/src/features/pkg/attw.ts @@ -8,6 +8,7 @@ import type { Problem, ProblemKind, } from '@arethetypeswrong/core' +import type { Buffer } from 'node:buffer' const debug = createDebug('tsdown:attw') const label = dim`[attw]` @@ -93,7 +94,7 @@ const profiles: Record['profile'], string[]> = { export async function attw( options: ResolvedConfig, - tarball: Uint8Array, + tarball: Buffer, ): Promise { if (!options.attw) return if (!options.pkg) {