Skip to content

Commit c2a368b

Browse files
authored
Support forceExclude to exclude dependencies (#248)
* Support forceExclude to exclude dependencies * Added unit test * Added documentation
1 parent 9f6fba8 commit c2a368b

File tree

3 files changed

+122
-21
lines changed

3 files changed

+122
-21
lines changed

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ custom:
192192
```
193193
> Note that only relative path is supported at the moment.
194194

195+
#### Forced inclusion
195196

196197
Sometimes it might happen that you use dynamic requires in your code, i.e. you
197198
require modules that are only known at runtime. Webpack is not able to detect
@@ -209,6 +210,26 @@ custom:
209210
- module2
210211
```
211212

213+
#### Forced exclusion
214+
215+
You can forcefully exclude detected external modules, e.g. if you have a module
216+
in your dependencies that is already installed at your provider's environment.
217+
218+
Just add them to the `forceExclude` array property and they will not be packaged.
219+
220+
```yaml
221+
# serverless.yml
222+
custom:
223+
webpackIncludeModules:
224+
forceExclude:
225+
- module1
226+
- module2
227+
```
228+
229+
If you specify a module in both arrays, `forceInclude` and `forceExclude`, the
230+
exclude wins and the module will not be packaged.
231+
232+
#### Examples
212233

213234
You can find an example setups in the [`examples`][link-examples] folder.
214235

lib/packExternalModules.js

Lines changed: 48 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,48 @@ const childProcess = require('child_process');
77
const fse = require('fs-extra');
88
const isBuiltinModule = require('is-builtin-module');
99

10+
/**
11+
* Add the given modules to a package json's dependencies.
12+
*/
13+
function addModulesToPackageJson(externalModules, packageJson) {
14+
_.forEach(externalModules, externalModule => {
15+
const splitModule = _.split(externalModule, '@');
16+
// If we have a scoped module we have to re-add the @
17+
if (_.startsWith(externalModule, '@')) {
18+
splitModule.splice(0, 1);
19+
splitModule[0] = '@' + splitModule[0];
20+
}
21+
const moduleVersion = _.join(_.tail(splitModule), '@');
22+
packageJson.dependencies = packageJson.dependencies || {};
23+
packageJson.dependencies[_.first(splitModule)] = moduleVersion;
24+
});
25+
}
26+
27+
/**
28+
* Remove a given list of excluded modules from a module list
29+
* @this - The active plugin instance
30+
*/
31+
function removeExcludedModules(modules, packageForceExcludes, log) {
32+
const excludedModules = _.remove(modules, externalModule => {
33+
const splitModule = _.split(externalModule, '@');
34+
// If we have a scoped module we have to re-add the @
35+
if (_.startsWith(externalModule, '@')) {
36+
splitModule.splice(0, 1);
37+
splitModule[0] = '@' + splitModule[0];
38+
}
39+
const moduleName = _.first(splitModule);
40+
return _.includes(packageForceExcludes, moduleName);
41+
});
42+
43+
if (log && !_.isEmpty(excludedModules)) {
44+
this.serverless.cli.log(`Excluding external modules: ${_.join(excludedModules, ', ')}`);
45+
}
46+
}
47+
48+
/**
49+
* Resolve the needed versions of production depenencies for external modules.
50+
* @this - The active plugin instance
51+
*/
1052
function getProdModules(externalModules, packagePath, dependencyGraph) {
1153
const packageJsonPath = path.join(process.cwd(), packagePath);
1254
const packageJson = require(packageJsonPath);
@@ -127,7 +169,9 @@ module.exports = {
127169
return BbPromise.resolve(stats);
128170
}
129171

172+
// Read plugin configuration
130173
const packageForceIncludes = _.get(includes, 'forceInclude', []);
174+
const packageForceExcludes = _.get(includes, 'forceExclude', []);
131175
const packagePath = includes.packagePath || './package.json';
132176
const packageJsonPath = path.join(process.cwd(), packagePath);
133177

@@ -182,6 +226,7 @@ module.exports = {
182226
);
183227
return getProdModules.call(this, externalModules, packagePath, dependencyGraph);
184228
}));
229+
removeExcludedModules.call(this, compositeModules, packageForceExcludes, true);
185230

186231
if (_.isEmpty(compositeModules)) {
187232
// The compiled code does not reference any external modules at all
@@ -200,17 +245,7 @@ module.exports = {
200245
description: `Packaged externals for ${this.serverless.service.service}`,
201246
private: true
202247
};
203-
_.forEach(compositeModules, compositeModule => {
204-
const splitModule = _.split(compositeModule, '@');
205-
// If we have a scoped module we have to re-add the @
206-
if (_.startsWith(compositeModule, '@')) {
207-
splitModule.splice(0, 1);
208-
splitModule[0] = '@' + splitModule[0];
209-
}
210-
const moduleVersion = _.join(_.tail(splitModule), '@');
211-
compositePackage.dependencies = compositePackage.dependencies || {};
212-
compositePackage.dependencies[_.first(splitModule)] = moduleVersion;
213-
});
248+
addModulesToPackageJson(compositeModules, compositePackage);
214249
this.serverless.utils.writeFileSync(compositePackageJson, JSON.stringify(compositePackage, null, 2));
215250

216251
// (1.a.2) Copy package-lock.json if it exists, to prevent unwanted upgrades
@@ -250,16 +285,8 @@ module.exports = {
250285
getExternalModules.call(this, compileStats),
251286
_.map(packageForceIncludes, whitelistedPackage => ({ external: whitelistedPackage }))
252287
), packagePath, dependencyGraph);
253-
_.forEach(prodModules, prodModule => {
254-
const splitModule = _.split(prodModule, '@');
255-
// If we have a scoped module we have to re-add the @
256-
if (_.startsWith(prodModule, '@')) {
257-
splitModule.splice(0, 1);
258-
splitModule[0] = '@' + splitModule[0];
259-
}
260-
const moduleVersion = _.join(_.tail(splitModule), '@');
261-
modulePackage.dependencies[_.first(splitModule)] = moduleVersion;
262-
});
288+
removeExcludedModules.call(this, prodModules, packageForceExcludes);
289+
addModulesToPackageJson(prodModules, modulePackage);
263290
this.serverless.utils.writeFileSync(modulePackageJson, JSON.stringify(modulePackage, null, 2));
264291

265292
// Copy modules

tests/packExternalModules.test.js

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,59 @@ describe('packExternalModules', () => {
458458
]));
459459
});
460460

461+
it('should exclude external modules when forced', () => {
462+
const expectedCompositePackageJSON = {
463+
name: 'test-service',
464+
version: '1.0.0',
465+
description: 'Packaged externals for test-service',
466+
private: true,
467+
dependencies: {
468+
'@scoped/vendor': '1.0.0',
469+
bluebird: '^3.4.0',
470+
pg: '^4.3.5'
471+
}
472+
};
473+
const expectedPackageJSON = {
474+
dependencies: {
475+
'@scoped/vendor': '1.0.0',
476+
bluebird: '^3.4.0',
477+
pg: '^4.3.5'
478+
}
479+
};
480+
serverless.service.custom = {
481+
webpackIncludeModules: {
482+
forceInclude: ['pg'],
483+
forceExclude: ['uuid']
484+
}
485+
};
486+
module.webpackOutputPath = 'outputPath';
487+
fsExtraMock.pathExists.yields(null, false);
488+
fsExtraMock.copy.yields();
489+
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
490+
childProcessMock.exec.onSecondCall().yields(null, '', '');
491+
childProcessMock.exec.onThirdCall().yields();
492+
return expect(module.packExternalModules(stats)).to.be.fulfilled
493+
.then(() => BbPromise.all([
494+
// The module package JSON and the composite one should have been stored
495+
expect(writeFileSyncStub).to.have.been.calledTwice,
496+
expect(writeFileSyncStub.firstCall.args[1]).to.equal(JSON.stringify(expectedCompositePackageJSON, null, 2)),
497+
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
498+
// The modules should have been copied
499+
expect(fsExtraMock.copy).to.have.been.calledOnce,
500+
// npm ls and npm prune should have been called
501+
expect(childProcessMock.exec).to.have.been.calledThrice,
502+
expect(childProcessMock.exec.firstCall).to.have.been.calledWith(
503+
'npm ls -prod -json -depth=1'
504+
),
505+
expect(childProcessMock.exec.secondCall).to.have.been.calledWith(
506+
'npm install'
507+
),
508+
expect(childProcessMock.exec.thirdCall).to.have.been.calledWith(
509+
'npm prune'
510+
)
511+
]));
512+
});
513+
461514
it('should read package-lock if found', () => {
462515
const expectedCompositePackageJSON = {
463516
name: 'test-service',

0 commit comments

Comments
 (0)