Permalink
Browse files

fix(bundler): support nodejs subfolder package.json

This fix implemented full algorithm described in https://nodejs.org/dist/latest-v8.x/docs/api/modules.html, after "high-level algorithm in pseudocode of what require.resolve() does".
  • Loading branch information...
huochunpeng committed Oct 15, 2018
1 parent 9ebe10a commit eef51f04f708049b6299c487019e4f609d3cc3b5
@@ -2,7 +2,7 @@
const path = require('path');
const SourceInclusion = require('./source-inclusion').SourceInclusion;
const minimatch = require('minimatch');
const fs = require('fs');
const fs = require('../file-system');
const logger = require('aurelia-logging').getLogger('DependencyInclusion');
const knownNonJsExtensions = ['.json', '.css', '.svg', '.html'];
@@ -149,6 +149,7 @@ exports.DependencyInclusion = class {
};
function resolvedResource(resource, description, projectRoot) {
const base = path.resolve(projectRoot, description.loaderConfig.path);
let mainShift = description.loaderConfig.main.split('/');
// when mainShift is [dist,commonjs]
@@ -166,33 +167,17 @@ function resolvedResource(resource, description, projectRoot) {
res = resource;
}
resolved = validResource(res, description, projectRoot);
resolved = validResource(res, base);
if (resolved) break;
} while (mainShift.length);
return resolved;
}
function validResource(resource, description, projectRoot) {
const resExtname = path.extname(resource).toLowerCase();
const base = description.loaderConfig.path;
if (!resExtname || knownNonJsExtensions.indexOf(resExtname) === -1) {
// when resource is foo/bar
// nodejs will try node_modules/foo/bar.js first, then node_modules/foo/bar/index.js
const directRef = resource + '.js';
const indexRef = resource + '/index.js';
if (fs.existsSync(path.resolve(projectRoot, base, directRef))) {
return directRef;
} else if (fs.existsSync(path.resolve(projectRoot, base, indexRef))) {
return indexRef;
}
}
if (fs.existsSync(path.resolve(projectRoot, base, resource))) {
return resource;
}
function validResource(resource, base) {
const resourcePath = path.resolve(base, resource);
const loaded = nodejsLoadAsFile(resourcePath) || nodejsLoadAsDirectory(resourcePath);
if (loaded) return path.relative(base, loaded).replace(/\\/g, '/');
}
function removeJsExtension(filePath) {
@@ -221,3 +206,78 @@ function commonLength(ids) {
return common.length;
}
// TODO: refactor
// Extract this, plus part of package-analyzer into nodejs-utils.
// I will do it after 2018-11-15 after my vacation. Chunpeng Huo
// https://nodejs.org/dist/latest-v8.x/docs/api/modules.html
// after "high-level algorithm in pseudocode of what require.resolve() does"
function nodejsLoadAsFile(resourcePath) {
if (fs.isFile(resourcePath)) {
return resourcePath;
}
if (fs.isFile(resourcePath + '.js')) {
return resourcePath + '.js';
}
if (fs.isFile(resourcePath + '.json')) {
return resourcePath + '.json';
}
// skip .node file that nobody uses
}
function nodejsLoadIndex(resourcePath) {
if (!fs.isDirectory(resourcePath)) return;
const indexJs = path.join(resourcePath, 'index.js');
if (fs.isFile(indexJs)) {
return indexJs;
}
const indexJson = path.join(resourcePath, 'index.json');
if (fs.isFile(indexJson)) {
return indexJson;
}
// skip index.node file that nobody uses
}
function nodejsLoadAsDirectory(resourcePath) {
if (!fs.isDirectory(resourcePath)) return;
const packageJson = path.join(resourcePath, 'package.json');
if (fs.isFile(packageJson)) {
let metadata;
try {
metadata = JSON.parse(fs.readFileSync(packageJson));
} catch (err) {
logger.error(err);
return;
}
let metaMain;
// try 1.browser > 2.module > 3.main
// the order is to target browser.
// when aurelia-cli introduces multi-targets build,
// it probably should use different order for electron app
// for electron 1.module > 2.browser > 3.main
if (typeof metadata.browser === 'string') {
// use package.json browser field if possible.
metaMain = metadata.browser;
} else if (typeof metadata.module === 'string') {
// prefer es module format over cjs, just like webpack.
// this improves compatibility with TypeScript.
metaMain = metadata.module;
} else if (typeof metadata.main === 'string') {
metaMain = metadata.main;
}
let mainFile = metaMain || 'index';
const mainResourcePath = path.resolve(resourcePath, mainFile);
return nodejsLoadAsFile(mainResourcePath) || nodejsLoadIndex(mainResourcePath);
}
return nodejsLoadIndex(resourcePath);
}
@@ -102,6 +102,24 @@ exports.statSync = function(path) {
return fs.statSync(path);
};
exports.isFile = function(path) {
try {
return fs.statSync(path).isFile();
} catch (err) {
// ignore
return false;
}
};
exports.isDirectory = function(path) {
try {
return fs.statSync(path).isDirectory();
} catch (err) {
// ignore
return false;
}
};
exports.writeFile = function(path, content, encoding) {
return new Promise((resolve, reject) => {
mkdirp(nodePath.dirname(path), err => {

Some generated files are not rendered by default. Learn more.

Oops, something went wrong.
@@ -314,6 +314,151 @@ describe('the DependencyInclusion module', () => {
.catch(e => done.fail(e));
});
it('traceResource at runtime add json resource to bundle', done => {
let bundle = {
bundler: bundler,
addAlias: jasmine.createSpy('addAlias'),
includes: [],
createMatcher: function(pattern) {
return new Minimatch(pattern, {
dot: true
});
}
};
let description = new DependencyDescription('my-package', 'npm');
description.loaderConfig = {
path: '../node_modules/my-package',
name: 'my-package',
main: 'index',
lazyMain: true
};
mockfs({
'node_modules/my-package/lib/foo.json': 'some-content'
});
let sut = new DependencyInclusion(bundle, description);
sut._getProjectRoot = () => 'src';
sut.traceResource('lib/foo')
.then(() => {
expect(bundle.includes.length).toBe(1);
expect(bundle.includes[0].pattern).toBe('../node_modules/my-package/lib/foo.json');
expect(bundle.addAlias).toHaveBeenCalledWith('my-package/lib/foo', 'my-package/lib/foo.json');
done();
})
.catch(e => done.fail(e));
});
it('traceResource at runtime add index resource to bundle', done => {
let bundle = {
bundler: bundler,
addAlias: jasmine.createSpy('addAlias'),
includes: [],
createMatcher: function(pattern) {
return new Minimatch(pattern, {
dot: true
});
}
};
let description = new DependencyDescription('my-package', 'npm');
description.loaderConfig = {
path: '../node_modules/my-package',
name: 'my-package',
main: 'index',
lazyMain: true
};
mockfs({
'node_modules/my-package/lib/foo/index.js': 'some-content'
});
let sut = new DependencyInclusion(bundle, description);
sut._getProjectRoot = () => 'src';
sut.traceResource('lib/foo')
.then(() => {
expect(bundle.includes.length).toBe(1);
expect(bundle.includes[0].pattern).toBe('../node_modules/my-package/lib/foo/index.js');
expect(bundle.addAlias).toHaveBeenCalledWith('my-package/lib/foo', 'my-package/lib/foo/index');
done();
})
.catch(e => done.fail(e));
});
it('traceResource at runtime add index.json resource to bundle', done => {
let bundle = {
bundler: bundler,
addAlias: jasmine.createSpy('addAlias'),
includes: [],
createMatcher: function(pattern) {
return new Minimatch(pattern, {
dot: true
});
}
};
let description = new DependencyDescription('my-package', 'npm');
description.loaderConfig = {
path: '../node_modules/my-package',
name: 'my-package',
main: 'index',
lazyMain: true
};
mockfs({
'node_modules/my-package/lib/foo/index.json': 'some-content'
});
let sut = new DependencyInclusion(bundle, description);
sut._getProjectRoot = () => 'src';
sut.traceResource('lib/foo')
.then(() => {
expect(bundle.includes.length).toBe(1);
expect(bundle.includes[0].pattern).toBe('../node_modules/my-package/lib/foo/index.json');
expect(bundle.addAlias).toHaveBeenCalledWith('my-package/lib/foo', 'my-package/lib/foo/index.json');
done();
})
.catch(e => done.fail(e));
});
it('traceResource at runtime add resource described by folder package.json to bundle', done => {
let bundle = {
bundler: bundler,
addAlias: jasmine.createSpy('addAlias'),
includes: [],
createMatcher: function(pattern) {
return new Minimatch(pattern, {
dot: true
});
}
};
let description = new DependencyDescription('my-package', 'npm');
description.loaderConfig = {
path: '../node_modules/my-package',
name: 'my-package',
main: 'index',
lazyMain: true
};
mockfs({
'node_modules/my-package/lib/foo/package.json': '{"main":"fmain","module":"fmodule"}',
'node_modules/my-package/lib/foo/fmodule.js': 'some-content'
});
let sut = new DependencyInclusion(bundle, description);
sut._getProjectRoot = () => 'src';
sut.traceResource('lib/foo')
.then(() => {
expect(bundle.includes.length).toBe(1);
expect(bundle.includes[0].pattern).toBe('../node_modules/my-package/lib/foo/fmodule.js');
expect(bundle.addAlias).toHaveBeenCalledWith('my-package/lib/foo', 'my-package/lib/foo/fmodule');
done();
})
.catch(e => done.fail(e));
});
it('conventionalAliases removes common folder', done => {
let bundle = {
bundler: bundler,
@@ -44,6 +44,34 @@ describe('The file-system module', () => {
mockfs.restore();
});
describe('The isFile function', () => {
it('returns true for file', () => {
expect(fs.isFile(readFile.path)).toBeTruthy();
});
it('returns false for directory', () => {
expect(fs.isFile(readDir)).toBeFalsy();
});
it('returns false for non-existing file', () => {
expect(fs.isFile(path.join(readDir, 'non-existing'))).toBeFalsy();
});
});
describe('The isDirectory function', () => {
it('returns false for file', () => {
expect(fs.isDirectory(readFile.path)).toBeFalsy();
});
it('returns true for directory', () => {
expect(fs.isDirectory(readDir)).toBeTruthy();
});
it('returns false for non-existing file', () => {
expect(fs.isDirectory(path.join(readDir, 'non-existing'))).toBeFalsy();
});
});
describe('The stat() function', () => {
it('reads the stats for a directory', done => {
fs.stat(readDir).then(stats => {

0 comments on commit eef51f0

Please sign in to comment.