Skip to content

Commit

Permalink
feat: support protocols for isValidNodeImport (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Oct 28, 2021
1 parent 17ee944 commit 0cfa4d9
Show file tree
Hide file tree
Showing 15 changed files with 141 additions and 4 deletions.
7 changes: 7 additions & 0 deletions src/_utils.ts
Expand Up @@ -38,3 +38,10 @@ export function matchAll (regex, string, addition) {
}
return matches
}

const ProtocolRegex = /^(?<proto>.+):.+$/

export function getProtocol (id: string): string | null {
const proto = id.match(ProtocolRegex)
return proto ? proto.groups.proto : null
}
38 changes: 37 additions & 1 deletion src/syntax.ts
Expand Up @@ -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

Expand All @@ -27,7 +29,41 @@ export function detectSyntax (code: string) {
}
}

export async function isValidNodeImport (id: string, opts: ResolveOptions & { code?: string } = {}): Promise<boolean> {
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<string>
}

const validNodeImportDefaults: ValidNodeImportOptions = {
allowedProtocols: ['node', 'file', 'data']
}

export async function isValidNodeImport (id: string, _opts: ValidNodeImportOptions = {}): Promise<boolean> {
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)

Expand Down
7 changes: 6 additions & 1 deletion 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'
Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions test/fixture/imports/cjs/index.cjs
@@ -0,0 +1 @@
module.exports = 'cjs'
5 changes: 5 additions & 0 deletions test/fixture/imports/cjs/package.json
@@ -0,0 +1,5 @@
{
"name": "cjs",
"version": "1.0.0",
"main": "index.cjs"
}
1 change: 1 addition & 0 deletions test/fixture/imports/esm-module/index.js
@@ -0,0 +1 @@
export default 'esm-module'
6 changes: 6 additions & 0 deletions test/fixture/imports/esm-module/package.json
@@ -0,0 +1,6 @@
{
"name": "esm-module",
"version": "1.0.0",
"type": "module",
"main": "index.js"
}
1 change: 1 addition & 0 deletions test/fixture/imports/esm/index.mjs
@@ -0,0 +1 @@
export default 'esm'
5 changes: 5 additions & 0 deletions test/fixture/imports/esm/package.json
@@ -0,0 +1,5 @@
{
"name": "esm",
"version": "1.0.0",
"main": "index.mjs"
}
1 change: 1 addition & 0 deletions test/fixture/imports/js-cjs/index.js
@@ -0,0 +1 @@
module.exports = 'js-cjs'
5 changes: 5 additions & 0 deletions test/fixture/imports/js-cjs/package.json
@@ -0,0 +1,5 @@
{
"name": "js-cjs",
"version": "1.0.0",
"main": "index.js"
}
1 change: 1 addition & 0 deletions test/fixture/imports/js-esm/index.js
@@ -0,0 +1 @@
export default 'js-esm'
5 changes: 5 additions & 0 deletions test/fixture/imports/js-esm/package.json
@@ -0,0 +1,5 @@
{
"name": "js-esm",
"version": "1.0.0",
"main": "index.js"
}
39 changes: 37 additions & 2 deletions 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
Expand Down Expand Up @@ -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')
}
})
}
})
23 changes: 23 additions & 0 deletions 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)
})
})

0 comments on commit 0cfa4d9

Please sign in to comment.