Skip to content

Commit b2453d0

Browse files
feat(attw): add profile-based filtering and configurable error levels (#313)
Co-authored-by: Kevin Deng <sxzz@sxzz.moe>
1 parent d6faa91 commit b2453d0

File tree

2 files changed

+120
-9
lines changed

2 files changed

+120
-9
lines changed

src/features/attw.ts

Lines changed: 94 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,82 @@ import { blue, dim } from 'ansis'
88
import Debug from 'debug'
99
import { fsRemove } from '../utils/fs'
1010
import { logger } from '../utils/logger'
11-
import type { ResolvedOptions } from '../options'
11+
import type { AttwOptions, ResolvedOptions } from '../options'
12+
import type { Problem } from '@arethetypeswrong/core'
1213

1314
const debug = Debug('tsdown:attw')
1415
const exec = promisify(child_process.exec)
1516

17+
/**
18+
* ATTW profiles.
19+
* Defines the resolution modes to ignore for each profile.
20+
*
21+
* @see https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/packages/cli/README.md#profiles
22+
*/
23+
const profiles: Record<Required<AttwOptions>['profile'], string[]> = {
24+
strict: [],
25+
node16: ['node10'],
26+
esmOnly: ['node10', 'node16-cjs'],
27+
}
28+
29+
/**
30+
* Format an ATTW problem for display
31+
*/
32+
function formatProblem(problem: Problem): string {
33+
const resolutionKind =
34+
'resolutionKind' in problem ? ` (${problem.resolutionKind})` : ''
35+
const entrypoint = 'entrypoint' in problem ? ` at ${problem.entrypoint}` : ''
36+
37+
switch (problem.kind) {
38+
case 'NoResolution':
39+
return ` ❌ No resolution${resolutionKind}${entrypoint}`
40+
41+
case 'UntypedResolution':
42+
return ` ⚠️ Untyped resolution${resolutionKind}${entrypoint}`
43+
44+
case 'FalseESM':
45+
return ` 🔄 False ESM: Types indicate ESM (${problem.typesModuleKind}) but implementation is CJS (${problem.implementationModuleKind})\n Types: ${problem.typesFileName} | Implementation: ${problem.implementationFileName}`
46+
47+
case 'FalseCJS':
48+
return ` 🔄 False CJS: Types indicate CJS (${problem.typesModuleKind}) but implementation is ESM (${problem.implementationModuleKind})\n Types: ${problem.typesFileName} | Implementation: ${problem.implementationFileName}`
49+
50+
case 'CJSResolvesToESM':
51+
return ` ⚡ CJS resolves to ESM${resolutionKind}${entrypoint}`
52+
53+
case 'NamedExports': {
54+
const missingExports =
55+
problem.missing?.length > 0
56+
? ` Missing: ${problem.missing.join(', ')}`
57+
: ''
58+
const allMissing = problem.isMissingAllNamed
59+
? ' (all named exports missing)'
60+
: ''
61+
return ` 📤 Named exports problem${allMissing}${missingExports}\n Types: ${problem.typesFileName} | Implementation: ${problem.implementationFileName}`
62+
}
63+
64+
case 'FallbackCondition':
65+
return ` 🎯 Fallback condition used${resolutionKind}${entrypoint}`
66+
67+
case 'FalseExportDefault':
68+
return ` 🎭 False export default\n Types: ${problem.typesFileName} | Implementation: ${problem.implementationFileName}`
69+
70+
case 'MissingExportEquals':
71+
return ` 📝 Missing export equals\n Types: ${problem.typesFileName} | Implementation: ${problem.implementationFileName}`
72+
73+
case 'InternalResolutionError':
74+
return ` 💥 Internal resolution error in ${problem.fileName} (${problem.resolutionOption})\n Module: ${problem.moduleSpecifier} | Mode: ${problem.resolutionMode}`
75+
76+
case 'UnexpectedModuleSyntax':
77+
return ` 📋 Unexpected module syntax in ${problem.fileName}\n Expected: ${problem.moduleKind} | Found: ${problem.syntax === 99 ? 'ESM' : 'CJS'}`
78+
79+
case 'CJSOnlyExportsDefault':
80+
return ` 🏷️ CJS only exports default in ${problem.fileName}`
81+
82+
default:
83+
return ` ❓ Unknown problem: ${JSON.stringify(problem)}`
84+
}
85+
}
86+
1687
export async function attw(options: ResolvedOptions): Promise<void> {
1788
if (!options.attw) return
1889
if (!options.pkg) {
@@ -38,7 +109,7 @@ export async function attw(options: ResolvedOptions): Promise<void> {
38109
try {
39110
const { stdout: tarballInfo } = await exec(
40111
`npm pack --json ----pack-destination ${tempDir}`,
41-
{ encoding: 'utf-8' },
112+
{ encoding: 'utf-8', cwd: options.cwd },
42113
)
43114
const parsed = JSON.parse(tarballInfo)
44115
if (!Array.isArray(parsed) || !parsed[0]?.filename) {
@@ -48,14 +119,29 @@ export async function attw(options: ResolvedOptions): Promise<void> {
48119
const tarball = await readFile(tarballPath)
49120

50121
const pkg = attwCore.createPackageFromTarballData(tarball)
51-
const checkResult = await attwCore.checkPackage(
52-
pkg,
53-
options.attw === true ? {} : options.attw,
54-
)
122+
const attwOptions = options.attw === true ? {} : options.attw
123+
const checkResult = await attwCore.checkPackage(pkg, attwOptions)
124+
const profile = attwOptions.profile ?? 'strict'
125+
const level = attwOptions.level ?? 'warn'
55126

56127
if (checkResult.types !== false && checkResult.problems) {
57-
for (const problem of checkResult.problems) {
58-
logger.warn('Are the types wrong problem:', problem)
128+
const problems = checkResult.problems.filter((problem) => {
129+
// Only apply profile filter to problems that have resolutionKind
130+
if ('resolutionKind' in problem) {
131+
return !profiles[profile]?.includes(problem.resolutionKind)
132+
}
133+
// Include all other problem types
134+
return true
135+
})
136+
if (problems.length) {
137+
const problemList = problems.map(formatProblem).join('\n')
138+
const problemMessage = `Are the types wrong problems found:\n${problemList}`
139+
140+
if (level === 'error') {
141+
throw new Error(problemMessage)
142+
}
143+
144+
logger.warn(problemMessage)
59145
}
60146
} else {
61147
logger.success(

src/options/types.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import type {
99
MarkPartial,
1010
Overwrite,
1111
} from '../utils/types'
12-
import type { CheckPackageOptions as AttwOptions } from '@arethetypeswrong/core'
12+
import type { CheckPackageOptions } from '@arethetypeswrong/core'
1313
import type { Hookable } from 'hookable'
1414
import type { PackageJson } from 'pkg-types'
1515
import type { Options as PublintOptions } from 'publint'
@@ -87,6 +87,31 @@ export interface ExportsOptions {
8787
) => Awaitable<Record<string, any>>
8888
}
8989

90+
export interface AttwOptions extends CheckPackageOptions {
91+
/**
92+
* Profiles select a set of resolution modes to require/ignore. All are evaluated but failures outside
93+
* of those required are ignored.
94+
*
95+
* The available profiles are:
96+
* - `strict`: requires all resolutions
97+
* - `node16`: ignores node10 resolution failures
98+
* - `esmOnly`: ignores CJS resolution failures
99+
*
100+
* @default 'strict'
101+
*/
102+
profile?: 'strict' | 'node16' | 'esmOnly'
103+
/**
104+
* The level of the check.
105+
*
106+
* The available levels are:
107+
* - `error`: fails the build
108+
* - `warn`: warns the build
109+
*
110+
* @default 'warn'
111+
*/
112+
level?: 'error' | 'warn'
113+
}
114+
90115
/**
91116
* Options for tsdown.
92117
*/

0 commit comments

Comments
 (0)