diff --git a/.changeset/good-glasses-wait.md b/.changeset/good-glasses-wait.md new file mode 100644 index 0000000..2582e1c --- /dev/null +++ b/.changeset/good-glasses-wait.md @@ -0,0 +1,11 @@ +--- +"@arethetypeswrong/core": minor +--- + +Add support for DefinitelyTyped analysis. + +- `createPackageFromNpm` now takes an options parameter that can control DefinitelyTyped inclusion. +- The `Package` type is now a class, with new properties and methods: + - `typesPackage` contains the package name and version for the included DefinitelyTyped package, if any. + - `mergedWithTypes(typesPackage: Package)` returns a new `Package` instance with all files from both packages and the `typesPackage` instance property metadata filled in. +- `createPackageFromTarballData` is no longer asynchronous. diff --git a/.changeset/loud-hairs-relate.md b/.changeset/loud-hairs-relate.md new file mode 100644 index 0000000..b3c04fc --- /dev/null +++ b/.changeset/loud-hairs-relate.md @@ -0,0 +1,9 @@ +--- +"@arethetypeswrong/cli": minor +--- + +Add support for DefinitelyTyped analysis. + +- `@types` packages will be fetched by default for implementation packages that do not contain any TypeScript files. +- `--definitely-typed` can be used to set the version of the `@types` package fetched. By default, the version is inferred from the implementation package version. +- `--no-definitely-typed` can be used to prevent `@types` package inclusion. diff --git a/packages/cli/README.md b/packages/cli/README.md index 5e3a45c..38ec420 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -75,7 +75,7 @@ attw --pack . #### From NPM -Specify the name (and, optionally, version range) of a package from the NPM registry instead of a local tarball filename. +Specify the name (and, optionally, version or SemVer range) of a package from the NPM registry instead of a local tarball filename. In the CLI: `--from-npm`, `-p` @@ -85,6 +85,17 @@ attw --from-npm In the config file, `fromNpm` can be a boolean value. +#### DefinitelyTyped + +When a package does not contain types, specifies the version or SemVer range of the DefinitelyTyped `@types` package to use. Defaults to inferring the best version match from the implementation package version. + +In the CLI: `--definitely-typed`, `--no-definitely-typed` + +```shell +attw -p --definitely-typed +attw -p --no-definitely-typed +``` + #### Format The format to print the output in. Defaults to `auto`. diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index b2955d7..40c686e 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -25,6 +25,7 @@ type Format = (typeof formats)[number]; export interface Opts { pack?: boolean; fromNpm?: boolean; + definitelyTyped?: boolean | string; summary?: boolean; emoji?: boolean; color?: boolean; @@ -55,6 +56,8 @@ particularly ESM-related module resolution issues.` ) .option("-P, --pack", "Run `npm pack` in the specified directory and delete the resulting .tgz file afterwards") .option("-p, --from-npm", "Read from the npm registry instead of a local file") + .addOption(new Option("--definitely-typed [version]", "Specify the version range of @types to use").default(true)) + .option("--no-definitely-typed", "Don't include @types") .addOption(new Option("-f, --format ", "Specify the print format").choices(formats).default("auto")) .option("-q, --quiet", "Don't print anything to STDOUT (overrides all other options)") .option( @@ -92,6 +95,13 @@ particularly ESM-related module resolution issues.` let analysis: core.CheckResult; let deleteTgz; + const dtIsPath = + typeof opts.definitelyTyped === "string" && + (opts.definitelyTyped.includes("/") || + opts.definitelyTyped.includes("\\") || + opts.definitelyTyped.endsWith(".tgz") || + opts.definitelyTyped.endsWith(".tar.gz")); + if (opts.fromNpm) { if (opts.pack) { program.error("--pack and --from-npm cannot be used together"); @@ -101,14 +111,18 @@ particularly ESM-related module resolution issues.` if (result.status === "error") { program.error(result.error); } else { - analysis = await core.checkPackage( - await core.createPackageFromNpm(`${result.data.name}@${result.data.version}`), - { - entrypoints: opts.entrypoints, - includeEntrypoints: opts.includeEntrypoints, - excludeEntrypoints: opts.excludeEntrypoints, - } - ); + const pkg = dtIsPath + ? (await core.createPackageFromNpm(`${result.data.name}@${result.data.version}`)).mergedWithTypes( + core.createPackageFromTarballData(new Uint8Array(await readFile(opts.definitelyTyped as string))) + ) + : await core.createPackageFromNpm(`${result.data.name}@${result.data.version}`, { + definitelyTyped: opts.definitelyTyped, + }); + analysis = await core.checkPackage(pkg, { + entrypoints: opts.entrypoints, + includeEntrypoints: opts.includeEntrypoints, + excludeEntrypoints: opts.excludeEntrypoints, + }); } } catch (error) { if (error instanceof FetchError) { @@ -156,7 +170,14 @@ particularly ESM-related module resolution issues.` } const file = await readFile(fileName); const data = new Uint8Array(file); - analysis = await core.checkPackage(await core.createPackageFromTarballData(data), { + const pkg = dtIsPath + ? core + .createPackageFromTarballData(data) + .mergedWithTypes( + core.createPackageFromTarballData(new Uint8Array(await readFile(opts.definitelyTyped as string))) + ) + : core.createPackageFromTarballData(data); + analysis = await core.checkPackage(pkg, { entrypoints: opts.entrypoints, includeEntrypoints: opts.includeEntrypoints, excludeEntrypoints: opts.excludeEntrypoints, diff --git a/packages/cli/src/render/typed.ts b/packages/cli/src/render/typed.ts index be61df1..ed8877c 100644 --- a/packages/cli/src/render/typed.ts +++ b/packages/cli/src/render/typed.ts @@ -31,6 +31,12 @@ export async function typed(analysis: core.Analysis, opts: Opts) { ); } + console.log(`${analysis.packageName} v${analysis.packageVersion}`); + if (analysis.types.kind === "@types") { + console.log(`${analysis.types.packageName} v${analysis.types.packageVersion}`); + } + console.log(); + if (opts.summary) { const defaultSummary = marked(!opts.emoji ? " No problems found" : " No problems found ๐ŸŒŸ"); const summaryTexts = Object.keys(grouped).map((kind) => { diff --git a/packages/cli/test/snapshots.test.ts b/packages/cli/test/snapshots.test.ts index 51d8986..d0119c6 100644 --- a/packages/cli/test/snapshots.test.ts +++ b/packages/cli/test/snapshots.test.ts @@ -28,6 +28,15 @@ const tests = [ ["vue@3.3.4.tgz", "--entrypoints . jsx-runtime"], ["vue@3.3.4.tgz", "--exclude-entrypoints macros -f ascii"], ["vue@3.3.4.tgz", "--include-entrypoints ./foo -f ascii"], + + [ + "big.js@6.2.1.tgz", + `--definitely-typed ${new URL("../../../core/test/fixtures/@types__big.js@6.2.0.tgz", import.meta.url).pathname}`, + ], + [ + "react@18.2.0.tgz", + `--definitely-typed ${new URL("../../../core/test/fixtures/@types__react@18.2.21.tgz", import.meta.url).pathname}`, + ], ]; const defaultOpts = "-f table-flipped"; diff --git a/packages/cli/test/snapshots/@apollo__client-3.7.16.tgz.md b/packages/cli/test/snapshots/@apollo__client-3.7.16.tgz.md index 685cbdc..96effc4 100644 --- a/packages/cli/test/snapshots/@apollo__client-3.7.16.tgz.md +++ b/packages/cli/test/snapshots/@apollo__client-3.7.16.tgz.md @@ -4,6 +4,8 @@ $ attw @apollo__client-3.7.16.tgz -f table-flipped +@apollo/client v3.7.16 + ๐Ÿ‘บ Import resolved to an ESM type declaration file, but a CommonJS JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md โš ๏ธ A require call resolved to an ESM JavaScript file, which is an error in Node and some bundlers. CommonJS consumers will need to use a dynamic import. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/CJSResolvesToESM.md diff --git a/packages/cli/test/snapshots/@ice__app@3.2.6.tgz.md b/packages/cli/test/snapshots/@ice__app@3.2.6.tgz.md index b87b5a8..d75c6ad 100644 --- a/packages/cli/test/snapshots/@ice__app@3.2.6.tgz.md +++ b/packages/cli/test/snapshots/@ice__app@3.2.6.tgz.md @@ -4,6 +4,8 @@ $ attw @ice__app@3.2.6.tgz -f table-flipped +@ice/app v3.2.6 + โš ๏ธ A require call resolved to an ESM JavaScript file, which is an error in Node and some bundlers. CommonJS consumers will need to use a dynamic import. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/CJSResolvesToESM.md ๐Ÿ’€ Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md diff --git a/packages/cli/test/snapshots/@reduxjs__toolkit@2.0.0-beta.0.tgz.md b/packages/cli/test/snapshots/@reduxjs__toolkit@2.0.0-beta.0.tgz.md index 9eba234..eaa0119 100644 --- a/packages/cli/test/snapshots/@reduxjs__toolkit@2.0.0-beta.0.tgz.md +++ b/packages/cli/test/snapshots/@reduxjs__toolkit@2.0.0-beta.0.tgz.md @@ -4,6 +4,8 @@ $ attw @reduxjs__toolkit@2.0.0-beta.0.tgz -f table-flipped +@reduxjs/toolkit v2.0.0-beta.0 + ๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md ๐Ÿฅด Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md diff --git a/packages/cli/test/snapshots/@vitejs__plugin-react@3.1.0.tgz.md b/packages/cli/test/snapshots/@vitejs__plugin-react@3.1.0.tgz.md index cadfdf2..839db41 100644 --- a/packages/cli/test/snapshots/@vitejs__plugin-react@3.1.0.tgz.md +++ b/packages/cli/test/snapshots/@vitejs__plugin-react@3.1.0.tgz.md @@ -4,6 +4,8 @@ $ attw @vitejs__plugin-react@3.1.0.tgz -f table-flipped +@vitejs/plugin-react v3.1.0 + ๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md diff --git a/packages/cli/test/snapshots/ajv@8.12.0.tgz.md b/packages/cli/test/snapshots/ajv@8.12.0.tgz.md index 62c4ba8..900fa61 100644 --- a/packages/cli/test/snapshots/ajv@8.12.0.tgz.md +++ b/packages/cli/test/snapshots/ajv@8.12.0.tgz.md @@ -4,6 +4,8 @@ $ attw ajv@8.12.0.tgz -f table-flipped +ajv v8.12.0 + No problems found ๐ŸŒŸ diff --git a/packages/cli/test/snapshots/astring@1.8.6.tgz.md b/packages/cli/test/snapshots/astring@1.8.6.tgz.md index c4b5592..025d997 100644 --- a/packages/cli/test/snapshots/astring@1.8.6.tgz.md +++ b/packages/cli/test/snapshots/astring@1.8.6.tgz.md @@ -4,6 +4,8 @@ $ attw astring@1.8.6.tgz -f table-flipped +astring v1.8.6 + ๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md diff --git a/packages/cli/test/snapshots/axios@1.4.0.tgz.md b/packages/cli/test/snapshots/axios@1.4.0.tgz.md index a17b923..31435de 100644 --- a/packages/cli/test/snapshots/axios@1.4.0.tgz.md +++ b/packages/cli/test/snapshots/axios@1.4.0.tgz.md @@ -4,6 +4,8 @@ $ attw axios@1.4.0.tgz -f table-flipped +axios v1.4.0 + โ“ Wildcard subpaths cannot yet be analyzed by this tool. https://github.com/arethetypeswrong/arethetypeswrong.github.io/issues/40 ๐Ÿ’€ Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md diff --git a/packages/cli/test/snapshots/big.js@6.2.1.tgz --definitely-typed UsersandrewDeveloperarethetypeswrong.github.iopackagescoretestfixtures@types__big.js@6.2.0.tgz.md b/packages/cli/test/snapshots/big.js@6.2.1.tgz --definitely-typed UsersandrewDeveloperarethetypeswrong.github.iopackagescoretestfixtures@types__big.js@6.2.0.tgz.md new file mode 100644 index 0000000..eff546a --- /dev/null +++ b/packages/cli/test/snapshots/big.js@6.2.1.tgz --definitely-typed UsersandrewDeveloperarethetypeswrong.github.iopackagescoretestfixtures@types__big.js@6.2.0.tgz.md @@ -0,0 +1,33 @@ +# big.js@6.2.1.tgz --definitely-typed /Users/andrew/Developer/arethetypeswrong.github.io/packages/core/test/fixtures/@types__big.js@6.2.0.tgz + +``` +$ attw big.js@6.2.1.tgz --definitely-typed /Users/andrew/Developer/arethetypeswrong.github.io/packages/core/test/fixtures/@types__big.js@6.2.0.tgz + + +big.js v6.2.1 +@types/big.js v6.2.0 + +๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md + +โŒ Import resolved to JavaScript files, but no type declarations were found. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/UntypedResolution.md + +โš ๏ธ A require call resolved to an ESM JavaScript file, which is an error in Node and some bundlers. CommonJS consumers will need to use a dynamic import. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/CJSResolvesToESM.md + + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ”‚ "big.js" โ”‚ "big.js/big.mjs" โ”‚ "big.js/big.js" โ”‚ "big.js/package.json" โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ node10 โ”‚ ๐ŸŸข โ”‚ โŒ No types โ”‚ โŒ No types โ”‚ ๐ŸŸข (JSON) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ node16 (from CJS) โ”‚ ๐ŸŸข (CJS) โ”‚ โŒ No types โ”‚ โŒ No types โ”‚ ๐ŸŸข (JSON) โ”‚ +โ”‚ โ”‚ โ”‚ โš ๏ธ ESM (dynamic import only) โ”‚ โ”‚ โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ node16 (from ESM) โ”‚ ๐ŸŽญ Masquerading as CJS โ”‚ โŒ No types โ”‚ โŒ No types โ”‚ ๐ŸŸข (JSON) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ bundler โ”‚ ๐ŸŸข โ”‚ โŒ No types โ”‚ โŒ No types โ”‚ ๐ŸŸข (JSON) โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + +``` + +Exit code: 1 \ No newline at end of file diff --git a/packages/cli/test/snapshots/commander@10.0.1.tgz -f table.md b/packages/cli/test/snapshots/commander@10.0.1.tgz -f table.md index 2bcecd1..4a7cc93 100644 --- a/packages/cli/test/snapshots/commander@10.0.1.tgz -f table.md +++ b/packages/cli/test/snapshots/commander@10.0.1.tgz -f table.md @@ -4,6 +4,8 @@ $ attw commander@10.0.1.tgz -f table +commander v10.0.1 + ๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md โŒ Import resolved to JavaScript files, but no type declarations were found. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/UntypedResolution.md diff --git a/packages/cli/test/snapshots/hexoid@1.0.0.tgz.md b/packages/cli/test/snapshots/hexoid@1.0.0.tgz.md index 81c17ca..0a9545f 100644 --- a/packages/cli/test/snapshots/hexoid@1.0.0.tgz.md +++ b/packages/cli/test/snapshots/hexoid@1.0.0.tgz.md @@ -4,6 +4,8 @@ $ attw hexoid@1.0.0.tgz -f table-flipped +hexoid v1.0.0 + โ—๏ธ The resolved types use export default where the JavaScript file appears to use module.exports =. This will cause TypeScript under the node16 module mode to think an extra .default property access is required, but that will likely fail at runtime. These types should use export = instead of export default. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseExportDefault.md diff --git a/packages/cli/test/snapshots/klona@2.0.6.tgz -f ascii.md b/packages/cli/test/snapshots/klona@2.0.6.tgz -f ascii.md index a247192..8f1da2a 100644 --- a/packages/cli/test/snapshots/klona@2.0.6.tgz -f ascii.md +++ b/packages/cli/test/snapshots/klona@2.0.6.tgz -f ascii.md @@ -4,6 +4,8 @@ $ attw klona@2.0.6.tgz -f ascii +klona v2.0.6 + ๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md diff --git a/packages/cli/test/snapshots/node-html-parser@6.1.5.tgz.md b/packages/cli/test/snapshots/node-html-parser@6.1.5.tgz.md index 00534df..98b91c5 100644 --- a/packages/cli/test/snapshots/node-html-parser@6.1.5.tgz.md +++ b/packages/cli/test/snapshots/node-html-parser@6.1.5.tgz.md @@ -4,6 +4,8 @@ $ attw node-html-parser@6.1.5.tgz -f table-flipped +node-html-parser v6.1.5 + ๐Ÿคจ CommonJS module simulates a default export with exports.default and exports.__esModule, but does not also set module.exports for compatibility with Node. Node, and some bundlers under certain conditions (https://andrewbranch.github.io/interop-test/#synthesizing-default-exports-for-cjs-modules), do not respect the __esModule marker, so accessing the intended default export will require a .default property access on the default import. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/CJSOnlyExportsDefault.md diff --git a/packages/cli/test/snapshots/postcss@8.4.21.tgz.md b/packages/cli/test/snapshots/postcss@8.4.21.tgz.md index 221bd21..de320ed 100644 --- a/packages/cli/test/snapshots/postcss@8.4.21.tgz.md +++ b/packages/cli/test/snapshots/postcss@8.4.21.tgz.md @@ -4,6 +4,8 @@ $ attw postcss@8.4.21.tgz -f table-flipped +postcss v8.4.21 + ๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md ๐Ÿ› Import resolved to types through a conditional package.json export, but only after failing to resolve through an earlier condition. This behavior is a TypeScript bug (https://github.com/microsoft/TypeScript/issues/50762). It may misrepresent the runtime behavior of this import and should not be relied upon. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FallbackCondition.md diff --git a/packages/cli/test/snapshots/react-chartjs-2@5.2.0.tgz.md b/packages/cli/test/snapshots/react-chartjs-2@5.2.0.tgz.md index 6be800c..740c069 100644 --- a/packages/cli/test/snapshots/react-chartjs-2@5.2.0.tgz.md +++ b/packages/cli/test/snapshots/react-chartjs-2@5.2.0.tgz.md @@ -4,6 +4,8 @@ $ attw react-chartjs-2@5.2.0.tgz -f table-flipped +react-chartjs-2 v5.2.0 + ๐Ÿ‘บ Import resolved to an ESM type declaration file, but a CommonJS JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseESM.md โš ๏ธ A require call resolved to an ESM JavaScript file, which is an error in Node and some bundlers. CommonJS consumers will need to use a dynamic import. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/CJSResolvesToESM.md diff --git a/packages/cli/test/snapshots/react@18.2.0.tgz --definitely-typed UsersandrewDeveloperarethetypeswrong.github.iopackagescoretestfixtures@types__react@18.2.21.tgz.md b/packages/cli/test/snapshots/react@18.2.0.tgz --definitely-typed UsersandrewDeveloperarethetypeswrong.github.iopackagescoretestfixtures@types__react@18.2.21.tgz.md new file mode 100644 index 0000000..96dd4cc --- /dev/null +++ b/packages/cli/test/snapshots/react@18.2.0.tgz --definitely-typed UsersandrewDeveloperarethetypeswrong.github.iopackagescoretestfixtures@types__react@18.2.21.tgz.md @@ -0,0 +1,32 @@ +# react@18.2.0.tgz --definitely-typed /Users/andrew/Developer/arethetypeswrong.github.io/packages/core/test/fixtures/@types__react@18.2.21.tgz + +``` +$ attw react@18.2.0.tgz --definitely-typed /Users/andrew/Developer/arethetypeswrong.github.io/packages/core/test/fixtures/@types__react@18.2.21.tgz + + +react v18.2.0 +@types/react v18.2.21 + + No problems found ๐ŸŒŸ + + +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ โ”‚ node10 โ”‚ node16 (from CJS) โ”‚ node16 (from ESM) โ”‚ bundler โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ "react" โ”‚ ๐ŸŸข โ”‚ ๐ŸŸข (CJS) โ”‚ ๐ŸŸข (CJS) โ”‚ ๐ŸŸข โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ "react/package.json" โ”‚ ๐ŸŸข (JSON) โ”‚ ๐ŸŸข (JSON) โ”‚ ๐ŸŸข (JSON) โ”‚ ๐ŸŸข (JSON) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ "react/jsx-runtime" โ”‚ ๐ŸŸข โ”‚ ๐ŸŸข (CJS) โ”‚ ๐ŸŸข (CJS) โ”‚ ๐ŸŸข โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ "react/jsx-dev-runtime" โ”‚ ๐ŸŸข โ”‚ ๐ŸŸข (CJS) โ”‚ ๐ŸŸข (CJS) โ”‚ ๐ŸŸข โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ "react/canary" โ”‚ ๐ŸŸข โ”‚ ๐ŸŸข (CJS) โ”‚ ๐ŸŸข (CJS) โ”‚ ๐ŸŸข โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ "react/experimental" โ”‚ ๐ŸŸข โ”‚ ๐ŸŸข (CJS) โ”‚ ๐ŸŸข (CJS) โ”‚ ๐ŸŸข โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + + +``` + +Exit code: 0 \ No newline at end of file diff --git a/packages/cli/test/snapshots/rfdc@1.3.0.tgz.md b/packages/cli/test/snapshots/rfdc@1.3.0.tgz.md index 44b9a37..cb319af 100644 --- a/packages/cli/test/snapshots/rfdc@1.3.0.tgz.md +++ b/packages/cli/test/snapshots/rfdc@1.3.0.tgz.md @@ -4,6 +4,8 @@ $ attw rfdc@1.3.0.tgz -f table-flipped +rfdc v1.3.0 + โŒ Import resolved to JavaScript files, but no type declarations were found. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/UntypedResolution.md diff --git a/packages/cli/test/snapshots/vue@3.3.4.tgz --entrypoints . jsx-runtime.md b/packages/cli/test/snapshots/vue@3.3.4.tgz --entrypoints . jsx-runtime.md index 482aa2f..d763b4b 100644 --- a/packages/cli/test/snapshots/vue@3.3.4.tgz --entrypoints . jsx-runtime.md +++ b/packages/cli/test/snapshots/vue@3.3.4.tgz --entrypoints . jsx-runtime.md @@ -4,6 +4,8 @@ $ attw vue@3.3.4.tgz --entrypoints . jsx-runtime +vue v3.3.4 + ๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md ๐Ÿฅด Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md diff --git a/packages/cli/test/snapshots/vue@3.3.4.tgz --entrypoints vue.md b/packages/cli/test/snapshots/vue@3.3.4.tgz --entrypoints vue.md index fce301d..0179660 100644 --- a/packages/cli/test/snapshots/vue@3.3.4.tgz --entrypoints vue.md +++ b/packages/cli/test/snapshots/vue@3.3.4.tgz --entrypoints vue.md @@ -4,6 +4,8 @@ $ attw vue@3.3.4.tgz --entrypoints vue +vue v3.3.4 + ๐Ÿฅด Import found in a type declaration file failed to resolve. Either this indicates that runtime resolution errors will occur, or (more likely) the types misrepresent the contents of the JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/InternalResolutionError.md diff --git a/packages/cli/test/snapshots/vue@3.3.4.tgz --exclude-entrypoints macros -f ascii.md b/packages/cli/test/snapshots/vue@3.3.4.tgz --exclude-entrypoints macros -f ascii.md index 473e225..65f8b6e 100644 --- a/packages/cli/test/snapshots/vue@3.3.4.tgz --exclude-entrypoints macros -f ascii.md +++ b/packages/cli/test/snapshots/vue@3.3.4.tgz --exclude-entrypoints macros -f ascii.md @@ -4,6 +4,8 @@ $ attw vue@3.3.4.tgz --exclude-entrypoints macros -f ascii +vue v3.3.4 + ๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md ๐Ÿ’€ Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md diff --git a/packages/cli/test/snapshots/vue@3.3.4.tgz --include-entrypoints .foo -f ascii.md b/packages/cli/test/snapshots/vue@3.3.4.tgz --include-entrypoints .foo -f ascii.md index 8333bb1..926827b 100644 --- a/packages/cli/test/snapshots/vue@3.3.4.tgz --include-entrypoints .foo -f ascii.md +++ b/packages/cli/test/snapshots/vue@3.3.4.tgz --include-entrypoints .foo -f ascii.md @@ -4,6 +4,8 @@ $ attw vue@3.3.4.tgz --include-entrypoints ./foo -f ascii +vue v3.3.4 + ๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md ๐Ÿ’€ Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md diff --git a/packages/cli/test/snapshots/vue@3.3.4.tgz.md b/packages/cli/test/snapshots/vue@3.3.4.tgz.md index 549caf9..9974d36 100644 --- a/packages/cli/test/snapshots/vue@3.3.4.tgz.md +++ b/packages/cli/test/snapshots/vue@3.3.4.tgz.md @@ -4,6 +4,8 @@ $ attw vue@3.3.4.tgz -f table-flipped +vue v3.3.4 + ๐ŸŽญ Import resolved to a CommonJS type declaration file, but an ESM JavaScript file. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/FalseCJS.md ๐Ÿ’€ Import failed to resolve to type declarations or JavaScript files. https://github.com/arethetypeswrong/arethetypeswrong.github.io/blob/main/docs/problems/NoResolution.md diff --git a/packages/core/src/checkPackage.ts b/packages/core/src/checkPackage.ts index 1b3badb..ed5d5a9 100644 --- a/packages/core/src/checkPackage.ts +++ b/packages/core/src/checkPackage.ts @@ -4,7 +4,14 @@ import { getFileProblems } from "./checks/fileProblems.js"; import { getResolutionBasedFileProblems } from "./checks/resolutionBasedFileProblems.js"; import type { Package } from "./createPackage.js"; import { createCompilerHosts, type CompilerHosts, CompilerHostWrapper } from "./multiCompilerHost.js"; -import type { CheckResult, EntrypointInfo, EntrypointResolutionAnalysis, Resolution, ResolutionKind } from "./types.js"; +import type { + AnalysisTypes, + CheckResult, + EntrypointInfo, + EntrypointResolutionAnalysis, + Resolution, + ResolutionKind, +} from "./types.js"; export interface CheckPackageOptions { /** @@ -24,15 +31,17 @@ export interface CheckPackageOptions { } export async function checkPackage(pkg: Package, options?: CheckPackageOptions): Promise { - const files = pkg.listFiles(); - const types = files.some(ts.hasTSFileExtension) ? "included" : false; - const parts = files[0].split("/"); - let packageName = parts[2]; - if (packageName.startsWith("@")) { - packageName = parts.slice(2, 4).join("/"); - } - const packageJsonContent = JSON.parse(pkg.readFile(`/node_modules/${packageName}/package.json`)); - const packageVersion = packageJsonContent.version; + const types: AnalysisTypes | false = pkg.typesPackage + ? { + kind: "@types", + ...pkg.typesPackage, + definitelyTypedUrl: JSON.parse(pkg.readFile(`/node_modules/${pkg.typesPackage.packageName}/package.json`)) + .homepage, + } + : pkg.containsTypes() + ? { kind: "included" } + : false; + const { packageName, packageVersion } = pkg; if (!types) { return { packageName, packageVersion, types }; } @@ -131,7 +140,12 @@ function getEntrypointInfo( options: CheckPackageOptions | undefined ): Record { const packageJson = JSON.parse(fs.readFile(`/node_modules/${packageName}/package.json`)); - const entrypoints = getEntrypoints(fs, packageJson.exports, options); + let entrypoints = getEntrypoints(fs, packageJson.exports, options); + if (fs.typesPackage) { + const typesPackageJson = JSON.parse(fs.readFile(`/node_modules/${fs.typesPackage.packageName}/package.json`)); + const typesEntrypoints = getEntrypoints(fs, typesPackageJson.exports, options); + entrypoints = unique([...entrypoints, ...typesEntrypoints]); + } const result: Record = {}; for (const entrypoint of entrypoints) { const resolutions: Record = { diff --git a/packages/core/src/createPackage.ts b/packages/core/src/createPackage.ts index 88adb7f..03d7c11 100644 --- a/packages/core/src/createPackage.ts +++ b/packages/core/src/createPackage.ts @@ -1,50 +1,197 @@ import { untar } from "@andrewbranch/untar.js"; import { gunzipSync } from "fflate"; import ts from "typescript"; -import { parsePackageSpec } from "./utils.js"; -import { maxSatisfying } from "semver"; - -export interface Package { - packageName: string; - packageVersion: string; - readFile: (path: string) => string; - fileExists: (path: string) => boolean; - directoryExists: (path: string) => boolean; - listFiles: () => string[]; +import { parsePackageSpec, type ParsedPackageSpec } from "./utils.js"; +import { maxSatisfying, major, minor, valid, validRange } from "semver"; + +export class Package { + #files: Record = {}; + readonly packageName: string; + readonly packageVersion: string; + readonly typesPackage?: { + packageName: string; + packageVersion: string; + }; + + constructor( + files: Record, + packageName: string, + packageVersion: string, + typesPackage?: { + packageName: string; + packageVersion: string; + } + ) { + this.#files = files; + this.packageName = packageName; + this.packageVersion = packageVersion; + this.typesPackage = typesPackage; + } + + readFile(path: string): string { + const file = this.#files[path]; + if (!file) { + throw new Error(`File not found: ${path}`); + } + if (typeof file === "string") { + return file; + } + const content = new TextDecoder().decode(file); + this.#files[path] = content; + return content; + } + + fileExists(path: string): boolean { + return path in this.#files; + } + + directoryExists(path: string): boolean { + path = ts.ensureTrailingDirectorySeparator(path); + for (const file in this.#files) { + if (file.startsWith(path)) { + return true; + } + } + return false; + } + + containsTypes(directory = "/"): boolean { + return this.listFiles(directory).some(ts.hasTSFileExtension); + } + + listFiles(directory = "/"): string[] { + directory = ts.ensureTrailingDirectorySeparator(directory); + return directory === "/" + ? Object.keys(this.#files) + : Object.keys(this.#files).filter((f) => f.startsWith(directory)); + } + + mergedWithTypes(typesPackage: Package): Package { + const files = { ...this.#files, ...typesPackage.#files }; + return new Package(files, this.packageName, this.packageVersion, { + packageName: typesPackage.packageName, + packageVersion: typesPackage.packageVersion, + }); + } +} + +export interface CreatePackageFromNpmOptions { + /** + * Controls inclusion of a corresponding `@types` package. Ignored if the implementation + * package contains TypeScript files. The value is the version or SemVer range of the + * `@types` package to include, `true` to infer the version from the implementation + * package version, or `false` to prevent inclusion of a `@types` package. + * @default true + */ + definitelyTyped?: string | boolean; } -export async function createPackageFromNpm(packageSpec: string): Promise { +export async function createPackageFromNpm( + packageSpec: string, + { definitelyTyped = true }: CreatePackageFromNpmOptions = {} +): Promise { const parsed = parsePackageSpec(packageSpec); if (parsed.status === "error") { throw new Error(parsed.error); } const packageName = parsed.data.name; - const packageVersion = parsed.data.version || "latest"; + const spec = + parsed.data.versionKind === "none" && typeof definitelyTyped === "string" + ? parsePackageSpec(`${packageName}@${definitelyTyped}`) + : parsed; + const { tarballUrl, version } = await getNpmTarballUrl(spec.data || parsed.data); + const pkg = await createPackageFromTarballUrl(tarballUrl); + if (!definitelyTyped || pkg.containsTypes()) { + return pkg; + } + + const typesPackageName = ts.getTypesPackageName(packageName); + let typesPackageData; + if (definitelyTyped === true) { + try { + typesPackageData = await getNpmTarballUrl({ + name: typesPackageName, + versionKind: "range", + version: `${major(version)}.${minor(version)}`, + }); + } catch { + try { + typesPackageData = await getNpmTarballUrl({ + name: typesPackageName, + versionKind: "range", + version: `${major(version)}`, + }); + } catch { + try { + typesPackageData = await getNpmTarballUrl({ + name: typesPackageName, + versionKind: "tag", + version: "latest", + }); + } catch { + typesPackageData = undefined; + } + } + } + } else { + typesPackageData = await getNpmTarballUrl({ + name: typesPackageName, + versionKind: valid(definitelyTyped) ? "exact" : validRange(definitelyTyped) ? "range" : "tag", + version: definitelyTyped, + }); + } + + if (typesPackageData) { + return pkg.mergedWithTypes(await createPackageFromTarballUrl(typesPackageData.tarballUrl)); + } + return pkg; +} + +async function getNpmTarballUrl(packageSpec: ParsedPackageSpec): Promise<{ tarballUrl: string; version: string }> { const registryUrl = - parsed.data.versionKind === "range" - ? `https://registry.npmjs.org/${packageName}` - : `https://registry.npmjs.org/${packageName}/${packageVersion}`; - const Accept = parsed.data.versionKind === "range" ? "application/vnd.npm.install-v1+json" : "application/json"; + packageSpec.versionKind === "range" || (packageSpec.versionKind === "tag" && packageSpec.version !== "latest") + ? `https://registry.npmjs.org/${packageSpec.name}` + : `https://registry.npmjs.org/${packageSpec.name}/${packageSpec.version || "latest"}`; + const Accept = + packageSpec.versionKind === "range" || (packageSpec.versionKind === "tag" && packageSpec.version !== "latest") + ? "application/vnd.npm.install-v1+json" + : "application/json"; const doc = await fetch(registryUrl, { headers: { Accept } }).then((r) => r.json()); - let tarballUrl; - if (parsed.data.versionKind === "range") { - const version = maxSatisfying(Object.keys(doc.versions), parsed.data.version); + let tarballUrl, version; + if (packageSpec.versionKind === "range") { + version = maxSatisfying(Object.keys(doc.versions), packageSpec.version); if (!version) { - throw new Error(`No version found matching '${packageVersion}'`); + throw new Error(`No version found matching '${packageSpec.version}'`); + } + tarballUrl = doc.versions[version].dist.tarball; + } else if (packageSpec.versionKind === "tag" && packageSpec.version !== "latest") { + version = doc["dist-tags"][packageSpec.version]; + if (!version) { + throw new Error(`No version found matching '${packageSpec.version}'`); } tarballUrl = doc.versions[version].dist.tarball; } else { + version = doc.version; tarballUrl = doc.dist.tarball; } - return createPackageFromTarballUrl(tarballUrl); + return { version, tarballUrl }; } export async function createPackageFromTarballUrl(tarballUrl: string): Promise { - const tarball = new Uint8Array((await fetch(tarballUrl).then((r) => r.arrayBuffer())) satisfies ArrayBuffer); + const tarball = await fetchTarball(tarballUrl); return createPackageFromTarballData(tarball); } -export async function createPackageFromTarballData(tarball: Uint8Array): Promise { +async function fetchTarball(tarballUrl: string) { + return new Uint8Array((await fetch(tarballUrl).then((r) => r.arrayBuffer())) satisfies ArrayBuffer); +} + +export function createPackageFromTarballData(tarball: Uint8Array): Package { + const { files, packageName, packageVersion } = extractTarball(tarball); + return new Package(files, packageName, packageVersion); +} + +function extractTarball(tarball: Uint8Array) { const data = untar(gunzipSync(tarball)); const prefix = data[0].filename.substring(0, data[0].filename.indexOf("/") + 1); const packageJsonText = data.find((f) => f.filename === `${prefix}package.json`)?.fileData; @@ -55,27 +202,5 @@ export async function createPackageFromTarballData(tarball: Uint8Array): Promise acc[ts.combinePaths("/node_modules/" + packageName, file.filename.substring(prefix.length))] = file.fileData; return acc; }, {}); - - return { - packageName, - packageVersion, - readFile: (path: string) => { - const file = files[path]; - if (!file) { - throw new Error(`File not found: ${path}`); - } - return new TextDecoder().decode(file); - }, - fileExists: (path: string) => path in files, - directoryExists: (path: string) => { - path = ts.ensureTrailingDirectorySeparator(path); - for (const file in files) { - if (file.startsWith(path)) { - return true; - } - } - return false; - }, - listFiles: () => Object.keys(files), - }; + return { files, packageName, packageVersion }; } diff --git a/packages/core/src/multiCompilerHost.ts b/packages/core/src/multiCompilerHost.ts index fc33d2f..6e1a0b2 100644 --- a/packages/core/src/multiCompilerHost.ts +++ b/packages/core/src/multiCompilerHost.ts @@ -128,7 +128,9 @@ export class CompilerHostWrapper { private createCompilerHost(fs: Package): ts.CompilerHost { const sourceFileCache = new Map(); return { - ...fs, + fileExists: fs.fileExists.bind(fs), + readFile: fs.readFile.bind(fs), + directoryExists: fs.directoryExists.bind(fs), getSourceFile: (fileName) => { const path = toPath(fileName); const cached = sourceFileCache.get(path); diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 4b777c9..7542536 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -9,10 +9,21 @@ export interface EntrypointInfo { isWildcard: boolean; } +export interface IncludedTypes { + kind: "included"; +} +export interface TypesPackage { + kind: "@types"; + packageName: string; + packageVersion: string; + definitelyTypedUrl?: string; +} +export type AnalysisTypes = IncludedTypes | TypesPackage; + export interface Analysis { packageName: string; packageVersion: string; - types: "included"; + types: AnalysisTypes; entrypoints: Record; problems: Problem[]; } @@ -98,10 +109,10 @@ export type ProblemKind = Problem["kind"]; export type FileProblemKind = FileProblem["kind"]; export type ResolutionBasedFileProblemKind = ResolutionBasedFileProblem["kind"]; -export type Failable = { status: "error"; error: string } | { status: "success"; data: T }; +export type Failable = { status: "error"; error: string; data?: never } | { status: "success"; data: T }; export interface ParsedPackageSpec { name: string; - versionKind: "none" | "exact" | "range"; + versionKind: "none" | "exact" | "range" | "tag"; version: string; } diff --git a/packages/core/src/utils.ts b/packages/core/src/utils.ts index 051059f..5485dca 100644 --- a/packages/core/src/utils.ts +++ b/packages/core/src/utils.ts @@ -160,12 +160,6 @@ export function parsePackageSpec(input: string): Failable { error: "Invalid package name", }; } - if (input.substring(0, i) === "@types") { - return { - status: "error", - error: "@types packages are not supported", - }; - } i++; } i = input.indexOf("@", i); @@ -201,7 +195,7 @@ export function parsePackageSpec(input: string): Failable { }; } return { - status: "error", - error: "Invalid version", + status: "success", + data: { versionKind: "tag", name, version }, }; } diff --git a/packages/core/test/fixtures/@types__big.js@6.2.0.tgz b/packages/core/test/fixtures/@types__big.js@6.2.0.tgz new file mode 100644 index 0000000..1346ca6 Binary files /dev/null and b/packages/core/test/fixtures/@types__big.js@6.2.0.tgz differ diff --git a/packages/core/test/fixtures/@types__react@18.2.21.tgz b/packages/core/test/fixtures/@types__react@18.2.21.tgz new file mode 100644 index 0000000..256624d Binary files /dev/null and b/packages/core/test/fixtures/@types__react@18.2.21.tgz differ diff --git a/packages/core/test/fixtures/big.js@6.2.1.tgz b/packages/core/test/fixtures/big.js@6.2.1.tgz new file mode 100644 index 0000000..2debf14 Binary files /dev/null and b/packages/core/test/fixtures/big.js@6.2.1.tgz differ diff --git a/packages/core/test/fixtures/react@18.2.0.tgz b/packages/core/test/fixtures/react@18.2.0.tgz new file mode 100644 index 0000000..6cb2cb9 Binary files /dev/null and b/packages/core/test/fixtures/react@18.2.0.tgz differ diff --git a/packages/core/test/snapshots.test.ts b/packages/core/test/snapshots.test.ts index 665b222..d9eaa96 100644 --- a/packages/core/test/snapshots.test.ts +++ b/packages/core/test/snapshots.test.ts @@ -21,13 +21,24 @@ describe("snapshots", async () => { } }); + const typesPackages: Record = { + "big.js@6.2.1.tgz": "@types__big.js@6.2.0.tgz", + "react@18.2.0.tgz": "@types__react@18.2.21.tgz", + }; + for (const fixture of fs.readdirSync(new URL("../fixtures", import.meta.url))) { - if (fixture === ".DS_Store") { + if (fixture === ".DS_Store" || fixture.startsWith("@types__")) { continue; } test(fixture, async () => { const tarball = await readFile(new URL(`../fixtures/${fixture}`, import.meta.url)); - const analysis = await checkPackage(await createPackageFromTarballData(tarball)); + const typesTarball = typesPackages[fixture] + ? await readFile(new URL(`../fixtures/${typesPackages[fixture]}`, import.meta.url)) + : undefined; + const pkg = createPackageFromTarballData(tarball); + const analysis = await checkPackage( + typesTarball ? pkg.mergedWithTypes(createPackageFromTarballData(typesTarball)) : pkg + ); const snapshotURL = new URL(`../snapshots/${fixture}.md`, import.meta.url); const expectedSnapshot = [ `# ${fixture}`, diff --git a/packages/core/test/snapshots/big.js@6.2.1.tgz.md b/packages/core/test/snapshots/big.js@6.2.1.tgz.md new file mode 100644 index 0000000..ac48955 --- /dev/null +++ b/packages/core/test/snapshots/big.js@6.2.1.tgz.md @@ -0,0 +1,58 @@ +# big.js@6.2.1.tgz + +## Problems + +```json +[ + { + "kind": "FalseCJS", + "entrypoint": ".", + "resolutionKind": "node16-esm" + }, + { + "kind": "UntypedResolution", + "entrypoint": "./big.mjs", + "resolutionKind": "node10" + }, + { + "kind": "UntypedResolution", + "entrypoint": "./big.mjs", + "resolutionKind": "node16-cjs" + }, + { + "kind": "CJSResolvesToESM", + "entrypoint": "./big.mjs", + "resolutionKind": "node16-cjs" + }, + { + "kind": "UntypedResolution", + "entrypoint": "./big.mjs", + "resolutionKind": "node16-esm" + }, + { + "kind": "UntypedResolution", + "entrypoint": "./big.mjs", + "resolutionKind": "bundler" + }, + { + "kind": "UntypedResolution", + "entrypoint": "./big.js", + "resolutionKind": "node10" + }, + { + "kind": "UntypedResolution", + "entrypoint": "./big.js", + "resolutionKind": "node16-cjs" + }, + { + "kind": "UntypedResolution", + "entrypoint": "./big.js", + "resolutionKind": "node16-esm" + }, + { + "kind": "UntypedResolution", + "entrypoint": "./big.js", + "resolutionKind": "bundler" + } +] +``` \ No newline at end of file diff --git a/packages/core/test/snapshots/react@18.2.0.tgz.md b/packages/core/test/snapshots/react@18.2.0.tgz.md new file mode 100644 index 0000000..1b51016 --- /dev/null +++ b/packages/core/test/snapshots/react@18.2.0.tgz.md @@ -0,0 +1,7 @@ +# react@18.2.0.tgz + +## Problems + +```json +[] +``` \ No newline at end of file diff --git a/packages/web/src/index.html b/packages/web/src/index.html index 4e166c0..1041a47 100644 --- a/packages/web/src/index.html +++ b/packages/web/src/index.html @@ -43,7 +43,7 @@

Are the types wrong?

- +

diff --git a/packages/web/src/renderer.ts b/packages/web/src/renderer.ts index c26be33..8705a15 100644 --- a/packages/web/src/renderer.ts +++ b/packages/web/src/renderer.ts @@ -59,10 +59,7 @@ export function subscribeRenderer(events: Events) { function render(prevState: State) { const state = getState(); updateView(messageElement, Message, { isError: state.message?.isError, text: state.message?.text || "" }); - updateView(packageInfoElement, PackageInfo, { - name: state.analysis?.packageName, - version: state.analysis?.packageVersion, - }); + updateView(packageInfoElement, PackageInfo, { analysis: state.analysis?.types ? state.analysis : undefined }); updateView(problemsElement, ProblemList, { analysis: state.analysis }); updateView(resolutionsElement, ChecksTable, { analysis: state.analysis }); updateView(checkButton, CheckButton, { disabled: !state.packageInfo.parsed }); diff --git a/packages/web/src/views/PackageInfo.ts b/packages/web/src/views/PackageInfo.ts index d46bf2d..4de60d5 100644 --- a/packages/web/src/views/PackageInfo.ts +++ b/packages/web/src/views/PackageInfo.ts @@ -1,14 +1,32 @@ -export function PackageInfo(props: { name?: string; version?: string }) { - if (!props.name || !props.version) { +import type { Analysis } from "@arethetypeswrong/core/types"; + +export function PackageInfo({ analysis }: { analysis?: Analysis }) { + if (!analysis) { return { className: "display-none" }; } return { className: "", innerHTML: ` - ${props.name} v${props.version} - - (npm, - unpkg) - `, +

+ ${analysis.packageName} v${analysis.packageVersion} + + (npm, + unpkg) + +

+ ${ + analysis.types.kind === "@types" + ? ` +

+ ${analysis.types.packageName} v${analysis.types.packageVersion} + + (npm, + unpkg, + DefinitelyTyped) + +

+ ` + : "" + }`, }; } diff --git a/packages/web/worker/worker.ts b/packages/web/worker/worker.ts index 0510143..9defa0c 100644 --- a/packages/web/worker/worker.ts +++ b/packages/web/worker/worker.ts @@ -4,6 +4,7 @@ import { createPackageFromTarballData, type CheckResult, } from "@arethetypeswrong/core"; +import { parsePackageSpec } from "@arethetypeswrong/core/utils"; export interface CheckPackageEventData { kind: "check-package"; @@ -25,7 +26,11 @@ export interface ResultMessage { onmessage = async (event: MessageEvent) => { const result = await checkPackage( event.data.kind === "check-file" - ? await createPackageFromTarballData(event.data.file) + ? createPackageFromTarballData(event.data.file) + : event.data.packageSpec.startsWith("@types/") + ? await createPackageFromNpm(unmangleScopedPackageName(event.data.packageSpec), { + definitelyTyped: parsePackageSpec(event.data.packageSpec).data?.version, + }) : await createPackageFromNpm(event.data.packageSpec) ); postMessage({ @@ -35,3 +40,7 @@ onmessage = async (event: MessageEvent