From 194cc802fe493b54324f0d729668c323ab6e999a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Sun, 24 Dec 2017 14:14:17 +0100 Subject: [PATCH 1/5] Transform ES modules to CommonJS before outputting --- esm.js | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++ index.js | 3 +- package.json | 3 ++ 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 esm.js diff --git a/esm.js b/esm.js new file mode 100644 index 0000000..a921655 --- /dev/null +++ b/esm.js @@ -0,0 +1,100 @@ +var acorn = require('acorn'); +var assignParent = require('estree-assign-parent'); +var scan = require('scope-analyzer'); +var through = require('through2'); + +module.exports = function esm () { + return through.obj(function (row, enc, cb) { + if (!row.esm) { + cb(null, row); + return; + } + + var ast = acorn.parse(row.source, { sourceType: 'module' }); + assignParent(ast); + scan.analyze(ast); + + var scope = scan.scope(ast); + + var esmDefaultName = '_esmDefault' + var patches = []; + ast.body.forEach(function (node) { + if (node.type === 'ExportDefaultDeclaration') { + if (node.declaration.id) { + esmDefaultName = node.declaration.id.name + } + patches.push({ + start: node.start, + end: node.declaration.start, + string: node.declaration.id ? '' : 'var _esmDefault = ' + }); + } + if (node.type === 'ImportDeclaration') { + patches.push({ start: node.start, end: node.end, string: '' }); + } + }); + + + var setup = '' + if (row.esm) { + if (row.esm.exports.length > 0) { + setup += 'function _esmSet(){throw new Error(\'Assignment to constant variable.\')}Object.defineProperties(exports, [' + row.esm.exports.forEach(function (record, i) { + if (i > 0) setup += ',' + if (record.name === 'default' && !record.as && !record.export) { + setup += '{key:"default",get:function(){return ' + esmDefaultName + '},set:_esmSet}' + } else { + setup += '{key:' + JSON.stringify(record.as) + ',get:function(){return ' + record.export + '},set:_esmSet}' + } + }); + setup += ']);' + } + + var needInterop = false; + var baseImports = {}; + row.esm.imports.forEach(function (record, i) { + var binding = scope.getBinding(record.as); + if (!record.esm) { + if (record.import !== 'default') { + throw new Error('The requested module does not provide an export named \'' + record.import + '\'') + } + setup += 'var ' + record.as + ' = ' + 'require(' + JSON.stringify(record.from) + ');'; + return; + } + + var base = baseImports[record.from]; + if (!base) { + base = baseImports[record.from] = '_esmImport' + i; + setup += 'var ' + base + ' = require(' + JSON.stringify(record.from) + ');'; + } + binding.references.forEach(function (ref, i) { + if (ref === binding.definition) return + patches.push({ + start: ref.start, + end: ref.end, + string: base + '.' + record.import + }); + }); + }); + } + + row.source = patch(row.source, patches) + row.source = setup + '\n' + row.source + cb(null, row); + }); +}; + +function patch (str, patches) { + patches = patches.slice().sort(function (a, b) { return a.start - b.start }) + + var offset = 0 + patches.forEach(function (r) { + var start = r.start + offset + var end = r.end + offset + str = str.slice(0, start) + r.string + str.slice(end) + + offset += r.start - r.end + r.string.length + }) + + return str +} diff --git a/index.js b/index.js index 3b96de8..c7af24f 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,7 @@ var fs = require('fs'); var path = require('path'); var combineSourceMap = require('combine-source-map'); +var esm = require('./esm'); var defaultPreludePath = path.join(__dirname, '_prelude.js'); var defaultPrelude = fs.readFileSync(defaultPreludePath, 'utf8'); @@ -25,7 +26,7 @@ module.exports = function (opts) { function (buf, enc, next) { parser.write(buf); next() }, function () { parser.end() } ); - parser.pipe(through.obj(write, end)); + parser.pipe(esm()).pipe(through.obj(write, end)); stream.standaloneModule = opts.standaloneModule; stream.hasExports = opts.hasExports; diff --git a/package.json b/package.json index 5bf686f..579699a 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,11 @@ }, "dependencies": { "JSONStream": "^1.0.3", + "acorn": "^5.2.1", "combine-source-map": "~0.8.0", "defined": "^1.0.0", + "estree-assign-parent": "^1.0.0", + "scope-analyzer": "^1.0.0", "through2": "^2.0.0", "umd": "^3.0.0" }, From b17c178a381021700f4f6c0542b39fd458acc475 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 26 Dec 2017 12:56:06 +0100 Subject: [PATCH 2/5] temp add prelude for installs from git --- _prelude.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 _prelude.js diff --git a/_prelude.js b/_prelude.js new file mode 100644 index 0000000..4ab156f --- /dev/null +++ b/_prelude.js @@ -0,0 +1 @@ +(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o Date: Tue, 26 Dec 2017 13:50:20 +0100 Subject: [PATCH 3/5] add esm test --- esm.js | 18 ++++++++++--- package.json | 2 +- test/esm.js | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 5 deletions(-) create mode 100644 test/esm.js diff --git a/esm.js b/esm.js index a921655..d853ac1 100644 --- a/esm.js +++ b/esm.js @@ -29,6 +29,15 @@ module.exports = function esm () { string: node.declaration.id ? '' : 'var _esmDefault = ' }); } + if (node.type === 'ExportNamedDeclaration') { + if (node.declaration) { + // Only erase the `export` bit before `export var` + patches.push({ start: node.start, end: node.declaration.start, string: '' }) + } else { + // Erase the entire declaration + patches.push({ start: node.start, end: node.end, string: '' }) + } + } if (node.type === 'ImportDeclaration') { patches.push({ start: node.start, end: node.end, string: '' }); } @@ -38,16 +47,17 @@ module.exports = function esm () { var setup = '' if (row.esm) { if (row.esm.exports.length > 0) { - setup += 'function _esmSet(){throw new Error(\'Assignment to constant variable.\')}Object.defineProperties(exports, [' + // Define getters for exports to support live bindings, and to prevent writes to them from outside this module + setup += 'function _esmSet(){throw new Error(\'Assignment to constant variable.\')}Object.defineProperties(exports, {' row.esm.exports.forEach(function (record, i) { if (i > 0) setup += ',' if (record.name === 'default' && !record.as && !record.export) { - setup += '{key:"default",get:function(){return ' + esmDefaultName + '},set:_esmSet}' + setup += 'default:{get:function(){return ' + esmDefaultName + '},set:_esmSet,enumerable:true}' } else { - setup += '{key:' + JSON.stringify(record.as) + ',get:function(){return ' + record.export + '},set:_esmSet}' + setup += JSON.stringify(record.as) + ':{get:function(){return ' + record.export + '},set:_esmSet,enumerable:true}' } }); - setup += ']);' + setup += '});' } var needInterop = false; diff --git a/package.json b/package.json index 579699a..22cc0f0 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "combine-source-map": "~0.8.0", "defined": "^1.0.0", "estree-assign-parent": "^1.0.0", - "scope-analyzer": "^1.0.0", + "scope-analyzer": "^1.1.1", "through2": "^2.0.0", "umd": "^3.0.0" }, diff --git a/test/esm.js b/test/esm.js new file mode 100644 index 0000000..8181857 --- /dev/null +++ b/test/esm.js @@ -0,0 +1,71 @@ +var test = require('tap').test; +var pack = require('../'); +var vm = require('vm'); + +test('esm', function (t) { + t.plan(3); + + var p = pack({ raw: true }); + var src = ''; + p.on('data', function (buf) { src += buf; }); + p.on('end', function () { + var expected = [ + ['type', 'function'], + ['value', 0], + ['value', 5] + ] + vm.runInNewContext(src, { + setTimeout: setTimeout, + setInterval: setInterval, + clearInterval: clearInterval, + console: { log: log } + }); + function log (a, b) { + var cur = expected.shift() + t.ok(a === cur[0] && b === cur[1]) + } + }); + + p.write({ + id: 'abc', + entry: true, + esm: { + imports: [ + { from: 'x', import: 'default', as: 'x', esm: true }, + { from: 'y', import: 'default', as: 'y' }, + { from: 'z', import: 'c', as: 'renamed', esm: true }, + { from: 'z', import: 'x', as: 'i', esm: true } + ], + exports: [] + }, + source: 'import x from "x"; import y from "y"; import { c as renamed, x as i } from "z"; y.b(x); x(renamed); setTimeout(function(){ x(renamed); clearInterval(i) }, 50)' + }) + // Test importing a CommonJS module + p.write({ + id: 'y', + source: 'exports.a = "a"; exports.b = function b () {console.log("type",typeof arguments[0])}' + }) + // Test live bindings + p.write({ + id: 'z', + esm: { + imports: [], + exports: [ + { export: 'c', as: 'c' }, + { export: 'x', as: 'x' } + ] + }, + source: 'export var c = 0; export var x = setInterval(function(){ c++ }, 10)' + }) + p.end({ + id: 'x', + esm: { + imports: [], + exports: [ + { export: 'c', as: 'default' } + ] + }, + source: 'export default c; function c(arg){ console.log("value", arg) }' + }); + +}); From e6cffae014b7f25c5d302bbae07f28ac4d843945 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 26 Dec 2017 14:13:46 +0100 Subject: [PATCH 4/5] unflake test --- package.json | 7 ++++--- test/esm.js | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 22cc0f0..15dad9d 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,12 @@ "umd": "^3.0.0" }, "devDependencies": { - "tap": "^10.7.2", - "uglify-js": "1.3.5", "concat-stream": "~1.5.1", "convert-source-map": "~1.1.0", - "parse-base64vlq-mappings": "~0.1.1" + "dedent": "^0.7.0", + "parse-base64vlq-mappings": "~0.1.1", + "tap": "^10.7.2", + "uglify-js": "1.3.5" }, "scripts": { "test": "tap test/*.js", diff --git a/test/esm.js b/test/esm.js index 8181857..4578534 100644 --- a/test/esm.js +++ b/test/esm.js @@ -1,4 +1,5 @@ var test = require('tap').test; +var dedent = require('dedent'); var pack = require('../'); var vm = require('vm'); @@ -15,9 +16,7 @@ test('esm', function (t) { ['value', 5] ] vm.runInNewContext(src, { - setTimeout: setTimeout, - setInterval: setInterval, - clearInterval: clearInterval, + T: t, console: { log: log } }); function log (a, b) { @@ -38,7 +37,15 @@ test('esm', function (t) { ], exports: [] }, - source: 'import x from "x"; import y from "y"; import { c as renamed, x as i } from "z"; y.b(x); x(renamed); setTimeout(function(){ x(renamed); clearInterval(i) }, 50)' + source: dedent` + import x from "x"; + import y from "y"; + import { c as renamed, x as i } from "z"; + y.b(x); + x(renamed); + i() + x(renamed); + ` }) // Test importing a CommonJS module p.write({ @@ -55,7 +62,7 @@ test('esm', function (t) { { export: 'x', as: 'x' } ] }, - source: 'export var c = 0; export var x = setInterval(function(){ c++ }, 10)' + source: 'export var c = 0; export var x = function(){ c += 5 }' }) p.end({ id: 'x', From de8593bd00c654e2a30c5ebaf7a18f9ae5a168df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Tue, 26 Dec 2017 14:14:18 +0100 Subject: [PATCH 5/5] remove commonjs names from ES modules, add ES modules runtime --- _prelude.js | 2 +- esm.js | 10 +++++----- index.js | 3 ++- prelude.js | 24 +++++++++++++++++++++--- test/esm.js | 13 ++++++++++++- 5 files changed, 41 insertions(+), 11 deletions(-) diff --git a/_prelude.js b/_prelude.js index 4ab156f..b37708e 100644 --- a/_prelude.js +++ b/_prelude.js @@ -1 +1 @@ -(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 0) { // Define getters for exports to support live bindings, and to prevent writes to them from outside this module - setup += 'function _esmSet(){throw new Error(\'Assignment to constant variable.\')}Object.defineProperties(exports, {' + setup += '_esmExport({' row.esm.exports.forEach(function (record, i) { if (i > 0) setup += ',' if (record.name === 'default' && !record.as && !record.export) { - setup += 'default:{get:function(){return ' + esmDefaultName + '},set:_esmSet,enumerable:true}' + setup += 'default:function(){return ' + esmDefaultName + '}' } else { - setup += JSON.stringify(record.as) + ':{get:function(){return ' + record.export + '},set:_esmSet,enumerable:true}' + setup += JSON.stringify(record.as) + ':function(){return ' + record.export + '}' } }); setup += '});' @@ -68,14 +68,14 @@ module.exports = function esm () { if (record.import !== 'default') { throw new Error('The requested module does not provide an export named \'' + record.import + '\'') } - setup += 'var ' + record.as + ' = ' + 'require(' + JSON.stringify(record.from) + ');'; + setup += 'var ' + record.as + ' = ' + '_esmRequire(' + JSON.stringify(record.from) + ');'; return; } var base = baseImports[record.from]; if (!base) { base = baseImports[record.from] = '_esmImport' + i; - setup += 'var ' + base + ' = require(' + JSON.stringify(record.from) + ');'; + setup += 'var ' + base + ' = _esmRequire(' + JSON.stringify(record.from) + ');'; } binding.references.forEach(function (ref, i) { if (ref === binding.definition) return diff --git a/index.js b/index.js index c7af24f..449d5f6 100644 --- a/index.js +++ b/index.js @@ -71,7 +71,7 @@ module.exports = function (opts) { (first ? '' : ','), JSON.stringify(row.id), ':[', - 'function(require,module,exports){\n', + row.esm ? 'function(_esmRequire,_esmExport){\n' : 'function(require,module,exports){\n', combineSourceMap.removeComments(row.source), '\n},', '{' + Object.keys(row.deps || {}).sort().map(function (key) { @@ -79,6 +79,7 @@ module.exports = function (opts) { + JSON.stringify(row.deps[key]) ; }).join(',') + '}', + row.esm ? ',1' : '', ']' ].join(''); diff --git a/prelude.js b/prelude.js index d291c69..bf83991 100644 --- a/prelude.js +++ b/prelude.js @@ -29,11 +29,29 @@ err.code = 'MODULE_NOT_FOUND'; throw err; } - var m = cache[name] = {exports:{}}; - modules[name][0].call(m.exports, function(x){ + var e = {}, m = cache[name] = {exports:e}; + function subReq(x){ var id = modules[name][1][x]; return newRequire(id ? id : x); - },m,m.exports,outer,modules,cache,entry); + } + // If [2] is truthy this is an ES module. + // ES modules expose exports by calling a function with + // [name, getter (for live bindings)] pairs. + if(modules[name][2]) { + modules[name][0].call(undefined, subReq, function(obj) { + var exp = Object.keys(obj); + for (var i = 0; i < exp.length; i++) { + Object.defineProperty(e, exp[i], { + get: obj[exp[i]], + set: function () { throw new Error('Assignment to constant variable.') }, + enumerable: true + }); + } + }); + } + else { + modules[name][0].call(m.exports, subReq,m,m.exports,outer,modules,cache,entry); + } } return cache[name].exports; } diff --git a/test/esm.js b/test/esm.js index 4578534..a835d29 100644 --- a/test/esm.js +++ b/test/esm.js @@ -4,7 +4,7 @@ var pack = require('../'); var vm = require('vm'); test('esm', function (t) { - t.plan(3); + t.plan(6); var p = pack({ raw: true }); var src = ''; @@ -25,6 +25,17 @@ test('esm', function (t) { } }); + p.write({ + id: 'test', + entry: true, + esm: { imports: [], exports: [] }, + source: dedent` + T.equal(typeof require, 'undefined') + T.equal(typeof module, 'undefined') + T.equal(typeof exports, 'undefined') + ` + }); + p.write({ id: 'abc', entry: true,