├── test ├── fixtures │ ├── shader.vert │ ├── test.json │ ├── vanilla.js │ ├── export-default-object.js │ ├── export-named.js │ ├── esm-module │ │ ├── index.js │ │ ├── index.mjs │ │ └── package.json │ ├── export-default.js │ ├── import-esm.js │ ├── pkg-fields │ │ ├── main.js │ │ ├── module.mjs │ │ ├── browser.js │ │ └── package.json │ ├── import-named.js │ ├── import-pkg-field.js │ ├── import-default.js │ ├── import-esm-with-cjs.js │ ├── import-builtin.js │ ├── pkg-fields-missing-browser │ │ ├── main.js │ │ ├── module.mjs │ │ └── package.json │ ├── pkg-fields-missing-module │ │ ├── main.js │ │ ├── browser.js │ │ └── package.json │ ├── import-dynamic.js │ ├── import-wildcard.js │ ├── import-contrast.js │ ├── require-contrast.js │ ├── import-json.js │ ├── export-default-object.mjs │ ├── import-with-brfs.js │ ├── import-with-glslify.js │ ├── import-with-glslify-2.js │ └── no-import-with-syntax.js ├── test-syntax.js ├── test-resolve.js ├── test-transform.js └── test-plugin.js ├── .gitignore ├── .npmignore ├── LICENSE.md ├── resolve.js ├── package.json ├── README.md ├── transform.js └── esmify.js /test/fixtures/shader.vert: -------------------------------------------------------------------------------- 1 | void main () { /* test */ } -------------------------------------------------------------------------------- /test/fixtures/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar" 3 | } -------------------------------------------------------------------------------- /test/fixtures/vanilla.js: -------------------------------------------------------------------------------- 1 | console.log('plain old JS'); 2 | -------------------------------------------------------------------------------- /test/fixtures/export-default-object.js: -------------------------------------------------------------------------------- 1 | console.log('invalid'); 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | -------------------------------------------------------------------------------- /test/fixtures/export-named.js: -------------------------------------------------------------------------------- 1 | export function foobar () { 2 | return 'baz'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/esm-module/index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('esm')(module)('./index.mjs'); 2 | -------------------------------------------------------------------------------- /test/fixtures/esm-module/index.mjs: -------------------------------------------------------------------------------- 1 | export function test () { 2 | return 'hello'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/export-default.js: -------------------------------------------------------------------------------- 1 | export default function () { 2 | return 'foo'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/import-esm.js: -------------------------------------------------------------------------------- 1 | import { test } from './esm-module'; 2 | console.log(test()); 3 | -------------------------------------------------------------------------------- /test/fixtures/pkg-fields/main.js: -------------------------------------------------------------------------------- 1 | export function test () { 2 | return 'hello main'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/pkg-fields/module.mjs: -------------------------------------------------------------------------------- 1 | export function test () { 2 | return 'hello mjs'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/import-named.js: -------------------------------------------------------------------------------- 1 | import { foobar } from './export-named'; 2 | console.log(foobar()); 3 | -------------------------------------------------------------------------------- /test/fixtures/import-pkg-field.js: -------------------------------------------------------------------------------- 1 | import { test } from './pkg-fields'; 2 | console.log(test()); 3 | -------------------------------------------------------------------------------- /test/fixtures/pkg-fields/browser.js: -------------------------------------------------------------------------------- 1 | export function test () { 2 | return 'hello browser'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/import-default.js: -------------------------------------------------------------------------------- 1 | import foobar from './export-default.js'; 2 | console.log(foobar()); 3 | -------------------------------------------------------------------------------- /test/fixtures/import-esm-with-cjs.js: -------------------------------------------------------------------------------- 1 | const { test } = require('./esm-module'); 2 | console.log(test()); 3 | -------------------------------------------------------------------------------- /test/fixtures/import-builtin.js: -------------------------------------------------------------------------------- 1 | import { format } from 'util'; 2 | console.log(format('%d %s', 2, 'cool')); 3 | -------------------------------------------------------------------------------- /test/fixtures/pkg-fields-missing-browser/main.js: -------------------------------------------------------------------------------- 1 | export function test () { 2 | return 'hello main'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/pkg-fields-missing-browser/module.mjs: -------------------------------------------------------------------------------- 1 | export function test () { 2 | return 'hello mjs'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/pkg-fields-missing-module/main.js: -------------------------------------------------------------------------------- 1 | export function test () { 2 | return 'hello main'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/import-dynamic.js: -------------------------------------------------------------------------------- 1 | // This is a URL from root of module 2 | import('./test/fixtures/vanilla.js'); 3 | -------------------------------------------------------------------------------- /test/fixtures/import-wildcard.js: -------------------------------------------------------------------------------- 1 | import * as util from 'util'; 2 | console.log(util.format('%d %s', 2, 'cool')); 3 | -------------------------------------------------------------------------------- /test/fixtures/pkg-fields-missing-module/browser.js: -------------------------------------------------------------------------------- 1 | export function test () { 2 | return 'hello browser'; 3 | } 4 | -------------------------------------------------------------------------------- /test/fixtures/import-contrast.js: -------------------------------------------------------------------------------- 1 | import { hex } from 'wcag-contrast'; 2 | console.log(Math.floor(hex('#fff', '#ff0000'))); 3 | -------------------------------------------------------------------------------- /test/fixtures/esm-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm-module", 3 | "main": "./index.js", 4 | "browser": "./index.mjs" 5 | } -------------------------------------------------------------------------------- /test/fixtures/require-contrast.js: -------------------------------------------------------------------------------- 1 | const { hex } = require('wcag-contrast'); 2 | console.log(Math.floor(hex('#fff', '#ff0000'))); 3 | -------------------------------------------------------------------------------- /test/fixtures/import-json.js: -------------------------------------------------------------------------------- 1 | import json from './test.json'; 2 | const json2 = require('./test.json'); 3 | console.log(json.foo, json2.foo); 4 | -------------------------------------------------------------------------------- /test/fixtures/pkg-fields-missing-browser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm-module", 3 | "main": "./main.js", 4 | "module": "./module.mjs" 5 | } -------------------------------------------------------------------------------- /test/fixtures/pkg-fields-missing-module/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm-module", 3 | "main": "./main.js", 4 | "browser": "./browser.js" 5 | } -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | *.log 4 | .DS_Store 5 | bundle.js 6 | test 7 | test.js 8 | demo/ 9 | .npmignore 10 | LICENSE.md -------------------------------------------------------------------------------- /test/fixtures/export-default-object.mjs: -------------------------------------------------------------------------------- 1 | import { format } from 'util'; 2 | 3 | export const foo = 'bar'; 4 | 5 | console.log(format('%s%s', 'hello', foo)); 6 | -------------------------------------------------------------------------------- /test/fixtures/pkg-fields/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esm-module", 3 | "main": "./main.js", 4 | "module": "./module.mjs", 5 | "browser": "./browser.js" 6 | } -------------------------------------------------------------------------------- /test/fixtures/import-with-brfs.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | console.log(fs.readFileSync(path.resolve(__dirname, 'shader.vert'), 'utf-8')) -------------------------------------------------------------------------------- /test/fixtures/import-with-glslify.js: -------------------------------------------------------------------------------- 1 | import { format } from 'util'; 2 | import glslify from 'glslify'; 3 | 4 | console.log(format('shader: %s', glslify('void main () {}'))); 5 | -------------------------------------------------------------------------------- /test/fixtures/import-with-glslify-2.js: -------------------------------------------------------------------------------- 1 | import { format } from 'util'; 2 | import glslify from 'glslify'; 3 | import path from 'path'; 4 | 5 | console.log(format('shader2: %s', glslify(path.resolve(__dirname, 'shader.vert')))); 6 | -------------------------------------------------------------------------------- /test/fixtures/no-import-with-syntax.js: -------------------------------------------------------------------------------- 1 | const obj = { 2 | ...{ other: 2 }, 3 | foo: 'bar' 4 | }; 5 | 6 | async function* agf() { 7 | await 1; 8 | } 9 | 10 | const a = async () => {}; 11 | 12 | import('./foo.js'); 13 | 14 | // this will trigger our naive transform! 15 | console.log('export'); 16 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2017 Matt DesLauriers 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is 9 | furnished to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 18 | DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 19 | OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE 20 | OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /resolve.js: -------------------------------------------------------------------------------- 1 | const browserResolve = require('browser-resolve'); 2 | const nodeResolve = require('resolve'); 3 | 4 | module.exports = function (id, opts, cb) { 5 | if (typeof opts === 'function') { 6 | cb = opts; 7 | opts = {}; 8 | } 9 | opts = opts || {}; 10 | 11 | const mainFields = opts.mainFields || [ 'browser', 'module', 'main' ]; 12 | const isBrowserResolve = mainFields.includes('browser'); 13 | const resolve = isBrowserResolve ? browserResolve : nodeResolve; 14 | 15 | const packageFilter = opts.packageFilter; 16 | opts = Object.assign({}, opts, { 17 | extensions: [ '.mjs', '.js' ], 18 | packageFilter: function (info, pkgdir) { 19 | if (packageFilter) info = packageFilter(info, pkgdir); 20 | 21 | const key = isBrowserResolve ? 'browser' : 'main'; 22 | for (let i = 0; i < mainFields.length; i++) { 23 | const target = mainFields[i]; 24 | let replacement = info[target]; 25 | 26 | // Special case to handle legacy browserify, taken from node-browser-resolve 27 | if (!replacement && target === 'browser' && typeof info.browserify === 'string') { 28 | replacement = info.browserify; 29 | } 30 | 31 | // We have a replacement, stop searching and assign it 32 | if (replacement) { 33 | info[key] = replacement; 34 | break; 35 | } 36 | 37 | // Otherwise we look for the next field... 38 | } 39 | return info; 40 | } 41 | }); 42 | delete opts.mainFields; 43 | return resolve(id, opts, cb); 44 | }; 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "esmify", 3 | "version": "2.1.1", 4 | "description": "parse and handle import/export for browserify", 5 | "main": "./esmify.js", 6 | "license": "MIT", 7 | "author": { 8 | "name": "Matt DesLauriers", 9 | "email": "dave.des@gmail.com", 10 | "url": "https://github.com/mattdesl" 11 | }, 12 | "dependencies": { 13 | "@babel/core": "^7.2.2", 14 | "@babel/plugin-syntax-async-generators": "^7.2.0", 15 | "@babel/plugin-syntax-dynamic-import": "^7.2.0", 16 | "@babel/plugin-syntax-object-rest-spread": "^7.2.0", 17 | "@babel/plugin-transform-modules-commonjs": "^7.2.0", 18 | "babel-plugin-import-to-require": "^1.0.0", 19 | "cached-path-relative": "^1.0.2", 20 | "concat-stream": "^1.6.2", 21 | "duplexer2": "^0.1.4", 22 | "through2": "^2.0.5" 23 | }, 24 | "devDependencies": { 25 | "brfs": "^2.0.1", 26 | "browserify": "^16.2.3", 27 | "budo": "^11.6.0", 28 | "esm": "^3.1.4", 29 | "glslify": "^6.4.1", 30 | "loud-rejection": "^1.6.0", 31 | "tape": "^4.9.2", 32 | "wcag-contrast": "1.2.0" 33 | }, 34 | "scripts": { 35 | "test": "tape test/test-*.js" 36 | }, 37 | "keywords": [ 38 | "browserify", 39 | "es", 40 | "esm", 41 | "es6", 42 | "import", 43 | "export", 44 | "require", 45 | "commonjs", 46 | "umd", 47 | "cjs", 48 | "es2015" 49 | ], 50 | "repository": { 51 | "type": "git", 52 | "url": "git://github.com/mattdesl/esmify.git" 53 | }, 54 | "homepage": "https://github.com/mattdesl/esmify", 55 | "bugs": { 56 | "url": "https://github.com/mattdesl/esmify/issues" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /test/test-syntax.js: -------------------------------------------------------------------------------- 1 | require('loud-rejection')(); 2 | const test = require('tape'); 3 | const path = require('path'); 4 | const browserify = require('browserify'); 5 | 6 | const run = (file, opt = {}) => { 7 | return new Promise((resolve, reject) => { 8 | browserify(path.resolve(__dirname, file), { 9 | plugin: [ 10 | [ require('../'), opt.plugin || {} ] 11 | ], 12 | ...opt.browserify 13 | }).bundle((err, src) => { 14 | if (err) { 15 | return reject(err); 16 | } 17 | resolve(src.toString()); 18 | }); 19 | }); 20 | }; 21 | 22 | test('should not bail on fancy new syntax', async t => { 23 | t.plan(1); 24 | try { 25 | const result = await run('./fixtures/no-import-with-syntax.js'); 26 | t.equal(result.trim(), ` 27 | (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module \'"+i+"\'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i {}; 41 | 42 | import('./foo.js'); // this will trigger our naive transform! 43 | 44 | console.log('export'); 45 | 46 | },{}]},{},[1]); 47 | `.trim()); 48 | } catch (err) { 49 | t.fail(err); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /test/test-resolve.js: -------------------------------------------------------------------------------- 1 | require('loud-rejection')(); 2 | const test = require('tape'); 3 | const path = require('path'); 4 | const resolve = require('../resolve'); 5 | 6 | const fixtures = path.resolve(__dirname, 'fixtures'); 7 | 8 | const run = (mainFields, expected, parent = 'pkg-fields') => { 9 | const defaultFields = [ 'browser', 'module', 'main' ]; 10 | test(`should handle ${(mainFields || defaultFields).join(', ')}`, t => { 11 | t.plan(1); 12 | resolve('./' + parent, { 13 | mainFields, 14 | basedir: fixtures 15 | }, (err, file) => { 16 | if (err) return t.fail(err); 17 | t.equal(file, path.resolve(fixtures, `${parent}/${expected}`)); 18 | }); 19 | }); 20 | }; 21 | 22 | run(undefined, 'browser.js'); // default values 23 | 24 | // kinda dumb but just permute each.... 25 | 26 | run([ 'browser', 'module', 'main' ], 'browser.js'); 27 | run([ 'browser', 'main', 'module' ], 'browser.js'); 28 | run([ 'browser', 'module' ], 'browser.js'); 29 | run([ 'browser', 'main' ], 'browser.js'); 30 | run([ 'browser' ], 'browser.js'); 31 | 32 | run([ 'module', 'browser', 'main' ], 'module.mjs'); 33 | run([ 'module', 'main', 'module' ], 'module.mjs'); 34 | run([ 'module', 'browser' ], 'module.mjs'); 35 | run([ 'module', 'main' ], 'module.mjs'); 36 | run([ 'module' ], 'module.mjs'); 37 | 38 | run([ 'main', 'browser', 'module' ], 'main.js'); 39 | run([ 'main', 'module', 'browser' ], 'main.js'); 40 | run([ 'main', 'browser' ], 'main.js'); 41 | run([ 'main', 'module' ], 'main.js'); 42 | 43 | // where some are missing 44 | run([ 'main', 'browser', 'module' ], 'main.js', 'pkg-fields-missing-browser'); 45 | run([ 'module', 'browser', 'main' ], 'module.mjs', 'pkg-fields-missing-browser'); 46 | run([ 'browser', 'module', 'main' ], 'module.mjs', 'pkg-fields-missing-browser'); 47 | run([ 'browser', 'main', 'module' ], 'main.js', 'pkg-fields-missing-browser'); 48 | 49 | run([ 'browser', 'module', 'main' ], 'browser.js', 'pkg-fields-missing-module'); 50 | run([ 'module', 'main' ], 'main.js', 'pkg-fields-missing-module'); 51 | run([ 'main', 'module' ], 'main.js', 'pkg-fields-missing-module'); 52 | run([ 'main', 'browser' ], 'main.js', 'pkg-fields-missing-module'); 53 | 54 | test('should handle main only', t => { 55 | t.plan(1); 56 | resolve('./', { 57 | basedir: path.resolve(__dirname, '../') 58 | }, (err, file) => { 59 | if (err) return t.fail(err); 60 | t.equal(file, require.resolve('../')); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /test/test-transform.js: -------------------------------------------------------------------------------- 1 | require('loud-rejection')(); 2 | const test = require('tape'); 3 | const path = require('path'); 4 | const vm = require('vm'); 5 | const browserify = require('browserify'); 6 | 7 | const run = async (file) => { 8 | const bundle = await new Promise((resolve, reject) => { 9 | browserify(path.resolve(__dirname, file), { 10 | transform: [ 11 | [ require('../transform'), { global: true } ] 12 | ] 13 | }).bundle((err, src) => { 14 | if (err) { 15 | return reject(err); 16 | } 17 | resolve(src.toString()); 18 | }); 19 | }); 20 | 21 | return new Promise(resolve => { 22 | vm.runInNewContext(bundle, { 23 | console: { 24 | log: (...args) => resolve(args.join(' ')) 25 | } 26 | }); 27 | }); 28 | }; 29 | 30 | test('should handle default export', async t => { 31 | t.plan(1); 32 | try { 33 | const result = await run('./fixtures/import-default'); 34 | t.equal(result, 'foo'); 35 | } catch (err) { 36 | t.fail(err); 37 | } 38 | }); 39 | 40 | test('should ignore JSON', async t => { 41 | t.plan(1); 42 | try { 43 | const result = await run('./fixtures/import-json.js'); 44 | t.equal(result, 'bar bar'); 45 | } catch (err) { 46 | t.fail(err); 47 | } 48 | }); 49 | 50 | test('should handle named export', async t => { 51 | t.plan(1); 52 | try { 53 | const result = await run('./fixtures/import-named'); 54 | t.equal(result, 'baz'); 55 | } catch (err) { 56 | t.fail(err); 57 | } 58 | }); 59 | 60 | test('should handle "esm" authored module using CJS', async t => { 61 | t.plan(1); 62 | try { 63 | const result = await run('./fixtures/import-esm-with-cjs'); 64 | t.equal(result, 'hello'); 65 | } catch (err) { 66 | t.fail(err); 67 | } 68 | }); 69 | 70 | test('should handle "esm" authored module using ES6', async t => { 71 | t.plan(1); 72 | try { 73 | const result = await run('./fixtures/import-esm'); 74 | t.equal(result, 'hello'); 75 | } catch (err) { 76 | t.fail(err); 77 | } 78 | }); 79 | 80 | test('should handle wildcard import', async t => { 81 | t.plan(1); 82 | try { 83 | const result = await run('./fixtures/import-wildcard'); 84 | t.equal(result, '2 cool'); 85 | } catch (err) { 86 | t.fail(err); 87 | } 88 | }); 89 | 90 | // Test doesn't yet work in Node.js but this at least works in browser. 91 | // test('should not fail on dynamic import', async t => { 92 | // t.plan(1); 93 | // try { 94 | // const result = await run('./fixtures/import-dynamic'); 95 | // t.equal(result, '2 cool'); 96 | // } catch (err) { 97 | // t.fail(err); 98 | // } 99 | // }); 100 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # esmify 2 | 3 | A dead-simple tool to add `import` / `export` ES Module syntax to your [browserify](https://www.npmjs.com/package/browserify) builds. 4 | 5 | The plugin makes the following changes to your bundler: 6 | 7 | - Adds `.mjs` extension to module resolution (which take precedence over `.js` files) 8 | - Resolves to `"module"` field in `package.json` when a `"browser"` field is not specified 9 | - Transforms ES Module syntax (static `import` / `export` statements) into CommonJS 10 | 11 | Use it with the `--plugin` or `-p` flags in browserify: 12 | 13 | ```js 14 | browserify index.js -p esmify > bundle.js 15 | ``` 16 | 17 | Also works with [budo](https://www.npmjs.com/package/budo) and similar tools, for example: 18 | 19 | ```js 20 | budo index.js --live -- -p esmify 21 | ``` 22 | 23 | Files that don't contain `import` / `export` syntax are ignored, as are dynamic import expressions. The plugin runs across your bundle (including `node_modules`) in order to support ESM-authored modules on npm. 24 | 25 | ## Install 26 | 27 | Use [npm](https://npmjs.com/) to install. 28 | 29 | ```sh 30 | npm install esmify --save-dev 31 | ``` 32 | 33 | Also can be used via API like so: 34 | 35 | ```js 36 | browserify({ 37 | plugin: [ 38 | [ require('esmify'), { /* ... options ... */ } ] 39 | ] 40 | }); 41 | ``` 42 | 43 | ## Usage 44 | 45 | #### `plugin = esmify(bundler, opt = {})` 46 | 47 | Returns a browswerify plugin function that operates on `bundler` with the given options: 48 | 49 | - `mainFields` which describes the order of importance of fields in package.json resolution, defaults to `[ 'browser', 'module', 'main' ]` 50 | - `nodeModules` (default `true`) to disable the transform on your `node_modules` tree, set this to `false`. This will speed up bundling but you may run into issues when trying to import ESM-published code from npm. 51 | - `plainImports` (Experimental) this feature will map named imports *directly* to their CommonJS counterparts, without going through Babel's inter-op functions. This is generally needed for static analysis of `fs`, `path` and other tools like `glslify` in browserify. Defaults to `[ 'fs', 'path', 'glslify' ]`. 52 | 53 | Under the hood, this uses Babel and `plugin-transform-modules-commonjs` to provide robust inter-op that handles a variety of use cases. 54 | 55 | #### `require('esmify/resolve')(id, opts, cb)` 56 | 57 | Resolve the given `id` using the module resolution algorithm from `esmify`, accepting `{ mainFields }` array to opts as well as other options passed to [resolve](https://www.npmjs.com/package/resolve) and [browser-resolve](https://www.npmjs.com/package/browser-resolve). 58 | 59 | Works like so: 60 | 61 | - If mainFields includes a `"browser"` field, use `browser-resolve`, otherwise use `resolve` 62 | - Look for package.json fields in order of `mainFields`, the first field that exists will be used 63 | 64 | ## License 65 | 66 | MIT, see [LICENSE.md](http://github.com/mattdesl/esmify/blob/master/LICENSE.md) for details. 67 | -------------------------------------------------------------------------------- /transform.js: -------------------------------------------------------------------------------- 1 | const babel = require('@babel/core'); 2 | const through = require('through2'); 3 | const { PassThrough } = require('stream'); 4 | const path = require('path'); 5 | const concat = require('concat-stream'); 6 | const duplexer = require('duplexer2'); 7 | 8 | const pluginDynamicImport = require('@babel/plugin-syntax-dynamic-import'); 9 | const pluginCJS = require('@babel/plugin-transform-modules-commonjs'); 10 | const pluginImportToRequire = require('babel-plugin-import-to-require'); 11 | 12 | // Gotta add these as well so babel doesn't bail out when it sees new syntax 13 | const pluginSyntaxRestSpread = require('@babel/plugin-syntax-object-rest-spread'); 14 | const pluginSyntaxGenerator = require('@babel/plugin-syntax-async-generators'); 15 | 16 | module.exports = createTransform(); 17 | module.exports.createTransform = createTransform; 18 | 19 | function createTransform (babelOpts = {}) { 20 | return function babelify (file, opts = {}) { 21 | const ext = path.extname(file); 22 | if (!babel.DEFAULT_EXTENSIONS.includes(ext)) { 23 | return new PassThrough(); 24 | } 25 | 26 | if (typeof babelOpts.filterFile === 'function') { 27 | if (!babelOpts.filterFile(file, opts)) { 28 | return new PassThrough(); 29 | } 30 | } 31 | 32 | if (babelOpts.logFile) console.log('Checking', file); 33 | 34 | const output = through(); 35 | const stream = duplexer(concat(code => { 36 | code = code.toString(); 37 | 38 | let isFilterAccept = true; 39 | if (typeof babelOpts.filterSource === 'function') { 40 | isFilterAccept = babelOpts.filterSource(code, file, opts); 41 | } 42 | 43 | // Skip files that don't use ES6 import/export syntax 44 | if (!isFilterAccept || !/\b(import|export)\b/g.test(code)) { 45 | output.push(code); 46 | output.push(null); 47 | return; 48 | } 49 | 50 | let plainImports = [].concat(babelOpts.plainImports).filter(Boolean); 51 | 52 | const settings = Object.assign({}, babelOpts, { 53 | babelrc: false, 54 | sourceMaps: 'inline', 55 | plugins: [ 56 | pluginSyntaxRestSpread, 57 | pluginSyntaxGenerator, 58 | plainImports.length > 0 59 | ? [ pluginImportToRequire, { modules: plainImports } ] 60 | : false, 61 | pluginDynamicImport, 62 | pluginCJS 63 | ].filter(Boolean), 64 | filename: file 65 | }); 66 | 67 | if (babelOpts.logFile) console.log('Transforming', file); 68 | 69 | delete settings.filterFile; 70 | delete settings.filterSource; 71 | delete settings.logFile; 72 | delete settings.plainImports; 73 | 74 | babel.transform(code, settings, (err, result) => { 75 | if (err) { 76 | stream.emit('error', err); 77 | } else { 78 | output.push(result.code); 79 | } 80 | output.push(null); 81 | }); 82 | }), output); 83 | return stream; 84 | }; 85 | } 86 | -------------------------------------------------------------------------------- /esmify.js: -------------------------------------------------------------------------------- 1 | const resolve = require('./resolve'); 2 | const { createTransform } = require('./transform'); 3 | const path = require('path'); 4 | const through = require('through2'); 5 | const relativePath = require('cached-path-relative'); 6 | 7 | module.exports = function (bundler, pluginOpts = {}) { 8 | const cwd = pluginOpts.basedir || process.cwd(); 9 | const logFile = pluginOpts.logFile; 10 | let defaultMainField = [ 'browser', 'module', 'main' ]; 11 | 12 | // TODO: Consider a better way to handle this. 13 | // Babel's import inter-op breaks certain modules being able to statically 14 | // analyze require statements; for example brfs and glslify. 15 | // This hack/workaround will *directly* translate certain known CommonJS 16 | // modules without going through inter-op. 17 | const plainImports = [ 'fs', 'path', 'glslify' ]; 18 | 19 | // User is disabling browser-field 20 | if (bundler._options.browserField === false) { 21 | defaultMainField = defaultMainField.filter(d => d !== 'browser'); 22 | } 23 | 24 | // We need to add in the .mjs and make it take precedence over .js files 25 | const idx = bundler._extensions.indexOf('.mjs'); 26 | if (idx >= 0) bundler._extensions.splice(idx, 1); 27 | bundler._extensions.unshift('.mjs'); 28 | 29 | const mainFields = pluginOpts.mainFields || defaultMainField; 30 | 31 | // Utility -> true if path is a top-level node_modules (i.e. not in source) 32 | const isNodeModule = (file, cwd) => { 33 | const dir = path.dirname(file); 34 | const relative = relativePath(cwd, dir); 35 | return relative.startsWith(`node_modules${path.sep}`); 36 | }; 37 | 38 | // Patch browserify resolve algorithm 39 | bundler._bresolve = function (id, opts, cb) { 40 | opts = Object.assign({}, opts, { 41 | mainFields, 42 | basedir: opts.basedir || path.dirname(opts.filename) 43 | }); 44 | return resolve(id, opts, (err, result, pkg) => { 45 | if (err) { 46 | // Provide cleaner error messaging for end-user 47 | return cb(new Error(`Cannot find module '${id}' from '${path.relative(cwd, opts.filename)}'`)); 48 | } else { 49 | cb(null, result, pkg); 50 | } 51 | }); 52 | }; 53 | 54 | // Insert esmify as the *initial* transform 55 | let firstRecord = true; 56 | bundler.pipeline.get('record').unshift(through.obj(function (chunk, enc, next) { 57 | if (firstRecord) { 58 | firstRecord = false; 59 | 60 | // We need two transforms to ensure they are run before all other browserify 61 | // transforms passed in via transform field and so forth. 62 | // 1st is a regular local transform 63 | this.push({ 64 | transform: createTransform({ plainImports, logFile, filterFile: file => !isNodeModule(file, cwd) }), 65 | global: false 66 | }); 67 | // 2nd is a global transform, but *only* running in node_modules, since 68 | // the above local transform already catches local files. 69 | if (pluginOpts.nodeModules !== false) { 70 | this.push({ 71 | transform: createTransform({ plainImports, logFile, filterFile: file => isNodeModule(file, cwd) }), 72 | global: true 73 | }); 74 | } 75 | 76 | next(null, chunk); 77 | } else { 78 | next(null, chunk); 79 | } 80 | })); 81 | }; 82 | -------------------------------------------------------------------------------- /test/test-plugin.js: -------------------------------------------------------------------------------- 1 | require('loud-rejection')(); 2 | const test = require('tape'); 3 | const path = require('path'); 4 | const vm = require('vm'); 5 | const browserify = require('browserify'); 6 | 7 | const run = async (file, opt = {}) => { 8 | const bundle = await new Promise((resolve, reject) => { 9 | browserify(path.resolve(__dirname, file), { 10 | plugin: [ 11 | [ require('../'), opt.plugin || {} ] 12 | ], 13 | ...opt.browserify 14 | }).bundle((err, src) => { 15 | if (err) { 16 | return reject(err); 17 | } 18 | resolve(src.toString()); 19 | }); 20 | }); 21 | 22 | return new Promise(resolve => { 23 | vm.runInNewContext(bundle, { 24 | console: { 25 | log: (...args) => resolve(args.join(' ')) 26 | } 27 | }); 28 | }); 29 | }; 30 | 31 | test('should ESM import builtins', async t => { 32 | t.plan(1); 33 | try { 34 | const result = await run('./fixtures/import-builtin.js'); 35 | t.equal(result, '2 cool'); 36 | } catch (err) { 37 | t.fail(err); 38 | } 39 | }); 40 | 41 | test('should ESM import with transforms', async t => { 42 | t.plan(1); 43 | try { 44 | const result = await run('./fixtures/import-with-glslify.js', { 45 | browserify: { 46 | transform: [ 'glslify' ] 47 | } 48 | }); 49 | t.equal(result, 'shader: #define GLSLIFY 1\nvoid main () {}'); 50 | } catch (err) { 51 | t.fail(err); 52 | } 53 | }); 54 | 55 | test('should ESM import with brfs', async t => { 56 | t.plan(1); 57 | try { 58 | const result = await run('./fixtures/import-with-brfs.js', { 59 | browserify: { 60 | transform: [ 'brfs' ] 61 | } 62 | }); 63 | t.equal(result, 'void main () { /* test */ }'); 64 | } catch (err) { 65 | t.fail(err); 66 | } 67 | }); 68 | 69 | test('should ESM import with transforms', async t => { 70 | t.plan(1); 71 | try { 72 | const result = await run('./fixtures/import-with-glslify-2.js', { 73 | browserify: { 74 | transform: [ 'glslify' ] 75 | } 76 | }); 77 | t.equal(result, 'shader2: #define GLSLIFY 1\nvoid main () { /* test */ }'); 78 | } catch (err) { 79 | t.fail(err); 80 | } 81 | }); 82 | 83 | test('should handle browser field by default from node', async t => { 84 | t.plan(1); 85 | try { 86 | const result = await run('./fixtures/import-pkg-field'); 87 | t.equal(result, 'hello browser'); 88 | } catch (err) { 89 | t.fail(err); 90 | } 91 | }); 92 | 93 | test('should handle browserify --node option', async t => { 94 | t.plan(1); 95 | try { 96 | const result = await run('./fixtures/import-pkg-field', { 97 | browserify: { 98 | node: true 99 | } 100 | }); 101 | t.equal(result, 'hello mjs'); 102 | } catch (err) { 103 | t.fail(err); 104 | } 105 | }); 106 | 107 | test('should handle mainFields option', async t => { 108 | t.plan(1); 109 | try { 110 | const result = await run('./fixtures/import-pkg-field', { 111 | plugin: { 112 | mainFields: [ 'module', 'main' ] 113 | } 114 | }); 115 | t.equal(result, 'hello mjs'); 116 | } catch (err) { 117 | t.fail(err); 118 | } 119 | }); 120 | 121 | test('should handle mjs by default', async t => { 122 | t.plan(1); 123 | try { 124 | const result = await run('./fixtures/export-default-object'); 125 | t.equal(result, 'hellobar'); 126 | } catch (err) { 127 | t.fail(err); 128 | } 129 | }); 130 | 131 | test('the js points elsewhere', async t => { 132 | t.plan(1); 133 | try { 134 | const result = await run('./fixtures/export-default-object.js'); 135 | t.equal(result, 'invalid'); 136 | } catch (err) { 137 | t.fail(err); 138 | } 139 | }); 140 | 141 | test('imports wcag-contrast', async t => { 142 | t.plan(1); 143 | try { 144 | const result = await run('./fixtures/import-contrast.js'); 145 | t.equal(result, '3'); 146 | } catch (err) { 147 | t.fail(err); 148 | } 149 | }); 150 | 151 | test('require()s wcag-contrast', async t => { 152 | t.plan(1); 153 | try { 154 | const result = await run('./fixtures/require-contrast.js'); 155 | t.equal(result, '3'); 156 | } catch (err) { 157 | t.fail(err); 158 | } 159 | }); 160 | --------------------------------------------------------------------------------