Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Capture prototype #20

Open
wants to merge 12 commits into
base: default
Choose a base branch
from
119 changes: 119 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
'use strict'

var fs = require('fs')
var path = require('path')
var callerPath = require('caller-path')
var resolvePath = require('resolve')
var hasAsyncHooks = require('has-async-hooks')()
var asyncHooks = hasAsyncHooks ? require('async_hooks') : null

function attachCb (promise, cb) {
if (cb) {
Expand All @@ -14,23 +17,139 @@ function attachCb (promise, cb) {
return promise
}

var bundleMappings

var captureBundles
var captureHooks
var activeCaptures = 0
if (hasAsyncHooks) {
captureBundles = new Map()

captureHooks = asyncHooks.createHook({
init: function (asyncId, type, triggerAsyncId) {
// Inherit bundles list from the parent
if (captureBundles.has(triggerAsyncId)) {
captureBundles.set(asyncId, captureBundles.get(triggerAsyncId))
}
},
destroy: function (asyncId) {
captureBundles.delete(asyncId)
}
})
}

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++

var 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 })
}
})
}

// 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 newContext () {
var asyncId = asyncHooks.executionAsyncId()
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)

function ondone (err, result) {
captureBundles.delete(asyncId) // no memory leak

activeCaptures--
if (activeCaptures === 0) {
captureHooks.disable()
}

cb(err, result, currentBundles)
}
}
}

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)
}

var currentBundles = hasAsyncHooks && activeCaptures > 0
? captureBundles.get(asyncHooks.executionAsyncId())
: null

var basedir = path.dirname(callerPath())
var resolved = new Promise(function (resolve, reject) {
resolvePath(filename, { basedir: basedir }, function (err, fullpath) {
if (err) return reject(err)

// Add the path to the bundle list if it is being captured
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)
})
})

return attachCb(resolved.then(require), cb)
}

module.exports.capture = capture
module.exports.loadManifest = loadManifest

Object.defineProperty(module.exports, 'createStream', {
configurable: true,
enumerable: true,
Expand Down
10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
},
"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",
"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",
Expand All @@ -31,11 +32,13 @@
"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",
"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"
Expand All @@ -50,10 +53,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": [
Expand Down
35 changes: 34 additions & 1 deletion plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand Down Expand Up @@ -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
Expand All @@ -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', []
])
Expand Down Expand Up @@ -295,6 +327,7 @@ function createSplitter (b, opts) {
cb(null, {
entry: entryId,
filename: basename,
modules: currentModules,
integrity: opts.sri ? sri.value : null
})
}
Expand Down
8 changes: 5 additions & 3 deletions test/basic/app.js
Original file line number Diff line number Diff line change
@@ -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)
})
}
8 changes: 5 additions & 3 deletions test/basic/expected/bundle.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
100 changes: 100 additions & 0 deletions test/capture.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
var test = require('tape')
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 = {
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', { skip: !hasAsyncHooks }, 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))
}
})

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({ filenames: true }, 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')
})

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)
.require(path.join(__dirname, '../'), { expose: 'split-require' })
.plugin(sr, {
dir: outdir,
manifest: manifest
})
.bundle(function (err, bundle) {
t.ifError(err)

ssr()
})

function ssr () {
sr.loadManifest(manifest)
sr.capture(function (cb) {
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.2.js'])
}
})
Loading