Skip to content

Commit

Permalink
remove path dependency of node_modules on package.json
Browse files Browse the repository at this point in the history
  • Loading branch information
ypxing committed Apr 22, 2021
1 parent 2371550 commit 1badc19
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 101 deletions.
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

0 comments on commit 1badc19

Please sign in to comment.