Skip to content

Commit

Permalink
feat: detect invalid node imports (#4)
Browse files Browse the repository at this point in the history
Co-authored-by: pooya parsa <pyapar@gmail.com>
  • Loading branch information
danielroe and pi0 authored Oct 28, 2021
1 parent 44eb58d commit 114f1d6
Show file tree
Hide file tree
Showing 9 changed files with 83 additions and 45 deletions.
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
"scripts": {
"build": "siroc build",
"lint": "eslint --ext .js,.ts .",
"test": "yarn lint && jest",
"prepublishOnly": "yarn build",
"release": "yarn test && standard-version && git push --follow-tags && npm publish"
"release": "yarn test && standard-version && git push --follow-tags && npm publish",
"test": "yarn lint && jest"
},
"dependencies": {
"allowlist": "^0.1.1",
"enhanced-resolve": "^5.8.3",
"mlly": "^0.3.10",
"pathe": "^0.2.0",
"ufo": "^0.7.9"
},
Expand Down
30 changes: 22 additions & 8 deletions src/externals.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { extname } from 'pathe'
import { isValidNodeImport } from 'mlly'
import type { ResolveOptions } from './resolve'
import { Matcher, matches, toMatcher } from './utils'
import { getProtocol, Matcher, matches, toMatcher } from './utils'
import { resolveId } from './resolve'

export interface ExternalsOptions {
Expand All @@ -16,7 +17,7 @@ export interface ExternalsOptions {
* Protocols that are allowed to be externalized.
* Any other matched protocol will be inlined.
*
* Default: ['node', 'fs', 'data']
* Default: ['node', 'file', 'data']
*/
externalProtocols?: Array<string>
/**
Expand All @@ -30,6 +31,11 @@ export interface ExternalsOptions {
* Resolve options (passed directly to [`enhanced-resolve`](https://github.com/webpack/enhanced-resolve))
*/
resolve?: Partial<ResolveOptions>
/**
* Try to automatically detect and inline invalid node imports
* matching file name (at first) and then loading code.
*/
detectInvalidNodeImports?: boolean
}

export const ExternalsDefaults: ExternalsOptions = {
Expand All @@ -45,13 +51,12 @@ export const ExternalsDefaults: ExternalsOptions = {
/\?/
],
external: [],
externalProtocols: ['node', 'fs', 'data'],
externalProtocols: ['node', 'file', 'data'],
externalExtensions: ['.js', '.mjs', '.cjs', '.node'],
resolve: {}
resolve: {},
detectInvalidNodeImports: true
}

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

export async function isExternal (id: string, importer: string, opts: ExternalsOptions = {}): Promise<null | { id: string, external: true }> {
// Apply defaults
opts = { ...ExternalsDefaults, ...opts }
Expand All @@ -68,11 +73,15 @@ export async function isExternal (id: string, importer: string, opts: ExternalsO
}

// Inline not allowed protocols
const proto = id.match(ProtocolRegex)
if (proto && !opts.externalProtocols.includes(proto.groups.proto)) {
const proto = getProtocol(id)
if (proto && !opts.externalProtocols.includes(proto)) {
return null
}

if (proto === 'data') {
return { id, external: true }
}

// Resolve id
const r = ctx.resolved = await resolveId(id, importer, opts.resolve).catch((_err) => {
return { id, path: id, external: null }
Expand All @@ -96,6 +105,11 @@ export async function isExternal (id: string, importer: string, opts: ExternalsO
matches(r.id, externalMatchers, ctx) ||
matches(r.path, externalMatchers, ctx)
) {
// Inline invalid node imports
if (opts.detectInvalidNodeImports && !await isValidNodeImport(r.path)) {
return null
}

return { id: r.id, external: true }
}

Expand Down
5 changes: 3 additions & 2 deletions src/resolve.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import { fileURLToPath } from 'url'
import { promisify } from 'util'
import { hasProtocol } from 'ufo'
import enhancedResolve from 'enhanced-resolve'
import { isNodeBuiltin } from 'mlly'
import type { ResolveOptions as EnhancedResolveOptions } from 'enhanced-resolve'
import { isBuiltin, getType } from './utils'
import { getType } from './utils'

export type ModuleType = 'commonjs' | 'module' | 'unknown'

Expand Down Expand Up @@ -51,7 +52,7 @@ export async function resolveId (id: string, base: string = '.', opts: ResolveOp
}
}

if (isBuiltin(id)) {
if (isNodeBuiltin(id)) {
return {
id: id.replace(/^node:/, ''),
path: id,
Expand Down
14 changes: 7 additions & 7 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { builtinModules } from 'module'
import type { ModuleType } from './resolve'

export type Matcher<T = any> = RegExp | ((input: string, ctx?: T) => boolean)

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

export function getProtocol (id: string): string | null {
const proto = id.match(ProtocolRegex)
return proto ? proto.groups.proto : null
}

export function matches <T = any> (input: string, matchers: Matcher<T>[], ctx?: T) {
return matchers.some((matcher) => {
if (matcher instanceof RegExp) {
Expand All @@ -22,12 +28,6 @@ export function toMatcher (pattern: any) {
return typeof pattern === 'string' ? new RegExp(`([\\/]|^)${pattern}([\\/]|$)`) : pattern
}

export function isBuiltin (id: string = '') {
// node:fs/promises => fs
id = id.replace(/^node:/, '').split('/')[0]
return builtinModules.includes(id)
}

export function getType (id: string, fallback: ModuleType = 'commonjs'): ModuleType {
if (id.endsWith('.cjs')) { return 'commonjs' }
if (id.endsWith('.mjs')) { return 'module' }
Expand Down
41 changes: 37 additions & 4 deletions test/external.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as upath from 'upath'
import { pathToFileURL } from 'url'
import { resolve } from 'pathe'
import { isExternal } from '../src/externals'

const fixtureDir = upath.resolve(__dirname, 'fixture')
// const r = (...p) => upath.resolve(fixtureDir, ...p)
const fixtureDir = resolve(__dirname, 'fixture')
const r = (...p) => resolve(fixtureDir, ...p)

describe('isExternal', () => {
const inputs: Array<{ input: Parameters<typeof isExternal>, output: any }> = [
Expand All @@ -11,19 +12,51 @@ describe('isExternal', () => {
output: null
},
{
input: ['vue', fixtureDir, { external: ['vue'] }],
input: ['vue', fixtureDir, { external: ['vue'], detectInvalidNodeImports: false }],
output: {
id: 'vue',
external: true
}
},
{
input: ['allowlist', fixtureDir, { external: ['allowlist'] }],
output: {
id: 'allowlist',
external: true
}
},
{
input: ['esm', fixtureDir, { external: ['node_modules'] }],
output: {
id: 'esm',
external: true
}
},
{
input: ['esm/index.js', fixtureDir, { external: ['node_modules'] }],
output: {
id: 'esm/index.js',
external: true
}
},
{
input: [pathToFileURL(r('node_modules/esm')).href, fixtureDir, { external: ['node_modules'] }],
output: {
id: r('node_modules/esm'),
external: true
}
},
{
input: ['data:text/javascript,console.log("hello!");', fixtureDir, { external: ['node_modules'] }],
output: {
id: 'data:text/javascript,console.log("hello!");',
external: true
}
},
{
input: ['invalid', fixtureDir, { external: ['node_modules'] }],
output: null
},
{
input: ['esm', fixtureDir],
output: null
Expand Down
1 change: 1 addition & 0 deletions test/fixture/node_modules/invalid/index.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions test/fixture/node_modules/invalid/package.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

23 changes: 1 addition & 22 deletions test/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,4 @@
import { isBuiltin, toMatcher } from '../src'

describe('isBuiltin', () => {
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) {
test(`'${id}': ${cases[id]}`, () => {
expect(isBuiltin(id)).toBe(cases[id])
})
}

test('undefined', () => {
expect(isBuiltin()).toBe(false)
})
})
import { toMatcher } from '../src'

describe('toMatcher', () => {
it('converts strings to RE', () => {
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3754,6 +3754,11 @@ mkdist@^0.3.3:
upath "^2.0.1"
vue-template-compiler "^2.6.14"

mlly@^0.3.10:
version "0.3.10"
resolved "https://registry.yarnpkg.com/mlly/-/mlly-0.3.10.tgz#cf3353565c84e951311c46c8d2c8b320d90f9eb3"
integrity sha512-vD3A7naDtIOqHYZhnYUrECRO6UODWNqz6T0TS/pxwolzVoWKX/mXJF1XSM3qxruCDtkzJbzJPgesByihY/r3EA==

modify-values@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022"
Expand Down

0 comments on commit 114f1d6

Please sign in to comment.