Skip to content

Commit

Permalink
Add "--target=node" and "--target=electron" option to produce Node/el…
Browse files Browse the repository at this point in the history
…ectron friendly bundles (#652)
  • Loading branch information
lbguilherme authored and devongovett committed Feb 5, 2018
1 parent 8c3c4be commit 420ed63
Show file tree
Hide file tree
Showing 15 changed files with 312 additions and 72 deletions.
7 changes: 6 additions & 1 deletion src/Bundler.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class Bundler extends EventEmitter {
'/' + Path.basename(options.outDir || 'dist');
const watch =
typeof options.watch === 'boolean' ? options.watch : !isProduction;
const target = options.target || 'browser';
return {
outDir: Path.resolve(options.outDir || 'dist'),
publicURL: publicURL,
Expand All @@ -77,7 +78,11 @@ class Bundler extends EventEmitter {
typeof options.killWorkers === 'boolean' ? options.killWorkers : true,
minify:
typeof options.minify === 'boolean' ? options.minify : isProduction,
hmr: typeof options.hmr === 'boolean' ? options.hmr : watch,
target: target,
hmr:
target === 'node'
? false
: typeof options.hmr === 'boolean' ? options.hmr : watch,
https: options.https || false,
logLevel: typeof options.logLevel === 'number' ? options.logLevel : 3,
mainFile: this.mainFile,
Expand Down
2 changes: 1 addition & 1 deletion src/FSCache.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const pkg = require('../package.json');
const logger = require('./Logger');

// These keys can affect the output, so if they differ, the cache should not match
const OPTION_KEYS = ['publicURL', 'minify', 'hmr'];
const OPTION_KEYS = ['publicURL', 'minify', 'hmr', 'target'];

class FSCache {
constructor(options) {
Expand Down
16 changes: 9 additions & 7 deletions src/assets/JSAsset.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,16 @@ class JSAsset extends Asset {
}

async transform() {
if (this.dependencies.has('fs') && FS_RE.test(this.contents)) {
await this.parseIfNeeded();
this.traverse(fsVisitor);
}
if (this.options.target === 'browser') {
if (this.dependencies.has('fs') && FS_RE.test(this.contents)) {
await this.parseIfNeeded();
this.traverse(fsVisitor);
}

if (GLOBAL_RE.test(this.contents)) {
await this.parseIfNeeded();
walk.ancestor(this.ast, insertGlobals, this);
if (GLOBAL_RE.test(this.contents)) {
await this.parseIfNeeded();
walk.ancestor(this.ast, insertGlobals, this);
}
}

if (this.isES6Module) {
Expand Down
3 changes: 2 additions & 1 deletion src/builtins/hmr-runtime.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ function Module(moduleName) {

module.bundle.Module = Module;

if (!module.bundle.parent && typeof WebSocket !== 'undefined') {
var parent = module.bundle.parent;
if ((!parent || !parent.isParcelRequire) && typeof WebSocket !== 'undefined') {
var hostname = process.env.HMR_HOSTNAME || location.hostname;
var protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
var ws = new WebSocket(protocol + '://' + hostname + ':' + process.env.HMR_PORT + '/');
Expand Down
3 changes: 2 additions & 1 deletion src/builtins/prelude.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ require = (function (modules, cache, entry) {
err.code = 'MODULE_NOT_FOUND';
throw err;
}

localRequire.resolve = resolve;

var module = cache[name] = new newRequire.Module(name);
Expand All @@ -59,6 +59,7 @@ require = (function (modules, cache, entry) {
this.exports = {};
}

newRequire.isParcelRequire = true;
newRequire.Module = Module;
newRequire.modules = modules;
newRequire.cache = cache;
Expand Down
15 changes: 15 additions & 0 deletions src/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ program
.option('--no-hmr', 'disable hot module replacement')
.option('--no-cache', 'disable the filesystem cache')
.option('--no-source-maps', 'disable sourcemaps')
.option(
'-t, --target [target]',
'set the runtime environment, either "node", "browser" or "electron". defaults to "browser"',
/^(node|browser|electron)$/
)
.option('-V, --version', 'output the version number')
.action(bundle);

Expand All @@ -52,6 +57,11 @@ program
.option('--no-hmr', 'disable hot module replacement')
.option('--no-cache', 'disable the filesystem cache')
.option('--no-source-maps', 'disable sourcemaps')
.option(
'-t, --target [target]',
'set the runtime environment, either "node", "browser" or "electron". defaults to "browser"',
/^(node|browser|electron)$/
)
.action(bundle);

program
Expand All @@ -67,6 +77,11 @@ program
)
.option('--no-minify', 'disable minification')
.option('--no-cache', 'disable the filesystem cache')
.option(
'-t, --target <target>',
'set the runtime environment, either "node", "browser" or "electron". defaults to "browser"',
/^(node|browser|electron)$/
)
.action(bundle);

program
Expand Down
8 changes: 8 additions & 0 deletions src/packagers/JSPackager.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ class JSPackager extends Packager {
this.externalModules = new Set();

let preludeCode = this.options.minify ? prelude.minified : prelude.source;
if (this.options.target === 'electron') {
preludeCode =
`process.env.HMR_PORT=${
this.options.hmrPort
};process.env.HMR_HOSTNAME=${JSON.stringify(
this.options.hmrHostname
)};` + preludeCode;
}
await this.dest.write(preludeCode + '({');
this.lineOffset = lineCounter(preludeCode);
}
Expand Down
9 changes: 9 additions & 0 deletions src/visitors/dependencies.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,15 @@ module.exports = {
};

function addDependency(asset, node, opts = {}) {
if (asset.options.target !== 'browser') {
const isRelativeImport =
node.value.startsWith('/') ||
node.value.startsWith('./') ||
node.value.startsWith('../');

if (!isRelativeImport) return;
}

opts.loc = node.loc && node.loc.start;
asset.addDependency(node.value, opts);
}
Expand Down
171 changes: 112 additions & 59 deletions test/fs.js
Original file line number Diff line number Diff line change
@@ -1,77 +1,130 @@
const assert = require('assert');
const fs = require('fs');
const {bundle, run, assertBundleTree} = require('./utils');

describe('fs', function() {
it('should inline a file as a string', async function() {
let b = await bundle(__dirname + '/integration/fs/index.js');
let output = run(b);
assert.equal(output, 'hello');
});
describe('--target=browser', function() {
it('should inline a file as a string', async function() {
let b = await bundle(__dirname + '/integration/fs/index.js');
let output = run(b);
assert.equal(output, 'hello');
});

it('should inline a file as a buffer', async function() {
let b = await bundle(__dirname + '/integration/fs-buffer/index.js');
let output = run(b);
assert.equal(output.constructor.name, 'Buffer');
assert.equal(output.length, 5);
});
it('should inline a file as a buffer', async function() {
let b = await bundle(__dirname + '/integration/fs-buffer/index.js');
let output = run(b);
assert.equal(output.constructor.name, 'Buffer');
assert.equal(output.length, 5);
});

it('should inline a file with fs require alias', async function() {
let b = await bundle(__dirname + '/integration/fs-alias/index.js');
let output = run(b);
assert.equal(output, 'hello');
});
it('should inline a file with fs require alias', async function() {
let b = await bundle(__dirname + '/integration/fs-alias/index.js');
let output = run(b);
assert.equal(output, 'hello');
});

it('should inline a file with fs require inline', async function() {
let b = await bundle(__dirname + '/integration/fs-inline/index.js');
let output = run(b);
assert.equal(output, 'hello');
});
it('should inline a file with fs require inline', async function() {
let b = await bundle(__dirname + '/integration/fs-inline/index.js');
let output = run(b);
assert.equal(output, 'hello');
});

it('should inline a file with fs require assignment', async function() {
let b = await bundle(__dirname + '/integration/fs-assign/index.js');
let output = run(b);
assert.equal(output, 'hello');
});
it('should inline a file with fs require assignment', async function() {
let b = await bundle(__dirname + '/integration/fs-assign/index.js');
let output = run(b);
assert.equal(output, 'hello');
});

it('should inline a file with fs require assignment alias', async function() {
let b = await bundle(__dirname + '/integration/fs-assign-alias/index.js');
let output = run(b);
assert.equal(output, 'hello');
});
it('should inline a file with fs require assignment alias', async function() {
let b = await bundle(__dirname + '/integration/fs-assign-alias/index.js');
let output = run(b);
assert.equal(output, 'hello');
});

it('should inline a file with fs require destructure', async function() {
let b = await bundle(__dirname + '/integration/fs-destructure/index.js');
let output = run(b);
assert.equal(output, 'hello');
});
it('should inline a file with fs require destructure', async function() {
let b = await bundle(__dirname + '/integration/fs-destructure/index.js');
let output = run(b);
assert.equal(output, 'hello');
});

it('should inline a file with fs require destructure assignment', async function() {
let b = await bundle(
__dirname + '/integration/fs-destructure-assign/index.js'
);
let output = run(b);
assert.equal(output, 'hello');
it('should inline a file with fs require destructure assignment', async function() {
let b = await bundle(
__dirname + '/integration/fs-destructure-assign/index.js'
);
let output = run(b);
assert.equal(output, 'hello');
});

it('should not evaluate fs calls when package.browser.fs is false', async function() {
let b = await bundle(
__dirname + '/integration/resolve-entries/ignore-fs.js'
);

assertBundleTree(b, {
name: 'ignore-fs.js',
// empty.js is generated by require('fs'), it gets mocked with an empty module
assets: ['empty.js', 'ignore-fs.js', 'index.js'],
childBundles: [
{
type: 'map'
}
]
});

let output = run(b);

assert.equal(typeof output.test, 'function');
assert.equal(output.test(), 'test-pkg-ignore-fs-ok');
});
});

it('should not evaluate fs calls when package.browser.fs is false', async function() {
let b = await bundle(
__dirname + '/integration/resolve-entries/ignore-fs.js'
);

assertBundleTree(b, {
name: 'ignore-fs.js',
// empty.js is generated by require('fs'), it gets mocked with an empty module
assets: ['empty.js', 'ignore-fs.js', 'index.js'],
childBundles: [
{
type: 'map'
}
]
describe('--target=node', function() {
it('should leave an attempt to read a file unchanged', async function() {
let b = await bundle(__dirname + '/integration/fs/index.js', {
target: 'node'
});

assertBundleTree(b, {
name: 'index.js',
assets: ['index.js'],
childBundles: [
{
type: 'map'
}
]
});

assert(fs.readFileSync(b.name).includes("require('fs')"));
assert(fs.readFileSync(b.name).includes('readFileSync'));

fs.writeFileSync(__dirname + '/dist/test.txt', 'hey');
let output = run(b);
assert.equal(output, 'hey');
});
});

let output = run(b);
describe('--target=electron', function() {
it('should leave an attempt to read a file unchanged', async function() {
let b = await bundle(__dirname + '/integration/fs/index.js', {
target: 'electron'
});

assert.equal(typeof output.test, 'function');
assert.equal(output.test(), 'test-pkg-ignore-fs-ok');
assertBundleTree(b, {
name: 'index.js',
assets: ['index.js'],
childBundles: [
{
type: 'map'
}
]
});

assert(fs.readFileSync(b.name).includes("require('fs')"));
assert(fs.readFileSync(b.name).includes('readFileSync'));

fs.writeFileSync(__dirname + '/dist/test.txt', 'hey');
let output = run(b);
assert.equal(output, 'hey');
});
});
});
41 changes: 41 additions & 0 deletions test/hmr.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,47 @@ describe('hmr', function() {
assert.deepEqual(msg.assets[0].deps, {});
});

it('should not enable HMR for --target=node', async function() {
await ncp(__dirname + '/integration/commonjs', __dirname + '/input');

b = bundler(__dirname + '/input/index.js', {
watch: true,
hmr: true,
target: 'node'
});
await b.bundle();

ws = new WebSocket('ws://localhost:' + b.options.hmrPort);

let err = await nextEvent(ws, 'error');
assert(err);
ws = null;
});

it('should enable HMR for --target=electron', async function() {
await ncp(__dirname + '/integration/commonjs', __dirname + '/input');

b = bundler(__dirname + '/input/index.js', {
watch: true,
hmr: true,
target: 'electron'
});
await b.bundle();

ws = new WebSocket('ws://localhost:' + b.options.hmrPort);

fs.writeFileSync(
__dirname + '/input/local.js',
'exports.a = 5; exports.b = 5;'
);

let msg = json5.parse(await nextEvent(ws, 'message'));
assert.equal(msg.type, 'update');
assert.equal(msg.assets.length, 1);
assert.equal(msg.assets[0].generated.js, 'exports.a = 5; exports.b = 5;');
assert.deepEqual(msg.assets[0].deps, {});
});

it('should emit an HMR update for all new dependencies along with the changed file', async function() {
await ncp(__dirname + '/integration/commonjs', __dirname + '/input');

Expand Down
1 change: 1 addition & 0 deletions test/integration/node_require/local.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
exports.b = 2;
6 changes: 6 additions & 0 deletions test/integration/node_require/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
var {a} = require('testmodule');
var {b} = require('./local');

module.exports = function () {
return a + b;
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 420ed63

Please sign in to comment.