From 4949ae4049e649b07d32503c47d121b271caa4ea Mon Sep 17 00:00:00 2001 From: bluwy Date: Wed, 27 Sep 2023 15:09:28 +0800 Subject: [PATCH] Error if field has invalid value type fixes #73 --- pkg/index.d.ts | 7 ++++ pkg/src/index.js | 35 ++++++++++++++++--- pkg/src/message.js | 12 +++++++ .../fixtures/invalid-field-types/package.json | 8 +++++ pkg/tests/playground.js | 6 ++++ site/rules.md | 4 +++ site/src/utils/message.js | 12 +++++++ 7 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 pkg/tests/fixtures/invalid-field-types/package.json diff --git a/pkg/index.d.ts b/pkg/index.d.ts index 07c087a..d6cd267 100644 --- a/pkg/index.d.ts +++ b/pkg/index.d.ts @@ -83,6 +83,13 @@ export type Message = expectExtension: string } > + | BaseMessage< + 'FIELD_INVALID_VALUE_TYPE', + { + actualType: string + expectTypes: string[] + } + > export interface Options { /** diff --git a/pkg/src/index.js b/pkg/src/index.js index d49d690..ee96ddd 100755 --- a/pkg/src/index.js +++ b/pkg/src/index.js @@ -50,7 +50,7 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) { // Relies on default node resolution // https://nodejs.org/api/modules.html#all-together // LOAD_INDEX(X) - if (!main && !module && !exports) { + if (main == null && module == null && exports == null) { promiseQueue.push(async () => { // check index.js only, others aren't our problem const defaultPath = vfs.pathJoin(pkgDir, 'index.js') @@ -83,8 +83,9 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) { * - It's mostly used for CJS * - It can be used for ESM, but if you're doing so, might as well use exports */ - if (main) { + if (main != null) { promiseQueue.push(async () => { + if (!ensureTypeOfField(main, ['string'], mainPkgPath)) return const mainPath = vfs.pathJoin(pkgDir, main) const mainContent = await readFile(mainPath, mainPkgPath, [ '.js', @@ -130,8 +131,9 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) { * - Is not a way to support dual packages in NodeJS * - Should be MJS always!! */ - if (module) { + if (module != null) { promiseQueue.push(async () => { + if (!ensureTypeOfField(module, ['string'], modulePkgPath)) return const modulePath = vfs.pathJoin(pkgDir, module) const moduleContent = await readFile(modulePath, modulePkgPath, [ '.js', @@ -176,7 +178,10 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) { } for (const field of knownFields) { const [fieldValue, fieldPkgPath] = getPublishedField(rootPkg, field) - if (typeof fieldValue === 'string') { + if ( + fieldValue != null && + ensureTypeOfField(fieldValue, ['string'], fieldPkgPath) + ) { promiseQueue.push(async () => { const fieldPath = vfs.pathJoin(pkgDir, fieldValue) await readFile(fieldPath, fieldPkgPath, ['.js', '/index.js']) @@ -347,6 +352,28 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) { }) } + /** + * @param {any} fieldValue + * @param {('string' | 'number' | 'boolean' | 'object')[]} expectTypes + * @param {string[]} pkgPath + */ + function ensureTypeOfField(fieldValue, expectTypes, pkgPath) { + // @ts-expect-error typeof doesn't need to match `expectedTypes` type but TS panics + if (!expectTypes.includes(typeof fieldValue)) { + messages.push({ + code: 'FIELD_INVALID_VALUE_TYPE', + args: { + actualType: typeof fieldValue, + expectTypes + }, + path: pkgPath, + type: 'error' + }) + return false + } + return true + } + /** * @param {string | Record} fieldValue * @param {string[]} currentPath diff --git a/pkg/src/message.js b/pkg/src/message.js index 58d84b5..ebe3892 100644 --- a/pkg/src/message.js +++ b/pkg/src/message.js @@ -119,6 +119,18 @@ export function formatMessage(m, pkg) { + `Consider splitting out two ${c.bold('"types"')} conditions for ${c.bold('"import"')} and ${c.bold('"require"')}, and use the ${c.yellow(m.args.expectExtension)} extension, ` + `e.g. ${c.bold(fp(expectPath))}: "${c.bold(replaceLast(pv(m.path), m.args.actualExtension, m.args.expectExtension))}"` } + case 'FIELD_INVALID_VALUE_TYPE': { + let expectStr = m.args.expectTypes[0] + for (let i = 1; i < m.args.expectTypes.length; i++) { + if (i === m.args.expectTypes.length - 1) { + expectStr += ` or ${m.args.expectTypes[i]}` + } else { + expectStr += `, ${m.args.expectTypes[i]}` + } + } + // prettier-ignore + return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} which is an invalid ${c.bold(m.args.actualType)} type. Expected a ${c.bold(expectStr)} type instead.` + } default: return } diff --git a/pkg/tests/fixtures/invalid-field-types/package.json b/pkg/tests/fixtures/invalid-field-types/package.json new file mode 100644 index 0000000..53793cd --- /dev/null +++ b/pkg/tests/fixtures/invalid-field-types/package.json @@ -0,0 +1,8 @@ +{ + "name": "publint-invalid-field-types", + "version": "0.0.1", + "private": true, + "main": 0, + "module": true, + "jsnext:main": false +} \ No newline at end of file diff --git a/pkg/tests/playground.js b/pkg/tests/playground.js index 19424b6..0691c43 100644 --- a/pkg/tests/playground.js +++ b/pkg/tests/playground.js @@ -20,6 +20,12 @@ testFixture('glob-deprecated', [ 'EXPORTS_GLOB_NO_MATCHED_FILES' ]) +testFixture('invalid-field-types', [ + 'FIELD_INVALID_VALUE_TYPE', + 'FIELD_INVALID_VALUE_TYPE', + 'FIELD_INVALID_VALUE_TYPE' +]) + testFixture('missing-files', [ ...Array(7).fill('FILE_DOES_NOT_EXIST'), 'FILE_NOT_PUBLISHED', diff --git a/site/rules.md b/site/rules.md index 79d16ec..3344be2 100644 --- a/site/rules.md +++ b/site/rules.md @@ -183,3 +183,7 @@ Depending on your setup, you can also use the `"exports"` field to directly expo } } ``` + +## `FIELD_INVALID_VALUE_TYPE` + +Some `package.json` fields has a set of allowed types, e.g. `string` or `object` only. If an invalid type is passed, this error message will be showed. diff --git a/site/src/utils/message.js b/site/src/utils/message.js index 46f5864..ab3cc59 100644 --- a/site/src/utils/message.js +++ b/site/src/utils/message.js @@ -118,6 +118,18 @@ function messageToString(m, pkg) { + `Consider splitting out two ${bold('"types"')} conditions for ${bold('"import"')} and ${bold('"require"')}, and use the ${warn(m.args.expectExtension)} extension, ` + `e.g. ${bold(fp(expectPath))}: "${bold(replaceLast(pv(m.path), m.args.actualExtension, m.args.expectExtension))}"` } + case 'FIELD_INVALID_VALUE_TYPE': { + let expectStr = m.args.expectTypes[0] + for (let i = 1; i < m.args.expectTypes.length; i++) { + if (i === m.args.expectTypes.length - 1) { + expectStr += ` or ${m.args.expectTypes[i]}` + } else { + expectStr += `, ${m.args.expectTypes[i]}` + } + } + // prettier-ignore + return `${bold(fp(m.path))} is ${bold(pv(m.path))} which is an invalid ${bold(m.args.actualType)} type. Expected a ${bold(expectStr)} type instead.` + } default: return }