From 9cbb9b06e1d66d686440519b3ae8bb6fe54736ad Mon Sep 17 00:00:00 2001 From: bluwy Date: Thu, 28 Sep 2023 21:30:26 +0800 Subject: [PATCH] Error on invalid jsx extensions close #76 --- pkg/index.d.ts | 4 ++ pkg/src/constants.js | 7 +++ pkg/src/index.js | 48 ++++++++++++++++++- pkg/src/message.js | 10 ++++ pkg/src/utils.js | 8 ++-- .../fixtures/invalid-jsx-extensions/main.cjsx | 0 .../fixtures/invalid-jsx-extensions/main.mjsx | 0 .../invalid-jsx-extensions/package.json | 7 +++ pkg/tests/playground.js | 4 ++ site/src/utils/message.js | 5 ++ 10 files changed, 87 insertions(+), 6 deletions(-) create mode 100644 pkg/tests/fixtures/invalid-jsx-extensions/main.cjsx create mode 100644 pkg/tests/fixtures/invalid-jsx-extensions/main.mjsx create mode 100644 pkg/tests/fixtures/invalid-jsx-extensions/package.json diff --git a/pkg/index.d.ts b/pkg/index.d.ts index 4ee7a0e..1bc835d 100644 --- a/pkg/index.d.ts +++ b/pkg/index.d.ts @@ -44,6 +44,10 @@ export type Message = actualFilePath?: string } > + | BaseMessage< + 'FILE_INVALID_JSX_EXTENSION', + { actualExtension: string; globbedFilePath?: string } + > | BaseMessage<'FILE_DOES_NOT_EXIST'> | BaseMessage<'FILE_NOT_PUBLISHED'> | BaseMessage<'MODULE_SHOULD_BE_ESM'> diff --git a/pkg/src/constants.js b/pkg/src/constants.js index 5b0fff6..81af27b 100644 --- a/pkg/src/constants.js +++ b/pkg/src/constants.js @@ -1,3 +1,10 @@ +// extensions that publint is able to parse and lint. while there's partial support +// for TypeScript, it's not completely safe to do so in a check yet +export const lintableFileExtensions = ['.js', '.mjs', '.cjs'] + +// common misconception that JSX is also affected by "m" and "c" semantics +export const invalidJsxExtensions = ['.mjsx', '.cjsx', '.mtsx', '.cjsx'] + // Some exports condition are known to be similar to the browser condition but // runs slightly different. Specifically, the `pkg.browser` field will be applied // even after resolving with these export conditions, which can be confusing at times. diff --git a/pkg/src/index.js b/pkg/src/index.js index 8e414aa..6f44622 100755 --- a/pkg/src/index.js +++ b/pkg/src/index.js @@ -1,4 +1,8 @@ -import { commonInternalPaths, knownBrowserishConditions } from './constants.js' +import { + commonInternalPaths, + invalidJsxExtensions, + knownBrowserishConditions +} from './constants.js' import { exportsGlob, getCodeFormat, @@ -111,6 +115,8 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) { '/index.js' ]) if (mainContent === false) return + if (hasInvalidJsxExtension(main, mainPkgPath)) return + if (!isFilePathLintable(main)) return const actualFormat = getCodeFormat(mainContent) const expectFormat = await getFilePathFormat(mainPath, vfs) if ( @@ -159,6 +165,8 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) { '/index.js' ]) if (moduleContent === false) return + if (hasInvalidJsxExtension(module, modulePkgPath)) return + if (!isFilePathLintable(main)) return const actualFormat = getCodeFormat(moduleContent) if (actualFormat === 'CJS') { messages.push({ @@ -248,6 +256,14 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) { ) const pq = createPromiseQueue() for (const filePath of files) { + if ( + hasInvalidJsxExtension( + filePath, + ['name'], + '/' + vfs.pathRelative(pkgDir, filePath) + ) + ) + continue if (!isFilePathLintable(filePath)) continue pq.push(async () => { const fileContent = await readFile(filePath, []) @@ -371,6 +387,28 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) { }) } + /** + * @param {string} filePath + * @param {string[]} currentPath + * @param {string} [globbedFilePath] only needed for globs + */ + function hasInvalidJsxExtension(filePath, currentPath, globbedFilePath) { + const matched = invalidJsxExtensions.find((ext) => filePath.endsWith(ext)) + if (matched) { + messages.push({ + code: 'FILE_INVALID_JSX_EXTENSION', + args: { + actualExtension: matched, + globbedFilePath + }, + path: currentPath, + type: 'error' + }) + return true + } + return false + } + /** * @param {any} fieldValue * @param {('string' | 'number' | 'boolean' | 'object')[]} expectTypes @@ -514,6 +552,14 @@ export async function publint({ pkgDir, vfs, level, strict, _packedFiles }) { // TODO: group glob warnings for (const filePath of exportsFiles) { + if ( + hasInvalidJsxExtension( + filePath, + currentPath, + isGlob ? './' + vfs.pathRelative(pkgDir, filePath) : undefined + ) + ) + return // TODO: maybe check .ts in the future if (!isFilePathLintable(filePath)) continue pq.push(async () => { diff --git a/pkg/src/message.js b/pkg/src/message.js index b94702f..ff20051 100644 --- a/pkg/src/message.js +++ b/pkg/src/message.js @@ -36,6 +36,16 @@ export function formatMessage(m, pkg) { // prettier-ignore return `${start} ends with the ${c.yellow(m.args.actualExtension)} extension, but the code is written in ${c.yellow(m.args.actualFormat)}. Consider using the ${c.yellow(m.args.expectExtension)} extension, e.g. ${c.bold(replaceLast(relativePath,m.args.actualExtension, m.args.expectExtension))}` } + case 'FILE_INVALID_JSX_EXTENSION': { + const is = m.args.globbedFilePath ? 'matches' : 'is' + const relativePath = m.args.globbedFilePath ?? pv(m.path) + const start = + m.path[0] === 'name' + ? c.bold(relativePath) + : `${c.bold(fp(m.path))} ${is} ${c.bold(relativePath)} which` + // prettier-ignore + return `${start} uses an invalid ${c.bold(m.args.actualExtension)} extension. You don't need to split ESM and CJS formats for JSX. You should write a single file in ESM with the ${c.bold('.jsx')} extension instead, e.g. ${c.bold(replaceLast(pv(m.path), m.args.actualExtension, '.jsx'))}` + } case 'FILE_DOES_NOT_EXIST': // prettier-ignore return `${c.bold(fp(m.path))} is ${c.bold(pv(m.path))} but the file does not exist.` diff --git a/pkg/src/utils.js b/pkg/src/utils.js index 35ebd31..cfa6fe3 100644 --- a/pkg/src/utils.js +++ b/pkg/src/utils.js @@ -1,3 +1,5 @@ +import { lintableFileExtensions } from './constants.js' + /** * @typedef {{ * name: string, @@ -213,11 +215,7 @@ export function isExplicitExtension(path) { * @param {string} filePath */ export function isFilePathLintable(filePath) { - return ( - filePath.endsWith('.js') || - filePath.endsWith('.mjs') || - filePath.endsWith('.cjs') - ) + return lintableFileExtensions.some((ext) => filePath.endsWith(ext)) } // support: diff --git a/pkg/tests/fixtures/invalid-jsx-extensions/main.cjsx b/pkg/tests/fixtures/invalid-jsx-extensions/main.cjsx new file mode 100644 index 0000000..e69de29 diff --git a/pkg/tests/fixtures/invalid-jsx-extensions/main.mjsx b/pkg/tests/fixtures/invalid-jsx-extensions/main.mjsx new file mode 100644 index 0000000..e69de29 diff --git a/pkg/tests/fixtures/invalid-jsx-extensions/package.json b/pkg/tests/fixtures/invalid-jsx-extensions/package.json new file mode 100644 index 0000000..1f154a5 --- /dev/null +++ b/pkg/tests/fixtures/invalid-jsx-extensions/package.json @@ -0,0 +1,7 @@ +{ + "name": "publint-invalid-jsx-extensions", + "version": "0.0.1", + "private": true, + "main": "./main.cjsx", + "module": "./main.mjsx" +} \ No newline at end of file diff --git a/pkg/tests/playground.js b/pkg/tests/playground.js index 9c9c432..005c905 100644 --- a/pkg/tests/playground.js +++ b/pkg/tests/playground.js @@ -31,6 +31,10 @@ testFixture('invalid-field-types', [ 'FIELD_INVALID_VALUE_TYPE' ]) +testFixture('invalid-jsx-extensions', [ + ...Array(4).fill('FILE_INVALID_JSX_EXTENSION') +]) + testFixture('missing-files', [ ...Array(7).fill('FILE_DOES_NOT_EXIST'), 'FILE_NOT_PUBLISHED', diff --git a/site/src/utils/message.js b/site/src/utils/message.js index 11a69d5..9fdb1ad 100644 --- a/site/src/utils/message.js +++ b/site/src/utils/message.js @@ -37,6 +37,11 @@ function messageToString(m, pkg) { // prettier-ignore return `${bold(relativePath)} ends with the ${warn(m.args.actualExtension)} extension, but the code is written in ${warn(m.args.actualFormat)}. Consider using the ${warn(m.args.expectExtension)} extension, e.g. ${bold(replaceLast(relativePath, m.args.actualExtension, m.args.expectExtension))}` } + case 'FILE_INVALID_JSX_EXTENSION': { + const relativePath = m.args.globbedFilePath ?? pv(m.path) + // prettier-ignore + return `${bold(relativePath)} uses an invalid ${bold(m.args.actualExtension)} extension. You don't need to split ESM and CJS formats for JSX. You should write a single file in ESM with the ${bold('.jsx')} extension instead, e.g. ${bold(replaceLast(pv(m.path), m.args.actualExtension, '.jsx'))}` + } case 'FILE_DOES_NOT_EXIST': // prettier-ignore return `File does not exist`