Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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