Skip to content

Commit 2728d89

Browse files
authored
Merge pull request #281 from serverless-heaven/fix-npm-file-references
Fix/Support npm file references
2 parents 670df6b + c0c12d2 commit 2728d89

File tree

4 files changed

+232
-16
lines changed

4 files changed

+232
-16
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,13 @@ custom:
246246
If you specify a module in both arrays, `forceInclude` and `forceExclude`, the
247247
exclude wins and the module will not be packaged.
248248

249+
#### Local modules
250+
251+
You can use `file:` version references in your `package.json` to use a node module
252+
from a local folder (e.g. `"mymodule": "file:../../myOtherProject/mymodule"`).
253+
With that you can do test deployments from the local machine with different
254+
module versions or modules before they are published officially.
255+
249256
#### Examples
250257
251258
You can find an example setups in the [`examples`][link-examples] folder.

lib/packExternalModules.js

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

10+
function rebaseFileReferences(pathToPackageRoot, moduleVersion) {
11+
if (/^file:[^/]{2}/.test(moduleVersion)) {
12+
const filePath = _.replace(moduleVersion, /^file:/, '');
13+
return _.replace(`file:${pathToPackageRoot}/${filePath}`, /\\/g, '/');
14+
}
15+
16+
return moduleVersion;
17+
}
18+
19+
function rebasePackageLock(pathToPackageRoot, module) {
20+
if (module.version) {
21+
module.version = rebaseFileReferences(pathToPackageRoot, module.version);
22+
}
23+
24+
if (module.dependencies) {
25+
_.forIn(module.dependencies, moduleDependency => {
26+
rebasePackageLock(pathToPackageRoot, moduleDependency);
27+
});
28+
}
29+
}
30+
1031
/**
1132
* Add the given modules to a package json's dependencies.
1233
*/
13-
function addModulesToPackageJson(externalModules, packageJson) {
34+
function addModulesToPackageJson(externalModules, packageJson, pathToPackageRoot) {
1435
_.forEach(externalModules, externalModule => {
1536
const splitModule = _.split(externalModule, '@');
1637
// If we have a scoped module we have to re-add the @
1738
if (_.startsWith(externalModule, '@')) {
1839
splitModule.splice(0, 1);
1940
splitModule[0] = '@' + splitModule[0];
2041
}
21-
const moduleVersion = _.join(_.tail(splitModule), '@');
42+
let moduleVersion = _.join(_.tail(splitModule), '@');
43+
// We have to rebase file references to the target package.json
44+
moduleVersion = rebaseFileReferences(pathToPackageRoot, moduleVersion);
2245
packageJson.dependencies = packageJson.dependencies || {};
2346
packageJson.dependencies[_.first(splitModule)] = moduleVersion;
2447
});
@@ -247,7 +270,8 @@ module.exports = {
247270
description: `Packaged externals for ${this.serverless.service.service}`,
248271
private: true
249272
};
250-
addModulesToPackageJson(compositeModules, compositePackage);
273+
const relPath = path.relative(compositeModulePath, path.dirname(packageJsonPath));
274+
addModulesToPackageJson(compositeModules, compositePackage, relPath);
251275
this.serverless.utils.writeFileSync(compositePackageJson, JSON.stringify(compositePackage, null, 2));
252276

253277
// (1.a.2) Copy package-lock.json if it exists, to prevent unwanted upgrades
@@ -256,8 +280,21 @@ module.exports = {
256280
.then(exists => {
257281
if (exists) {
258282
this.serverless.cli.log('Package lock found - Using locked versions');
259-
return BbPromise.fromCallback(cb => fse.copy(packageLockPath, path.join(compositeModulePath, 'package-lock.json'), cb))
260-
.catch(err => this.serverless.cli.log(`Warning: Could not copy lock file: ${err.message}`));
283+
try {
284+
const packageLockJson = this.serverless.utils.readFileSync(packageLockPath);
285+
/**
286+
* We should not be modifying 'package-lock.json'
287+
* because this file should be treat as internal to npm.
288+
*
289+
* Rebase package-lock is a temporary workaround and must be
290+
* removed as soon as https://github.com/npm/npm/issues/19183 gets fixed.
291+
*/
292+
rebasePackageLock(relPath, packageLockJson);
293+
294+
this.serverless.utils.writeFileSync(path.join(compositeModulePath, 'package-lock.json'), JSON.stringify(packageLockJson, null, 2));
295+
} catch(err) {
296+
this.serverless.cli.log(`Warning: Could not read lock file: ${err.message}`);
297+
}
261298
}
262299
return BbPromise.resolve();
263300
})
@@ -288,7 +325,8 @@ module.exports = {
288325
_.map(packageForceIncludes, whitelistedPackage => ({ external: whitelistedPackage }))
289326
), packagePath, dependencyGraph);
290327
removeExcludedModules.call(this, prodModules, packageForceExcludes);
291-
addModulesToPackageJson(prodModules, modulePackage);
328+
const relPath = path.relative(modulePath, path.dirname(packageJsonPath));
329+
addModulesToPackageJson(prodModules, modulePackage, relPath);
292330
this.serverless.utils.writeFileSync(modulePackageJson, JSON.stringify(modulePackage, null, 2));
293331

294332
// GOOGLE: Copy modules only if not google-cloud-functions
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"dependencies": {
3+
"archiver": "^2.0.0",
4+
"bluebird": "^3.4.0",
5+
"fs-extra": "^0.26.7",
6+
"glob": "^7.1.2",
7+
"localmodule": "file:../../mymodule",
8+
"lodash": "^4.17.4",
9+
"npm-programmatic": "0.0.5",
10+
"uuid": "^5.4.1",
11+
"ts-node": "^3.2.0",
12+
"@scoped/vendor": "1.0.0",
13+
"pg": "^4.3.5"
14+
},
15+
"devDependencies": {
16+
"babel-eslint": "^7.2.3",
17+
"chai": "^4.1.0",
18+
"chai-as-promised": "^7.1.1",
19+
"eslint": "^4.3.0",
20+
"eslint-plugin-import": "^2.7.0",
21+
"eslint-plugin-lodash": "^2.4.4",
22+
"eslint-plugin-promise": "^3.5.0",
23+
"istanbul": "^0.4.5",
24+
"mocha": "^3.4.2",
25+
"mockery": "^2.1.0",
26+
"serverless": "^1.17.0",
27+
"sinon": "^2.3.8",
28+
"sinon-chai": "^2.12.0"
29+
},
30+
"peerDependencies": {
31+
"webpack": "*"
32+
}
33+
}

tests/packExternalModules.test.js

Lines changed: 148 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const Serverless = require('serverless');
1212
const childProcessMockFactory = require('./mocks/child_process.mock');
1313
const fsExtraMockFactory = require('./mocks/fs-extra.mock');
1414
const packageMock = require('./mocks/package.mock.json');
15+
const packageLocalRefMock = require('./mocks/packageLocalRef.mock.json');
1516

1617
chai.use(require('chai-as-promised'));
1718
chai.use(require('sinon-chai'));
@@ -29,6 +30,7 @@ describe('packExternalModules', () => {
2930
let fsExtraMock;
3031
// Serverless stubs
3132
let writeFileSyncStub;
33+
let readFileSyncStub;
3234

3335
before(() => {
3436
sandbox = sinon.sandbox.create();
@@ -59,6 +61,7 @@ describe('packExternalModules', () => {
5961
_.set(serverless, 'service.service', 'test-service');
6062

6163
writeFileSyncStub = sandbox.stub(serverless.utils, 'writeFileSync');
64+
readFileSyncStub = sandbox.stub(serverless.utils, 'readFileSync');
6265
_.set(serverless, 'service.custom.webpackIncludeModules', true);
6366

6467
module = _.assign({
@@ -72,6 +75,7 @@ describe('packExternalModules', () => {
7275
afterEach(() => {
7376
// Reset all counters and restore all stubbed functions
7477
writeFileSyncStub.reset();
78+
readFileSyncStub.reset();
7579
childProcessMock.exec.reset();
7680
fsExtraMock.pathExists.reset();
7781
fsExtraMock.copy.reset();
@@ -151,6 +155,50 @@ describe('packExternalModules', () => {
151155
}
152156
]
153157
};
158+
const statsWithFileRef = {
159+
stats: [
160+
{
161+
compilation: {
162+
chunks: [
163+
{
164+
modules: [
165+
{
166+
identifier: _.constant('"crypto"')
167+
},
168+
{
169+
identifier: _.constant('"uuid/v4"')
170+
},
171+
{
172+
identifier: _.constant('external "eslint"')
173+
},
174+
{
175+
identifier: _.constant('"mockery"')
176+
},
177+
{
178+
identifier: _.constant('"@scoped/vendor/module1"')
179+
},
180+
{
181+
identifier: _.constant('external "@scoped/vendor/module2"')
182+
},
183+
{
184+
identifier: _.constant('external "uuid/v4"')
185+
},
186+
{
187+
identifier: _.constant('external "localmodule"')
188+
},
189+
{
190+
identifier: _.constant('external "bluebird"')
191+
},
192+
]
193+
}
194+
],
195+
compiler: {
196+
outputPath: '/my/Service/Path/.webpack/service'
197+
}
198+
}
199+
}
200+
]
201+
};
154202

155203
it('should do nothing if webpackIncludeModules is not set', () => {
156204
_.unset(serverless, 'service.custom.webpackIncludeModules');
@@ -212,6 +260,102 @@ describe('packExternalModules', () => {
212260
]));
213261
});
214262

263+
it('should rebase file references', () => {
264+
const expectedLocalModule = 'file:../../locals/../../mymodule';
265+
const expectedCompositePackageJSON = {
266+
name: 'test-service',
267+
version: '1.0.0',
268+
description: 'Packaged externals for test-service',
269+
private: true,
270+
dependencies: {
271+
'@scoped/vendor': '1.0.0',
272+
uuid: '^5.4.1',
273+
localmodule: 'file:../../locals/../../mymodule',
274+
bluebird: '^3.4.0'
275+
}
276+
};
277+
const expectedPackageJSON = {
278+
dependencies: {
279+
'@scoped/vendor': '1.0.0',
280+
uuid: '^5.4.1',
281+
localmodule: expectedLocalModule,
282+
bluebird: '^3.4.0'
283+
}
284+
};
285+
286+
const fakePackageLockJSON = {
287+
name: 'test-service',
288+
version: '1.0.0',
289+
description: 'Packaged externals for test-service',
290+
private: true,
291+
dependencies: {
292+
'@scoped/vendor': '1.0.0',
293+
uuid: {
294+
version: '^5.4.1'
295+
},
296+
bluebird: {
297+
version: '^3.4.0'
298+
},
299+
localmodule: {
300+
version: 'file:../../mymodule'
301+
}
302+
}
303+
};
304+
const expectedPackageLockJSON = {
305+
name: 'test-service',
306+
version: '1.0.0',
307+
description: 'Packaged externals for test-service',
308+
private: true,
309+
dependencies: {
310+
'@scoped/vendor': '1.0.0',
311+
uuid: {
312+
version: '^5.4.1'
313+
},
314+
bluebird: {
315+
version: '^3.4.0'
316+
},
317+
localmodule: {
318+
version: expectedLocalModule
319+
}
320+
}
321+
};
322+
323+
_.set(serverless, 'service.custom.webpackIncludeModules.packagePath', path.join('locals', 'package.json'));
324+
module.webpackOutputPath = 'outputPath';
325+
readFileSyncStub.returns(fakePackageLockJSON);
326+
fsExtraMock.pathExists.yields(null, true);
327+
fsExtraMock.copy.yields();
328+
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
329+
childProcessMock.exec.onSecondCall().yields(null, '', '');
330+
childProcessMock.exec.onThirdCall().yields();
331+
module.compileStats = statsWithFileRef;
332+
333+
sandbox.stub(process, 'cwd').returns(path.join('/my/Service/Path'));
334+
mockery.registerMock(path.join(process.cwd(), 'locals', 'package.json'), packageLocalRefMock);
335+
336+
return expect(module.packExternalModules()).to.be.fulfilled
337+
.then(() => BbPromise.all([
338+
// The module package JSON and the composite one should have been stored
339+
expect(writeFileSyncStub).to.have.been.calledThrice,
340+
expect(writeFileSyncStub.firstCall.args[1]).to.equal(JSON.stringify(expectedCompositePackageJSON, null, 2)),
341+
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageLockJSON, null, 2)),
342+
expect(writeFileSyncStub.thirdCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
343+
// The modules should have been copied
344+
expect(fsExtraMock.copy).to.have.been.calledOnce,
345+
// npm ls and npm prune should have been called
346+
expect(childProcessMock.exec).to.have.been.calledThrice,
347+
expect(childProcessMock.exec.firstCall).to.have.been.calledWith(
348+
'npm ls -prod -json -depth=1'
349+
),
350+
expect(childProcessMock.exec.secondCall).to.have.been.calledWith(
351+
'npm install'
352+
),
353+
expect(childProcessMock.exec.thirdCall).to.have.been.calledWith(
354+
'npm prune'
355+
)
356+
]));
357+
});
358+
215359
it('should skip module copy for Google provider', () => {
216360
const expectedCompositePackageJSON = {
217361
name: 'test-service',
@@ -601,10 +745,7 @@ describe('packExternalModules', () => {
601745
expect(writeFileSyncStub.firstCall.args[1]).to.equal(JSON.stringify(expectedCompositePackageJSON, null, 2)),
602746
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
603747
// The modules should have been copied
604-
expect(fsExtraMock.copy).to.have.been.calledTwice,
605-
expect(fsExtraMock.copy.firstCall).to.have.been.calledWith(
606-
sinon.match(/package-lock.json$/)
607-
),
748+
expect(fsExtraMock.copy).to.have.been.calledOnce,
608749
// npm ls and npm prune should have been called
609750
expect(childProcessMock.exec).to.have.been.calledThrice,
610751
expect(childProcessMock.exec.firstCall).to.have.been.calledWith(
@@ -640,9 +781,9 @@ describe('packExternalModules', () => {
640781
};
641782

642783
module.webpackOutputPath = 'outputPath';
784+
readFileSyncStub.throws(new Error('Failed to read package-lock.json'));
643785
fsExtraMock.pathExists.yields(null, true);
644-
fsExtraMock.copy.onFirstCall().yields(new Error('Failed to read package-lock.json'));
645-
fsExtraMock.copy.onSecondCall().yields();
786+
fsExtraMock.copy.onFirstCall().yields();
646787
childProcessMock.exec.onFirstCall().yields(null, '{}', '');
647788
childProcessMock.exec.onSecondCall().yields(null, '', '');
648789
childProcessMock.exec.onThirdCall().yields();
@@ -654,10 +795,7 @@ describe('packExternalModules', () => {
654795
expect(writeFileSyncStub.firstCall.args[1]).to.equal(JSON.stringify(expectedCompositePackageJSON, null, 2)),
655796
expect(writeFileSyncStub.secondCall.args[1]).to.equal(JSON.stringify(expectedPackageJSON, null, 2)),
656797
// The modules should have been copied
657-
expect(fsExtraMock.copy).to.have.been.calledTwice,
658-
expect(fsExtraMock.copy.firstCall).to.have.been.calledWith(
659-
sinon.match(/package-lock.json$/)
660-
),
798+
expect(fsExtraMock.copy).to.have.been.calledOnce,
661799
// npm ls and npm prune should have been called
662800
expect(childProcessMock.exec).to.have.been.calledThrice,
663801
expect(childProcessMock.exec.firstCall).to.have.been.calledWith(

0 commit comments

Comments
 (0)