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

Add ability to specify the node_modules relative dir #689

Merged
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,22 @@ custom:
```
> Note that only relative path is supported at the moment.


`peerDependencies` of all above external dependencies will also be packed into the Serverless
artifact. By default, `node_modules` in the same directory as `package.json` (current working directory
or specified by`packagePath`) will be used.

However in some configuration (like monorepo), `node_modules` is in parent directory which is different from
where `package.json` is. Set `nodeModulesRelativeDir` to specify the relative directory where `node_modules` is.

```yaml
# serverless.yml
custom:
webpack:
includeModules:
nodeModulesRelativeDir: '../../' # relative path to current working directory.
```

#### Runtime dependencies

If a runtime dependency is detected that is found in the `devDependencies` section and
Expand Down
40 changes: 31 additions & 9 deletions lib/packExternalModules.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ function removeExcludedModules(modules, packageForceExcludes, log) {
* Resolve the needed versions of production dependencies for external modules.
* @this - The active plugin instance
*/
function getProdModules(externalModules, packagePath, dependencyGraph, forceExcludes) {
function getProdModules(externalModules, packagePath, nodeModulesRelativeDir, dependencyGraph, forceExcludes) {
const packageJsonPath = path.join(process.cwd(), packagePath);
const packageJson = require(packageJsonPath);
const prodModules = [];
Expand All @@ -83,14 +83,24 @@ function getProdModules(externalModules, packagePath, dependencyGraph, forceExcl
if (moduleVersion) {
prodModules.push(`${module.external}@${moduleVersion}`);

let nodeModulesBase = path.join(path.dirname(path.join(process.cwd(), packagePath)), 'node_modules');

if (nodeModulesRelativeDir) {
const customNodeModulesDir = path.join(process.cwd(), nodeModulesRelativeDir, 'node_modules');

if (fse.pathExistsSync(customNodeModulesDir)) {
nodeModulesBase = customNodeModulesDir;
} else {
this.serverless.cli.log(
`WARNING: ${customNodeModulesDir} dose not exist. Please check nodeModulesRelativeDir setting`
);
}
}

// Check if the module has any peer dependencies and include them too
try {
const modulePackagePath = path.join(
path.dirname(path.join(process.cwd(), packagePath)),
'node_modules',
module.external,
'package.json'
);
const modulePackagePath = path.join(nodeModulesBase, module.external, 'package.json');

const peerDependencies = require(modulePackagePath).peerDependencies;
if (!_.isEmpty(peerDependencies)) {
this.options.verbose && this.serverless.cli.log(`Adding explicit peers for dependency ${module.external}`);
Expand All @@ -115,14 +125,17 @@ function getProdModules(externalModules, packagePath, dependencyGraph, forceExcl
this,
_.map(peerDependencies, (value, key) => ({ external: key })),
packagePath,
nodeModulesRelativeDir,
dependencyGraph,
forceExcludes
);
Array.prototype.push.apply(prodModules, peerModules);
}
}
} catch (e) {
this.serverless.cli.log(`WARNING: Could not check for peer dependencies of ${module.external}`);
this.serverless.cli.log(
`WARNING: Could not check for peer dependencies of ${module.external}. Set nodeModulesRelativeDir if node_modules is in different directory.`
);
}
} else {
if (!packageJson.devDependencies || !packageJson.devDependencies[module.external]) {
Expand Down Expand Up @@ -254,6 +267,7 @@ module.exports = {
const packageForceIncludes = _.get(includes, 'forceInclude', []);
const packageForceExcludes = _.get(includes, 'forceExclude', []);
const packagePath = includes.packagePath || './package.json';
const nodeModulesRelativeDir = includes.nodeModulesRelativeDir;
const packageJsonPath = path.join(process.cwd(), packagePath);
const packageScripts = _.reduce(
this.configuration.packagerOptions.scripts || [],
Expand Down Expand Up @@ -296,7 +310,14 @@ module.exports = {
external: whitelistedPackage
}))
);
return getProdModules.call(this, externalModules, packagePath, dependencyGraph, packageForceExcludes);
return getProdModules.call(
this,
externalModules,
packagePath,
nodeModulesRelativeDir,
dependencyGraph,
packageForceExcludes
);
})
);
removeExcludedModules.call(this, compositeModules, packageForceExcludes, true);
Expand Down Expand Up @@ -384,6 +405,7 @@ module.exports = {
}))
),
packagePath,
nodeModulesRelativeDir,
dependencyGraph,
packageForceExcludes
);
Expand Down
242 changes: 150 additions & 92 deletions tests/packExternalModules.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ describe('packExternalModules', () => {
writeFileSyncStub.restore();
readFileSyncStub.restore();
fsExtraMock.pathExists.reset();
fsExtraMock.pathExistsSync.reset();
fsExtraMock.copy.reset();
sandbox.reset();
});
Expand Down Expand Up @@ -1201,102 +1202,159 @@ describe('packExternalModules', () => {
});

describe('optional behavior', () => {
before(() => {
const peerDepPackageJson = require('./data/package-peerdeps.json');
mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
mockery.registerMock(path.join(process.cwd(), 'package.json'), peerDepPackageJson);
// Mock request-promise package.json
const rpPackageJson = require('./data/rp-package-optional.json');
const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
mockery.registerMock(rpPackagePath, rpPackageJson);
});

after(() => {
mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock);
const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
mockery.deregisterMock(rpPackagePath);
});
const expectedCompositePackageJSON = {
name: 'test-service',
version: '1.0.0',
description: 'Packaged externals for test-service',
private: true,
scripts: {},
dependencies: {
bluebird: '^3.5.0',
'request-promise': '^4.2.1',
request: '^2.82.0'
}
};
const expectedPackageJSON = {
name: 'test-service',
version: '1.0.0',
description: 'Packaged externals for test-service',
private: true,
scripts: {},
dependencies: {
bluebird: '^3.5.0',
'request-promise': '^4.2.1',
request: '^2.82.0'
}
};

it('should skip optional peer dependencies', () => {
const expectedCompositePackageJSON = {
name: 'test-service',
version: '1.0.0',
description: 'Packaged externals for test-service',
private: true,
scripts: {},
dependencies: {
bluebird: '^3.5.0',
'request-promise': '^4.2.1',
request: '^2.82.0'
}
};
const expectedPackageJSON = {
name: 'test-service',
version: '1.0.0',
description: 'Packaged externals for test-service',
private: true,
scripts: {},
dependencies: {
bluebird: '^3.5.0',
'request-promise': '^4.2.1',
request: '^2.82.0'
const dependencyGraph = require('./data/npm-ls-peerdeps.json');
const peerDepStats = {
stats: [
{
compilation: new WebpackCompilationMock([
{
identifier: _.constant('"crypto"')
},
{
identifier: _.constant('"uuid/v4"')
},
{
identifier: _.constant('"mockery"')
},
{
identifier: _.constant('"@scoped/vendor/module1"')
},
{
identifier: _.constant('external "bluebird"')
},
{
identifier: _.constant('external "request-promise"')
}
])
}
};
]
};

const dependencyGraph = require('./data/npm-ls-peerdeps.json');
const peerDepStats = {
stats: [
{
compilation: new WebpackCompilationMock([
{
identifier: _.constant('"crypto"')
},
{
identifier: _.constant('"uuid/v4"')
},
{
identifier: _.constant('"mockery"')
},
{
identifier: _.constant('"@scoped/vendor/module1"')
},
{
identifier: _.constant('external "bluebird"')
},
{
identifier: _.constant('external "request-promise"')
}
])
}
]
};
describe('without nodeModulesRelativeDir', () => {
before(() => {
const peerDepPackageJson = require('./data/package-peerdeps.json');
mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
mockery.registerMock(path.join(process.cwd(), 'package.json'), peerDepPackageJson);
// Mock request-promise package.json
const rpPackageJson = require('./data/rp-package-optional.json');
const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
mockery.registerMock(rpPackagePath, rpPackageJson);
});

after(() => {
mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock);
const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
mockery.deregisterMock(rpPackagePath);
});

it('should skip optional peer dependencies', () => {
module.webpackOutputPath = 'outputPath';
fsExtraMock.pathExists.yields(null, false);
fsExtraMock.copy.yields();
packagerMock.getProdDependencies.returns(BbPromise.resolve(dependencyGraph));
packagerMock.install.returns(BbPromise.resolve());
packagerMock.prune.returns(BbPromise.resolve());
packagerMock.runScripts.returns(BbPromise.resolve());
module.compileStats = peerDepStats;
return expect(module.packExternalModules()).to.be.fulfilled.then(() =>
BbPromise.all([
// The module package JSON and the composite one should have been stored
expect(writeFileSyncStub).to.have.been.calledTwice,
expect(writeFileSyncStub.firstCall.args[1]).to.equal(
JSON.stringify(expectedCompositePackageJSON, null, 2)
),
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
// The modules should have been copied
expect(fsExtraMock.copy).to.have.been.calledOnce,
// npm ls and npm prune should have been called
expect(packagerMock.getProdDependencies).to.have.been.calledOnce,
expect(packagerMock.install).to.have.been.calledOnce,
expect(packagerMock.prune).to.have.been.calledOnce,
expect(packagerMock.runScripts).to.have.been.calledOnce
])
);
});
});

module.webpackOutputPath = 'outputPath';
fsExtraMock.pathExists.yields(null, false);
fsExtraMock.copy.yields();
packagerMock.getProdDependencies.returns(BbPromise.resolve(dependencyGraph));
packagerMock.install.returns(BbPromise.resolve());
packagerMock.prune.returns(BbPromise.resolve());
packagerMock.runScripts.returns(BbPromise.resolve());
module.compileStats = peerDepStats;
return expect(module.packExternalModules()).to.be.fulfilled.then(() =>
BbPromise.all([
// The module package JSON and the composite one should have been stored
expect(writeFileSyncStub).to.have.been.calledTwice,
expect(writeFileSyncStub.firstCall.args[1]).to.equal(
JSON.stringify(expectedCompositePackageJSON, null, 2)
),
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
// The modules should have been copied
expect(fsExtraMock.copy).to.have.been.calledOnce,
// npm ls and npm prune should have been called
expect(packagerMock.getProdDependencies).to.have.been.calledOnce,
expect(packagerMock.install).to.have.been.calledOnce,
expect(packagerMock.prune).to.have.been.calledOnce,
expect(packagerMock.runScripts).to.have.been.calledOnce
])
);
describe('with nodeModulesRelativeDir', () => {
before(() => {
const peerDepPackageJson = require('./data/package-peerdeps.json');
mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
mockery.registerMock(path.join(process.cwd(), 'package.json'), peerDepPackageJson);
// Mock request-promise package.json
const rpPackageJson = require('./data/rp-package-optional.json');
const rpPackagePath = path.join(process.cwd(), '../../', 'node_modules', 'request-promise', 'package.json');
mockery.registerMock(rpPackagePath, rpPackageJson);
});

after(() => {
mockery.deregisterMock(path.join(process.cwd(), 'package.json'));
mockery.registerMock(path.join(process.cwd(), 'package.json'), packageMock);
const rpPackagePath = path.join(process.cwd(), 'node_modules', 'request-promise', 'package.json');
mockery.deregisterMock(rpPackagePath);
});

it('should skip optional peer dependencies', () => {
module.configuration = new Configuration({
webpack: {
includeModules: {
nodeModulesRelativeDir: '../../'
}
}
});
module.webpackOutputPath = 'outputPath';
fsExtraMock.pathExists.yields(null, false);
fsExtraMock.pathExistsSync.returns(true);
fsExtraMock.copy.yields();
packagerMock.getProdDependencies.returns(BbPromise.resolve(dependencyGraph));
packagerMock.install.returns(BbPromise.resolve());
packagerMock.prune.returns(BbPromise.resolve());
packagerMock.runScripts.returns(BbPromise.resolve());
module.compileStats = peerDepStats;
return expect(module.packExternalModules()).to.be.fulfilled.then(() =>
BbPromise.all([
// The module package JSON and the composite one should have been stored
expect(writeFileSyncStub).to.have.been.calledTwice,
expect(writeFileSyncStub.firstCall.args[1]).to.equal(
JSON.stringify(expectedCompositePackageJSON, null, 2)
),
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
// The modules should have been copied
expect(fsExtraMock.copy).to.have.been.calledOnce,
// npm ls and npm prune should have been called
expect(packagerMock.getProdDependencies).to.have.been.calledOnce,
expect(packagerMock.install).to.have.been.calledOnce,
expect(packagerMock.prune).to.have.been.calledOnce,
expect(packagerMock.runScripts).to.have.been.calledOnce
])
);
});
});
});
});
Expand Down