Skip to content

Commit

Permalink
Error on invalid jsx extensions
Browse files Browse the repository at this point in the history
close #76
  • Loading branch information
bluwy committed Sep 28, 2023
1 parent 160318f commit 9cbb9b0
Show file tree
Hide file tree
Showing 10 changed files with 87 additions and 6 deletions.
4 changes: 4 additions & 0 deletions pkg/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'>
Expand Down
7 changes: 7 additions & 0 deletions pkg/src/constants.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
48 changes: 47 additions & 1 deletion pkg/src/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
import { commonInternalPaths, knownBrowserishConditions } from './constants.js'
import {
commonInternalPaths,
invalidJsxExtensions,
knownBrowserishConditions
} from './constants.js'
import {
exportsGlob,
getCodeFormat,
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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, [])
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 () => {
Expand Down
10 changes: 10 additions & 0 deletions pkg/src/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.`
Expand Down
8 changes: 3 additions & 5 deletions pkg/src/utils.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { lintableFileExtensions } from './constants.js'

/**
* @typedef {{
* name: string,
Expand Down Expand Up @@ -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:
Expand Down
Empty file.
Empty file.
7 changes: 7 additions & 0 deletions pkg/tests/fixtures/invalid-jsx-extensions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "publint-invalid-jsx-extensions",
"version": "0.0.1",
"private": true,
"main": "./main.cjsx",
"module": "./main.mjsx"
}
4 changes: 4 additions & 0 deletions pkg/tests/playground.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
5 changes: 5 additions & 0 deletions site/src/utils/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down

0 comments on commit 9cbb9b0

Please sign in to comment.