From 5e29bc9ba2888e6abca4bf4bd4d797d202cc696f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 6 Apr 2018 13:42:31 +0200 Subject: [PATCH 01/12] Capture prototype --- index.js | 36 ++++++++++++++++++++++++++++++++++++ test.js | 13 +++++++++++++ test2.js | 2 ++ test3.js | 1 + 4 files changed, 52 insertions(+) create mode 100644 test.js create mode 100644 test2.js create mode 100644 test3.js diff --git a/index.js b/index.js index 8ce3afc..b87463d 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,7 @@ var path = require('path') var callerPath = require('caller-path') var resolvePath = require('resolve') +var asyncHooks = require('async_hooks') function attachCb (promise, cb) { if (cb) { @@ -14,16 +15,49 @@ function attachCb (promise, cb) { return promise } +var captureBundles = new Map() + +function capture (run, cb) { + var hooks = asyncHooks.createHook({ + init: function (asyncId, type, triggerAsyncId) { + if (captureBundles.has(triggerAsyncId)) { + captureBundles.set(asyncId, captureBundles.get(triggerAsyncId)) + } + require('fs').writeSync(2, '' + asyncId + ' ' + triggerAsyncId + ' ' + type + '\n') + }, + destroy: function (asyncId) { + captureBundles.delete(asyncId) + } + }) + + hooks.enable() + + var currentBundles = [] + captureBundles.set(asyncHooks.executionAsyncId(), currentBundles) + + var p = run(ondone) + if (p && p.then) p.then(function (result) { ondone(null, result) }, ondone) + + function ondone () { + hooks.disable() + cb(currentBundles) + } +} + module.exports = function load (filename, cb) { if (typeof filename === 'object' && filename._options) { return require('./plugin')(filename, cb) } + var currentBundles = captureBundles.get(asyncHooks.executionAsyncId()) + console.log('load', currentBundles) + var basedir = path.dirname(callerPath()) var resolved = new Promise(function (resolve, reject) { resolvePath(filename, { basedir: basedir }, function (err, fullpath) { if (err) return reject(err) + currentBundles.push(fullpath) resolve(fullpath) }) }) @@ -31,6 +65,8 @@ module.exports = function load (filename, cb) { return attachCb(resolved.then(require), cb) } +module.exports.capture = capture + Object.defineProperty(module.exports, 'createStream', { configurable: true, enumerable: true, diff --git a/test.js b/test.js new file mode 100644 index 0000000..21256d5 --- /dev/null +++ b/test.js @@ -0,0 +1,13 @@ +var sr = require('./') + +async function main () { + var val = await sr('./test2') + + console.log(val) +} + +sr.capture(function () { + return main() +}, function (bundles) { + console.log(bundles) +}) diff --git a/test2.js b/test2.js new file mode 100644 index 0000000..9d3fa05 --- /dev/null +++ b/test2.js @@ -0,0 +1,2 @@ +module.exports = Promise.resolve() + .then(() => require('./')('./test3')) diff --git a/test3.js b/test3.js new file mode 100644 index 0000000..ededdc5 --- /dev/null +++ b/test3.js @@ -0,0 +1 @@ +module.exports = 10 From 26d1e408ad3a58ec8b069bfcbe24bdb84cb93c3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 6 Apr 2018 14:14:16 +0200 Subject: [PATCH 02/12] with a test --- index.js | 22 +++++++++++++++++----- package.json | 6 ++++-- test.js | 13 ------------- test/capture.js | 34 ++++++++++++++++++++++++++++++++++ test/capture/app.js | 10 ++++++++++ test/capture/data1.js | 16 ++++++++++++++++ test/capture/data3.js | 1 + test/capture/json.json | 1 + test/capture/render.js | 9 +++++++++ test/capture/route1.js | 5 +++++ test/capture/route2.js | 13 +++++++++++++ test/capture/view1.js | 3 +++ test/capture/view2.js | 3 +++ test2.js | 2 -- test3.js | 1 - 15 files changed, 116 insertions(+), 23 deletions(-) delete mode 100644 test.js create mode 100644 test/capture.js create mode 100644 test/capture/app.js create mode 100644 test/capture/data1.js create mode 100644 test/capture/data3.js create mode 100644 test/capture/json.json create mode 100644 test/capture/render.js create mode 100644 test/capture/route1.js create mode 100644 test/capture/route2.js create mode 100644 test/capture/view1.js create mode 100644 test/capture/view2.js delete mode 100644 test2.js delete mode 100644 test3.js diff --git a/index.js b/index.js index b87463d..0d8b49b 100644 --- a/index.js +++ b/index.js @@ -20,10 +20,10 @@ var captureBundles = new Map() function capture (run, cb) { var hooks = asyncHooks.createHook({ init: function (asyncId, type, triggerAsyncId) { + // Inherit bundles list from the parent if (captureBundles.has(triggerAsyncId)) { captureBundles.set(asyncId, captureBundles.get(triggerAsyncId)) } - require('fs').writeSync(2, '' + asyncId + ' ' + triggerAsyncId + ' ' + type + '\n') }, destroy: function (asyncId) { captureBundles.delete(asyncId) @@ -33,14 +33,27 @@ function capture (run, cb) { hooks.enable() var currentBundles = [] - captureBundles.set(asyncHooks.executionAsyncId(), currentBundles) + var asyncId = asyncHooks.executionAsyncId() + captureBundles.set(asyncId, currentBundles) + + if (!cb) { + var promise = new Promise(function (resolve, reject) { + cb = function (err, result, bundles) { + if (err) reject(err) + else resolve({ result: result, bundles: bundles }) + } + }) + } var p = run(ondone) if (p && p.then) p.then(function (result) { ondone(null, result) }, ondone) - function ondone () { + return promise + + function ondone (err, result) { + captureBundles.delete(asyncId) // no memory leak hooks.disable() - cb(currentBundles) + cb(err, result, currentBundles) } } @@ -50,7 +63,6 @@ module.exports = function load (filename, cb) { } var currentBundles = captureBundles.get(asyncHooks.executionAsyncId()) - console.log('load', currentBundles) var basedir = path.dirname(callerPath()) var resolved = new Promise(function (resolve, reject) { diff --git a/package.json b/package.json index cb49fe2..ae55c76 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "run-series": "^1.1.4", "standard": "^10.0.3", "tap-diff": "^0.1.1", + "tap-min": "^1.2.2", "tape": "^4.8.0", "tape-run": "^3.0.4", "uglify-es": "^3.3.7" @@ -50,10 +51,11 @@ "url": "git+https://github.com/goto-bus-stop/split-require.git" }, "scripts": { - "test": "npm run test:lint && npm run test:tap && npm run test:browser", + "test": "npm run test:lint && npm run test:tap && npm run test:server && npm run test:browser", "test:lint": "standard", "test:tap": "tape test/test.js | tap-diff", - "test:browser": "browserify -p [ ./plugin --out ./test/browser/static ] -r ./browser:split-require test/browser | tape-run --static ./test/browser/static" + "test:server": "tape test/capture.js | tap-min", + "test:browser": "browserify -p [ ./plugin --out ./test/browser/static ] -r ./browser:split-require test/browser | tape-run --static ./test/browser/static | tap-diff" }, "standard": { "ignore": [ diff --git a/test.js b/test.js deleted file mode 100644 index 21256d5..0000000 --- a/test.js +++ /dev/null @@ -1,13 +0,0 @@ -var sr = require('./') - -async function main () { - var val = await sr('./test2') - - console.log(val) -} - -sr.capture(function () { - return main() -}, function (bundles) { - console.log(bundles) -}) diff --git a/test/capture.js b/test/capture.js new file mode 100644 index 0000000..0c7c5db --- /dev/null +++ b/test/capture.js @@ -0,0 +1,34 @@ +var test = require('tape') +var path = require('path') + +var expected = { + one: [ + path.join(__dirname, 'capture/view1.js'), + path.join(__dirname, 'capture/data1.js'), + path.join(__dirname, 'capture/data3.js') + ].sort(), + two: [ + path.join(__dirname, 'capture/view2.js'), + path.join(__dirname, 'capture/data3.js') + ].sort() +} + +test('capture', function (t) { + t.plan(200) + var app = require('./capture/app') + + // Ensure multiple concurrent renders don't interfere + // by just setting off a ton of them + for (var i = 0; i < 200; i++) { + var which = Math.random() > 0.5 ? 'one' : 'two' + queue(which) + } + + function queue (which) { + setTimeout(function () { + app(which).then(function (result) { + t.deepEqual(result.bundles.sort(), expected[which]) + }).catch(t.fail) + }, 4 + Math.floor(Math.random() * 10)) + } +}) diff --git a/test/capture/app.js b/test/capture/app.js new file mode 100644 index 0000000..18185cd --- /dev/null +++ b/test/capture/app.js @@ -0,0 +1,10 @@ +var sr = require('../../') +var render = require('./render') + +module.exports = async function app (route) { + var { bundles, result } = await sr.capture(function () { + return render(route) + }) + + return { result, bundles } +} diff --git a/test/capture/data1.js b/test/capture/data1.js new file mode 100644 index 0000000..873d1f5 --- /dev/null +++ b/test/capture/data1.js @@ -0,0 +1,16 @@ +var fs = require('fs') +var path = require('path') +var sr = require('../../') + +module.exports = function () { + return new Promise((resolve, reject )=> { + // callback somewhere + fs.readFile(path.join(__dirname, 'json.json'), function (err, d) { + if (err) reject(err) + else sr('./data3', function (err, d3) { + if (err) reject(err) + else resolve(`this is json: ${JSON.stringify(JSON.parse(d))} and this is d3: ${d3}`) + }) + }) + }) +} diff --git a/test/capture/data3.js b/test/capture/data3.js new file mode 100644 index 0000000..6887896 --- /dev/null +++ b/test/capture/data3.js @@ -0,0 +1 @@ +module.exports = 3 diff --git a/test/capture/json.json b/test/capture/json.json new file mode 100644 index 0000000..734d895 --- /dev/null +++ b/test/capture/json.json @@ -0,0 +1 @@ +{"some":"json"} diff --git a/test/capture/render.js b/test/capture/render.js new file mode 100644 index 0000000..da7d720 --- /dev/null +++ b/test/capture/render.js @@ -0,0 +1,9 @@ +var route1 = require('./route1') +var route2 = require('./route2') + +module.exports = function (route) { + if (route === 'one') { + return route1() + } + return require('util').promisify(route2)() +} diff --git a/test/capture/route1.js b/test/capture/route1.js new file mode 100644 index 0000000..1396a98 --- /dev/null +++ b/test/capture/route1.js @@ -0,0 +1,5 @@ +var sr = require('../../') + +module.exports = async function route1 () { + return (await sr('./view1'))(await sr('./data1')) +} diff --git a/test/capture/route2.js b/test/capture/route2.js new file mode 100644 index 0000000..074d252 --- /dev/null +++ b/test/capture/route2.js @@ -0,0 +1,13 @@ +var parallel = require('run-parallel') +module.exports = function (cb) { + process.nextTick(function () { + Promise.resolve().then(function () { + parallel([ + function (cb) { require('../../')('./view2', cb) }, + function (cb) { require('../../')('./data3', cb) } + ], function (err, xyz) { + cb(null, xyz[0](`Second route. ${xyz[1]}`)) + }) + }) + }) +} diff --git a/test/capture/view1.js b/test/capture/view1.js new file mode 100644 index 0000000..d73503f --- /dev/null +++ b/test/capture/view1.js @@ -0,0 +1,3 @@ +module.exports = async function (data) { + return `

${await data()}

` +} diff --git a/test/capture/view2.js b/test/capture/view2.js new file mode 100644 index 0000000..36f5d9d --- /dev/null +++ b/test/capture/view2.js @@ -0,0 +1,3 @@ +module.exports = function (data) { + return `

${data}

` +} diff --git a/test2.js b/test2.js deleted file mode 100644 index 9d3fa05..0000000 --- a/test2.js +++ /dev/null @@ -1,2 +0,0 @@ -module.exports = Promise.resolve() - .then(() => require('./')('./test3')) diff --git a/test3.js b/test3.js deleted file mode 100644 index ededdc5..0000000 --- a/test3.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = 10 From b4786c66390afc8a2e3fc4a9153311a0ef5b1449 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 6 Apr 2018 14:16:24 +0200 Subject: [PATCH 03/12] make sure both capture test branches yield to the event loop for a full tick --- test/capture/route2.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/capture/route2.js b/test/capture/route2.js index 074d252..08e7692 100644 --- a/test/capture/route2.js +++ b/test/capture/route2.js @@ -4,7 +4,8 @@ module.exports = function (cb) { Promise.resolve().then(function () { parallel([ function (cb) { require('../../')('./view2', cb) }, - function (cb) { require('../../')('./data3', cb) } + function (cb) { require('../../')('./data3', cb) }, + function (cb) { setTimeout(cb, 1) } // ensure that there is an event loop yield at some point ], function (err, xyz) { cb(null, xyz[0](`Second route. ${xyz[1]}`)) }) From 4a593f49822f39888e7a3da2ea6c1c77f327c9b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 6 Apr 2018 14:18:36 +0200 Subject: [PATCH 04/12] Throw if async_hooks doesnt exist --- index.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 0d8b49b..4a4f54d 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,12 @@ var path = require('path') var callerPath = require('caller-path') var resolvePath = require('resolve') -var asyncHooks = require('async_hooks') +var asyncHooks +try { + asyncHooks = require('async_hooks') +} catch (err) { + // async_hooks are not supported. +} function attachCb (promise, cb) { if (cb) { @@ -18,6 +23,8 @@ function attachCb (promise, cb) { var captureBundles = new Map() function capture (run, cb) { + if (!asyncHooks) throw new Error('async_hooks is not available. Upgrade your Node version to 8.1.0 or higher') + var hooks = asyncHooks.createHook({ init: function (asyncId, type, triggerAsyncId) { // Inherit bundles list from the parent @@ -62,14 +69,16 @@ module.exports = function load (filename, cb) { return require('./plugin')(filename, cb) } - var currentBundles = captureBundles.get(asyncHooks.executionAsyncId()) + var currentBundles = asyncHooks && captureBundles.get(asyncHooks.executionAsyncId()) var basedir = path.dirname(callerPath()) var resolved = new Promise(function (resolve, reject) { resolvePath(filename, { basedir: basedir }, function (err, fullpath) { if (err) return reject(err) - currentBundles.push(fullpath) + // Add the path to the bundle list if it is being captured + if (currentBundles) currentBundles.push(fullpath) + resolve(fullpath) }) }) From 728a17146811ca147fb98ff6dbd61afbcf34030b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 6 Apr 2018 14:20:15 +0200 Subject: [PATCH 05/12] skip capture test if async_hooks does not exist --- test/capture.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/test/capture.js b/test/capture.js index 0c7c5db..76e4e85 100644 --- a/test/capture.js +++ b/test/capture.js @@ -1,6 +1,15 @@ var test = require('tape') var path = require('path') +var hasAsyncHooks = (function () { + try { + require('async_hooks') + return true + } catch (err) { + return false + } +})() + var expected = { one: [ path.join(__dirname, 'capture/view1.js'), @@ -13,7 +22,7 @@ var expected = { ].sort() } -test('capture', function (t) { +test('capture', { skip: !hasAsyncHooks }, function (t) { t.plan(200) var app = require('./capture/app') From be5a838f076ad2cfb8f185491db890c63a662ea8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 6 Apr 2018 14:23:21 +0200 Subject: [PATCH 06/12] reuse a single hook (50% faster and probably saves lots of ram too) --- index.js | 27 +++++++++++++++++++-------- 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/index.js b/index.js index 4a4f54d..a6080fa 100644 --- a/index.js +++ b/index.js @@ -20,12 +20,13 @@ function attachCb (promise, cb) { return promise } -var captureBundles = new Map() +var captureBundles +var captureHooks +var activeCaptures = 0 +if (asyncHooks) { + captureBundles = new Map() -function capture (run, cb) { - if (!asyncHooks) throw new Error('async_hooks is not available. Upgrade your Node version to 8.1.0 or higher') - - var hooks = asyncHooks.createHook({ + captureHooks = asyncHooks.createHook({ init: function (asyncId, type, triggerAsyncId) { // Inherit bundles list from the parent if (captureBundles.has(triggerAsyncId)) { @@ -36,8 +37,13 @@ function capture (run, cb) { captureBundles.delete(asyncId) } }) +} + +function capture (run, cb) { + if (!asyncHooks) throw new Error('async_hooks is not available. Upgrade your Node version to 8.1.0 or higher') - hooks.enable() + if (activeCaptures === 0) captureHooks.enable() + activeCaptures++ var currentBundles = [] var asyncId = asyncHooks.executionAsyncId() @@ -59,7 +65,12 @@ function capture (run, cb) { function ondone (err, result) { captureBundles.delete(asyncId) // no memory leak - hooks.disable() + + activeCaptures-- + if (activeCaptures === 0) { + captureHooks.disable() + } + cb(err, result, currentBundles) } } @@ -69,7 +80,7 @@ module.exports = function load (filename, cb) { return require('./plugin')(filename, cb) } - var currentBundles = asyncHooks && captureBundles.get(asyncHooks.executionAsyncId()) + var currentBundles = asyncHooks ? captureBundles.get(asyncHooks.executionAsyncId()) : null var basedir = path.dirname(callerPath()) var resolved = new Promise(function (resolve, reject) { From b94aa3508648289b470476fce8f4934f63cb5c45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 6 Apr 2018 14:27:14 +0200 Subject: [PATCH 07/12] pin browser-pack to the version with the old prelude so i dont have to update every test --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ae55c76..20f81c9 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ }, "dependencies": { "acorn-node": "^1.1.0", - "browser-pack": "^6.0.2", + "browser-pack": "6.0.4", "caller-path": "^2.0.0", "convert-source-map": "^1.5.0", "end-of-stream": "^1.4.0", From ee770658c0e563912f4c73465f314acbc071a3cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 6 Apr 2018 14:33:17 +0200 Subject: [PATCH 08/12] add dummy test when async_hooks is absent so tap-min accepts it --- test/capture.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/capture.js b/test/capture.js index 76e4e85..b746b85 100644 --- a/test/capture.js +++ b/test/capture.js @@ -41,3 +41,8 @@ test('capture', { skip: !hasAsyncHooks }, function (t) { }, 4 + Math.floor(Math.random() * 10)) } }) + +test('no capture', { skip: hasAsyncHooks }, function (t) { + t.plan(1) + t.pass('ok') +}) From 2d9ea4a7a43e093084ec64819ba6f9c5fb0baff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 6 Apr 2018 15:07:12 +0200 Subject: [PATCH 09/12] Ensure separate async context for each capture() call --- index.js | 33 ++++++++++++++++++++++----------- test/capture.js | 20 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index a6080fa..7433c82 100644 --- a/index.js +++ b/index.js @@ -46,8 +46,6 @@ function capture (run, cb) { activeCaptures++ var currentBundles = [] - var asyncId = asyncHooks.executionAsyncId() - captureBundles.set(asyncId, currentBundles) if (!cb) { var promise = new Promise(function (resolve, reject) { @@ -58,20 +56,33 @@ function capture (run, cb) { }) } - var p = run(ondone) - if (p && p.then) p.then(function (result) { ondone(null, result) }, ondone) + // Make sure we're in a new async execution context + // This way doing two .capture() calls side by side from the same + // sync function won't interfere + // + // sr.capture(fn1) + // sr.capture(fn2) + process.nextTick(newContext) return promise - function ondone (err, result) { - captureBundles.delete(asyncId) // no memory leak + function newContext () { + var asyncId = asyncHooks.executionAsyncId() + captureBundles.set(asyncId, currentBundles) - activeCaptures-- - if (activeCaptures === 0) { - captureHooks.disable() - } + var p = run(ondone) + if (p && p.then) p.then(function (result) { ondone(null, result) }, ondone) + + function ondone (err, result) { + captureBundles.delete(asyncId) // no memory leak - cb(err, result, currentBundles) + activeCaptures-- + if (activeCaptures === 0) { + captureHooks.disable() + } + + cb(err, result, currentBundles) + } } } diff --git a/test/capture.js b/test/capture.js index b746b85..06fcd80 100644 --- a/test/capture.js +++ b/test/capture.js @@ -1,5 +1,6 @@ var test = require('tape') var path = require('path') +var sr = require('../') var hasAsyncHooks = (function () { try { @@ -42,6 +43,25 @@ test('capture', { skip: !hasAsyncHooks }, function (t) { } }) +test('capture sync', { skip: !hasAsyncHooks }, function (t) { + t.plan(20) + var render = require('./capture/render') + + // similar to above, but every call originates in the same + // async context + for (var i = 0; i < 20; i++) { + (function (i) { + var which = i % 2 ? 'one' : 'two' + + sr.capture(function () { + return render(which) + }).then(function (result) { + t.deepEqual(result.bundles.sort(), expected[which]) + }).catch(t.fail) + }(i)) + } +}) + test('no capture', { skip: hasAsyncHooks }, function (t) { t.plan(1) t.pass('ok') From b64d74664764b1c12f98735a9a88af881f5b6fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 20 Apr 2018 15:16:12 +0200 Subject: [PATCH 10/12] has-async-hooks --- index.js | 16 +++++++--------- package.json | 1 + test/capture.js | 10 +--------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/index.js b/index.js index 7433c82..65fdc25 100644 --- a/index.js +++ b/index.js @@ -3,12 +3,8 @@ var path = require('path') var callerPath = require('caller-path') var resolvePath = require('resolve') -var asyncHooks -try { - asyncHooks = require('async_hooks') -} catch (err) { - // async_hooks are not supported. -} +var hasAsyncHooks = require('has-async-hooks')() +var asyncHooks = hasAsyncHooks ? require('async_hooks') : null function attachCb (promise, cb) { if (cb) { @@ -23,7 +19,7 @@ function attachCb (promise, cb) { var captureBundles var captureHooks var activeCaptures = 0 -if (asyncHooks) { +if (hasAsyncHooks) { captureBundles = new Map() captureHooks = asyncHooks.createHook({ @@ -40,7 +36,7 @@ if (asyncHooks) { } function capture (run, cb) { - if (!asyncHooks) throw new Error('async_hooks is not available. Upgrade your Node version to 8.1.0 or higher') + if (!hasAsyncHooks) throw new Error('async_hooks is not available. Upgrade your Node version to 8.1.0 or higher') if (activeCaptures === 0) captureHooks.enable() activeCaptures++ @@ -91,7 +87,9 @@ module.exports = function load (filename, cb) { return require('./plugin')(filename, cb) } - var currentBundles = asyncHooks ? captureBundles.get(asyncHooks.executionAsyncId()) : null + var currentBundles = hasAsyncHooks && activeCaptures > 0 + ? captureBundles.get(asyncHooks.executionAsyncId()) + : null var basedir = path.dirname(callerPath()) var resolved = new Promise(function (resolve, reject) { diff --git a/package.json b/package.json index 20f81c9..0d7864e 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "estree-is-require": "^1.0.0", "estree-walk": "^2.2.0", "flush-write-stream": "^1.0.2", + "has-async-hooks": "^1.0.0", "labeled-stream-splicer": "^2.0.0", "object-delete-value": "^1.0.0", "object-values": "^1.0.0", diff --git a/test/capture.js b/test/capture.js index 06fcd80..966bfe5 100644 --- a/test/capture.js +++ b/test/capture.js @@ -1,16 +1,8 @@ var test = require('tape') var path = require('path') +var hasAsyncHooks = require('has-async-hooks')() var sr = require('../') -var hasAsyncHooks = (function () { - try { - require('async_hooks') - return true - } catch (err) { - return false - } -})() - var expected = { one: [ path.join(__dirname, 'capture/view1.js'), From 1c41a0bbd2612134fc1b23bbabb32bc11c7b4436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 20 Apr 2018 16:08:44 +0200 Subject: [PATCH 11/12] capture with manifest --- index.js | 48 ++++++++++++++++++++++++++++++++--- plugin.js | 35 ++++++++++++++++++++++++- test/basic/app.js | 8 +++--- test/basic/expected/bundle.js | 8 +++--- test/capture.js | 38 ++++++++++++++++++++++++++- test/capture/app.js | 2 +- 6 files changed, 127 insertions(+), 12 deletions(-) diff --git a/index.js b/index.js index 65fdc25..da95437 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ 'use strict' +var fs = require('fs') var path = require('path') var callerPath = require('caller-path') var resolvePath = require('resolve') @@ -16,6 +17,8 @@ function attachCb (promise, cb) { return promise } +var bundleMappings + var captureBundles var captureHooks var activeCaptures = 0 @@ -35,8 +38,21 @@ if (hasAsyncHooks) { }) } -function capture (run, cb) { +function capture (opts, run, cb) { + if (typeof opts === 'function') { + cb = run + run = opts + opts = {} + } + if (!opts) opts = {} + if (!hasAsyncHooks) throw new Error('async_hooks is not available. Upgrade your Node version to 8.1.0 or higher') + if (!bundleMappings && opts.filenames !== true) { + throw new Error('Load a manifest file before using splitRequire.capture(). ' + + 'This is required to inform split-require about the bundle filenames. ' + + 'If you want the filenames for the unbundled entry points instead, do ' + + '`splitRequire.capture({ filenames: true }, run, cb)`.') + } if (activeCaptures === 0) captureHooks.enable() activeCaptures++ @@ -64,7 +80,10 @@ function capture (run, cb) { function newContext () { var asyncId = asyncHooks.executionAsyncId() - captureBundles.set(asyncId, currentBundles) + captureBundles.set(asyncId, { + list: currentBundles, + filenames: opts.filenames === true + }) var p = run(ondone) if (p && p.then) p.then(function (result) { ondone(null, result) }, ondone) @@ -82,6 +101,20 @@ function capture (run, cb) { } } +function loadManifest (manifest) { + if (!bundleMappings) bundleMappings = new Map() + + var mappings = JSON.parse(fs.readFileSync(manifest, 'utf8')) + var basedir = path.dirname(path.resolve(manifest)) + var publicPath = mappings.publicPath + var bundles = mappings.bundles + Object.keys(bundles).forEach(function (bundleName) { + bundles[bundleName].forEach(function (filename) { + bundleMappings.set(path.join(basedir, filename), path.join(publicPath, bundleName)) + }) + }) +} + module.exports = function load (filename, cb) { if (typeof filename === 'object' && filename._options) { return require('./plugin')(filename, cb) @@ -97,7 +130,15 @@ module.exports = function load (filename, cb) { if (err) return reject(err) // Add the path to the bundle list if it is being captured - if (currentBundles) currentBundles.push(fullpath) + if (currentBundles) { + if (currentBundles.filenames) { + currentBundles.list.push(fullpath) + } else { + var bundle = bundleMappings.get(fullpath) + if (!bundle) return reject(new Error('Could not find \'' + fullpath + '\' in the bundle manifest')) + currentBundles.list.push(bundle) + } + } resolve(fullpath) }) @@ -107,6 +148,7 @@ module.exports = function load (filename, cb) { } module.exports.capture = capture +module.exports.loadManifest = loadManifest Object.defineProperty(module.exports, 'createStream', { configurable: true, diff --git a/plugin.js b/plugin.js index 01e96e6..aef97f7 100644 --- a/plugin.js +++ b/plugin.js @@ -107,6 +107,20 @@ function createSplitter (b, opts) { } return fs.createWriteStream(path.join(outputDir, bundleName)) } + var writeManifest = typeof opts.manifest === 'function' ? opts.manifest : function (manifest, cb) { + var manifestPath = null + if (typeof opts.manifest === 'string') { + manifestPath = opts.manifest + } else if (!opts.manifest) { + return cb() + } else { + var outdir = opts.out || opts.dir + if (!outdir || outdir.indexOf('%f') !== -1) return cb() // Just don't write it I guess? + manifestPath = path.join(opts.out || opts.dir, 'split-require.json') + } + + fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2), cb) + } var publicPath = opts.public || './' var rows = [] @@ -239,6 +253,14 @@ function createSplitter (b, opts) { runParallel(pipelines, function (err, mappings) { if (err) return cb(err) var sri = {} + var manifest = { + publicPath: publicPath, + bundles: mappings.reduce(function (obj, x) { + obj[x.filename] = x.modules + return obj + }, {}) + } + mappings = mappings.reduce(function (obj, x) { obj[x.entry] = path.join(publicPath, x.filename) if (x.integrity) sri[x.entry] = x.integrity @@ -257,13 +279,23 @@ function createSplitter (b, opts) { self.push(row) }) - cb(null) + writeManifest(manifest, cb) }) } function createPipeline (entryId, depRows, cb) { var entry = getRow(entryId) + var currentModules = [] + var recordModules = through.obj(function (rec, enc, next) { + if (rec.file) { + currentModules.push(typeof opts.manifest === 'string' + ? path.relative(path.dirname(opts.manifest), rec.file) + : rec.file) + } + next(null, rec) + }) var pipeline = splicer.obj([ + 'record', [ recordModules ], 'pack', [ pack({ raw: true }) ], 'wrap', [] ]) @@ -295,6 +327,7 @@ function createSplitter (b, opts) { cb(null, { entry: entryId, filename: basename, + modules: currentModules, integrity: opts.sri ? sri.value : null }) } diff --git a/test/basic/app.js b/test/basic/app.js index ef203da..0fcbe29 100644 --- a/test/basic/app.js +++ b/test/basic/app.js @@ -1,6 +1,8 @@ var xyz = require('./xyz') var splitRequire = require('split-require') -splitRequire('./dynamic', function (err, exports) { - console.log(xyz(10) + exports) -}) +module.exports = function (cb) { + splitRequire('./dynamic', function (err, exports) { + cb(xyz(10) + exports) + }) +} diff --git a/test/basic/expected/bundle.js b/test/basic/expected/bundle.js index 0fa63a6..864349d 100644 --- a/test/basic/expected/bundle.js +++ b/test/basic/expected/bundle.js @@ -4,9 +4,11 @@ require("split-require").b = {"2":"bundle.2.js"}; var xyz = require('./xyz') var splitRequire = require('split-require') -splitRequire(2, function (err, exports) { - console.log(xyz(10) + exports) -}) +module.exports = function (cb) { + splitRequire(2, function (err, exports) { + cb(xyz(10) + exports) + }) +} },{"./xyz":4,"split-require":"split-require"}],4:[function(require,module,exports){ module.exports = function xyz (num) { diff --git a/test/capture.js b/test/capture.js index 966bfe5..87f4d8a 100644 --- a/test/capture.js +++ b/test/capture.js @@ -1,6 +1,8 @@ var test = require('tape') var path = require('path') var hasAsyncHooks = require('has-async-hooks')() +var mkdirp = require('mkdirp') +var browserify = require('browserify') var sr = require('../') var expected = { @@ -45,7 +47,7 @@ test('capture sync', { skip: !hasAsyncHooks }, function (t) { (function (i) { var which = i % 2 ? 'one' : 'two' - sr.capture(function () { + sr.capture({ filenames: true }, function () { return render(which) }).then(function (result) { t.deepEqual(result.bundles.sort(), expected[which]) @@ -58,3 +60,37 @@ test('no capture', { skip: hasAsyncHooks }, function (t) { t.plan(1) t.pass('ok') }) + +test('capture bundles', { skip: !hasAsyncHooks }, function (t) { + t.plan(4) + + var outdir = path.join(__dirname, 'capture/actual/') + var manifest = path.join(outdir, 'split-require.json') + var entry = path.join(__dirname, 'basic/app.js') + + mkdirp.sync(outdir) + + browserify(entry) + .plugin(sr, { + dir: outdir, + manifest: manifest + }) + .bundle(function (err, bundle) { + t.ifError(err) + + ssr() + }) + + function ssr () { + sr.loadManifest(manifest) + sr.capture(function (cb) { + require(entry)(cb.bind(null, null)) + }, ondone) + } + + function ondone (err, result, bundles) { + t.ifError(err) + t.equal(result, 146) + t.deepEqual(bundles, ['bundle.3.js']) + } +}) diff --git a/test/capture/app.js b/test/capture/app.js index 18185cd..232802a 100644 --- a/test/capture/app.js +++ b/test/capture/app.js @@ -2,7 +2,7 @@ var sr = require('../../') var render = require('./render') module.exports = async function app (route) { - var { bundles, result } = await sr.capture(function () { + var { bundles, result } = await sr.capture({ filenames: true }, function () { return render(route) }) From 38c25e33504f8db7a8bc8c89fd282baab7fa7c35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9e=20Kooi?= Date: Fri, 20 Apr 2018 16:21:53 +0200 Subject: [PATCH 12/12] Fix resolution error in test --- package.json | 1 + test/capture.js | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0d7864e..0c9f74d 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "factor-bundle": "^2.5.0", "has-object-spread": "^1.0.0", "mkdirp": "^0.5.1", + "proxyquire": "^2.0.1", "read-file-tree": "^1.1.0", "rimraf": "^2.6.2", "run-series": "^1.1.4", diff --git a/test/capture.js b/test/capture.js index 87f4d8a..035af38 100644 --- a/test/capture.js +++ b/test/capture.js @@ -3,6 +3,7 @@ var path = require('path') var hasAsyncHooks = require('has-async-hooks')() var mkdirp = require('mkdirp') var browserify = require('browserify') +var proxyquire = require('proxyquire') var sr = require('../') var expected = { @@ -71,6 +72,7 @@ test('capture bundles', { skip: !hasAsyncHooks }, function (t) { mkdirp.sync(outdir) browserify(entry) + .require(path.join(__dirname, '../'), { expose: 'split-require' }) .plugin(sr, { dir: outdir, manifest: manifest @@ -84,13 +86,15 @@ test('capture bundles', { skip: !hasAsyncHooks }, function (t) { function ssr () { sr.loadManifest(manifest) sr.capture(function (cb) { - require(entry)(cb.bind(null, null)) + sr['@noCallThru'] = true // AAAAAAAAAAAAAAA + sr['@global'] = true // AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + proxyquire(entry, { 'split-require': sr })(cb.bind(null, null)) }, ondone) } function ondone (err, result, bundles) { t.ifError(err) t.equal(result, 146) - t.deepEqual(bundles, ['bundle.3.js']) + t.deepEqual(bundles, ['bundle.2.js']) } })