diff --git a/src/_utils.ts b/src/_utils.ts index f07f8d2..7fe9a64 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -38,3 +38,10 @@ export function matchAll (regex, string, addition) { } return matches } + +const ProtocolRegex = /^(?.+):.+$/ + +export function getProtocol (id: string): string | null { + const proto = id.match(ProtocolRegex) + return proto ? proto.groups.proto : null +} diff --git a/src/syntax.ts b/src/syntax.ts index 921afee..781c192 100644 --- a/src/syntax.ts +++ b/src/syntax.ts @@ -2,6 +2,8 @@ import { promises as fsp } from 'fs' import { extname } from 'pathe' import { readPackageJSON } from 'pkg-types' import { ResolveOptions, resolvePath } from './resolve' +import { isNodeBuiltin } from './utils' +import { getProtocol } from './_utils' const ESM_RE = /([\s;]|^)(import[\w,{}\s*]*from|import\s*['"*{]|export\b\s*([*{]|default|type)|import\.meta\b)/m @@ -27,7 +29,41 @@ export function detectSyntax (code: string) { } } -export async function isValidNodeImport (id: string, opts: ResolveOptions & { code?: string } = {}): Promise { +export interface ValidNodeImportOptions extends ResolveOptions { + /** + * The contents of the import, which may be analyzed to see if it contains + * CJS or ESM syntax as a last step in checking whether it is a valid import. + */ + code?: string + /** + * Protocols that are allowed as valid node imports. + * + * Default: ['node', 'file', 'data'] + */ + allowedProtocols?: Array +} + +const validNodeImportDefaults: ValidNodeImportOptions = { + allowedProtocols: ['node', 'file', 'data'] +} + +export async function isValidNodeImport (id: string, _opts: ValidNodeImportOptions = {}): Promise { + if (isNodeBuiltin(id)) { + return true + } + + const opts = { ...validNodeImportDefaults, ..._opts } + + const proto = getProtocol(id) + if (proto && !opts.allowedProtocols.includes(proto)) { + return false + } + + // node is already validated by isNodeBuiltin and file will be normalized by resolvePath + if (proto === 'data') { + return true + } + const resolvedPath = await resolvePath(id, opts) const extension = extname(resolvedPath) diff --git a/src/utils.ts b/src/utils.ts index 3a9c5d6..8a50c28 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,3 @@ - import { fileURLToPath as _fileURLToPath } from 'url' import { promises as fsp } from 'fs' import { normalizeSlash, BUILTIN_MODULES } from './_utils' @@ -33,3 +32,9 @@ export function toDataURL (code: string): string { const base64 = Buffer.from(code).toString('base64') return `data:text/javascript;base64,${base64}` } + +export function isNodeBuiltin (id: string = '') { + // node:fs/promises => fs + id = id.replace(/^node:/, '').split('/')[0] + return BUILTIN_MODULES.has(id) +} diff --git a/test/fixture/imports/cjs/index.cjs b/test/fixture/imports/cjs/index.cjs new file mode 100644 index 0000000..011bde7 --- /dev/null +++ b/test/fixture/imports/cjs/index.cjs @@ -0,0 +1 @@ +module.exports = 'cjs' diff --git a/test/fixture/imports/cjs/package.json b/test/fixture/imports/cjs/package.json new file mode 100644 index 0000000..a52022f --- /dev/null +++ b/test/fixture/imports/cjs/package.json @@ -0,0 +1,5 @@ +{ + "name": "cjs", + "version": "1.0.0", + "main": "index.cjs" +} diff --git a/test/fixture/imports/esm-module/index.js b/test/fixture/imports/esm-module/index.js new file mode 100644 index 0000000..467d60c --- /dev/null +++ b/test/fixture/imports/esm-module/index.js @@ -0,0 +1 @@ +export default 'esm-module' diff --git a/test/fixture/imports/esm-module/package.json b/test/fixture/imports/esm-module/package.json new file mode 100644 index 0000000..37f1b9a --- /dev/null +++ b/test/fixture/imports/esm-module/package.json @@ -0,0 +1,6 @@ +{ + "name": "esm-module", + "version": "1.0.0", + "type": "module", + "main": "index.js" +} diff --git a/test/fixture/imports/esm/index.mjs b/test/fixture/imports/esm/index.mjs new file mode 100644 index 0000000..b8a885d --- /dev/null +++ b/test/fixture/imports/esm/index.mjs @@ -0,0 +1 @@ +export default 'esm' diff --git a/test/fixture/imports/esm/package.json b/test/fixture/imports/esm/package.json new file mode 100644 index 0000000..2931a73 --- /dev/null +++ b/test/fixture/imports/esm/package.json @@ -0,0 +1,5 @@ +{ + "name": "esm", + "version": "1.0.0", + "main": "index.mjs" +} diff --git a/test/fixture/imports/js-cjs/index.js b/test/fixture/imports/js-cjs/index.js new file mode 100644 index 0000000..d4fe56a --- /dev/null +++ b/test/fixture/imports/js-cjs/index.js @@ -0,0 +1 @@ +module.exports = 'js-cjs' diff --git a/test/fixture/imports/js-cjs/package.json b/test/fixture/imports/js-cjs/package.json new file mode 100644 index 0000000..206fec1 --- /dev/null +++ b/test/fixture/imports/js-cjs/package.json @@ -0,0 +1,5 @@ +{ + "name": "js-cjs", + "version": "1.0.0", + "main": "index.js" +} diff --git a/test/fixture/imports/js-esm/index.js b/test/fixture/imports/js-esm/index.js new file mode 100644 index 0000000..4e9f232 --- /dev/null +++ b/test/fixture/imports/js-esm/index.js @@ -0,0 +1 @@ +export default 'js-esm' diff --git a/test/fixture/imports/js-esm/package.json b/test/fixture/imports/js-esm/package.json new file mode 100644 index 0000000..350deb7 --- /dev/null +++ b/test/fixture/imports/js-esm/package.json @@ -0,0 +1,5 @@ +{ + "name": "js-esm", + "version": "1.0.0", + "main": "index.js" +} diff --git a/test/syntax.test.mjs b/test/syntax.test.mjs index 65d5aa3..b87d9c6 100644 --- a/test/syntax.test.mjs +++ b/test/syntax.test.mjs @@ -1,5 +1,6 @@ -import { expect } from 'chai' -import { detectSyntax } from 'mlly' +import { expect, AssertionError } from 'chai' +import { detectSyntax, isValidNodeImport } from 'mlly' +import { join } from 'pathe' const staticTests = { // ESM @@ -35,3 +36,37 @@ describe('detectSyntax', () => { }) } }) + +const nodeImportTests = { + [import.meta.url]: true, + 'node:fs': true, + fs: true, + 'fs/promises': true, + 'node:fs/promises': true, + // We can't detect these are invalid node imports + 'fs/fake': true, + 'node:fs/fake': true, + vue: 'error', + [join(import.meta.url, '../invalid')]: 'error', + 'data:text/javascript,console.log("hello!");': true, + [join(import.meta.url, '../fixture/imports/cjs')]: true, + [join(import.meta.url, '../fixture/imports/esm')]: true, + [join(import.meta.url, '../fixture/imports/esm-module')]: true, + [join(import.meta.url, '../fixture/imports/js-cjs')]: true, + [join(import.meta.url, '../fixture/imports/js-esm')]: false +} + +describe('isValidNodeImport', () => { + for (const [input, result] of Object.entries(nodeImportTests)) { + it(input, async () => { + try { + expect(await isValidNodeImport(input)).to.equal(result) + } catch (e) { + if (e instanceof AssertionError) { + throw e + } + expect(result).to.equal('error') + } + }) + } +}) diff --git a/test/utils.test.mjs b/test/utils.test.mjs new file mode 100644 index 0000000..c66d97e --- /dev/null +++ b/test/utils.test.mjs @@ -0,0 +1,23 @@ +import { isNodeBuiltin } from 'mlly' +import { expect } from 'chai' + +describe('isNodeBuiltin', () => { + const cases = { + fs: true, + fake: false, + 'node:fs': true, + 'node:fake': false, + 'fs/promises': true, + 'fs/fake': true // invalid import + } + + for (const id in cases) { + it(`'${id}': ${cases[id]}`, () => { + expect(isNodeBuiltin(id)).to.equal(cases[id]) + }) + } + + it('undefined', () => { + expect(isNodeBuiltin()).to.equal(false) + }) +})