Skip to content

Commit ae05049

Browse files
authored
fix(twilio-run): support exact dependency ranges in templates (#370)
npm install does not preserve the passed dependency ranges resulting in templates that have exact dependency versions specified but get added with a semver range to the package.json. This change installs exact dependencies separately with the --save-exact flag. fix #365
1 parent b5d6771 commit ae05049

File tree

2 files changed

+148
-6
lines changed

2 files changed

+148
-6
lines changed

packages/twilio-run/__tests__/templating/filesystem.test.ts

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,129 @@ test('installation with a dependency file', async () => {
376376
);
377377
});
378378

379+
test('installation with a dependency file with exact dependencies', async () => {
380+
// The typing of `got` is not exactly correct for this - it expects a
381+
// buffer but depending on inputs `got` can actually return an object.
382+
// @ts-ignore
383+
mocked(got).mockImplementation(() =>
384+
// @ts-ignore
385+
Promise.resolve({
386+
body: { dependencies: { foo: '1.0.0', got: '6.9.0' } },
387+
})
388+
);
389+
390+
// For this test, getFirstMatchingDirectory never errors.
391+
mocked(
392+
fsHelpers.getFirstMatchingDirectory
393+
).mockImplementation((basePath: string, directories: Array<string>): string =>
394+
path.join(basePath, directories[0])
395+
);
396+
397+
await writeFiles(
398+
[
399+
{
400+
name: 'package.json',
401+
type: 'package.json',
402+
content: 'https://example.com/package.json',
403+
directory: '',
404+
},
405+
{
406+
name: '.env',
407+
type: '.env',
408+
content: 'https://example.com/.env',
409+
directory: '',
410+
},
411+
],
412+
'./testing/',
413+
'example',
414+
'hello'
415+
);
416+
417+
expect(downloadFile).toHaveBeenCalledTimes(1);
418+
expect(downloadFile).toHaveBeenCalledWith(
419+
'https://example.com/.env',
420+
join('testing', '.env')
421+
);
422+
423+
expect(got).toHaveBeenCalledTimes(1);
424+
expect(got).toHaveBeenCalledWith('https://example.com/package.json', {
425+
json: true,
426+
});
427+
428+
expect(install).toHaveBeenCalledTimes(1);
429+
expect(install).toHaveBeenCalledWith(
430+
{ foo: '1.0.0', got: '6.9.0' },
431+
{ cwd: './testing/', exact: true }
432+
);
433+
});
434+
435+
test('installation with a dependency file with mixed dependencies', async () => {
436+
// The typing of `got` is not exactly correct for this - it expects a
437+
// buffer but depending on inputs `got` can actually return an object.
438+
// @ts-ignore
439+
mocked(got).mockImplementation(() =>
440+
// @ts-ignore
441+
Promise.resolve({
442+
body: {
443+
dependencies: {
444+
foo: '^1.0.0',
445+
got: '6.9.0',
446+
twilio: '^3',
447+
'@twilio/runtime-handler': '1.2.0-rc.3',
448+
},
449+
},
450+
})
451+
);
452+
453+
// For this test, getFirstMatchingDirectory never errors.
454+
mocked(
455+
fsHelpers.getFirstMatchingDirectory
456+
).mockImplementation((basePath: string, directories: Array<string>): string =>
457+
path.join(basePath, directories[0])
458+
);
459+
460+
await writeFiles(
461+
[
462+
{
463+
name: 'package.json',
464+
type: 'package.json',
465+
content: 'https://example.com/package.json',
466+
directory: '',
467+
},
468+
{
469+
name: '.env',
470+
type: '.env',
471+
content: 'https://example.com/.env',
472+
directory: '',
473+
},
474+
],
475+
'./testing/',
476+
'example',
477+
'hello'
478+
);
479+
480+
expect(downloadFile).toHaveBeenCalledTimes(1);
481+
expect(downloadFile).toHaveBeenCalledWith(
482+
'https://example.com/.env',
483+
join('testing', '.env')
484+
);
485+
486+
expect(got).toHaveBeenCalledTimes(1);
487+
expect(got).toHaveBeenCalledWith('https://example.com/package.json', {
488+
json: true,
489+
});
490+
491+
expect(install).toHaveBeenCalledTimes(2);
492+
expect(install).toHaveBeenCalledWith(
493+
{ foo: '^1.0.0', twilio: '^3' },
494+
{ cwd: './testing/' }
495+
);
496+
expect(install).toHaveBeenCalledWith(
497+
{ got: '6.9.0', '@twilio/runtime-handler': '1.2.0-rc.3' },
498+
{ cwd: './testing/', exact: true }
499+
);
500+
});
501+
379502
test('installation with an existing dot-env file', async () => {
380503
mocked(fileExists).mockReturnValue(Promise.resolve(true));
381504
mocked(readFile).mockReturnValue(Promise.resolve('# Comment\nFOO=BAR\n'));
@@ -431,7 +554,7 @@ test('installation with overlapping function files throws errors before writing'
431554
path.join(basePath, directories[0])
432555
);
433556

434-
mocked(fileExists).mockImplementation(p =>
557+
mocked(fileExists).mockImplementation((p) =>
435558
Promise.resolve(p == join('functions', 'example', 'hello.js'))
436559
);
437560

@@ -470,7 +593,7 @@ test('installation with overlapping asset files throws errors before writing', a
470593
path.join(basePath, directories[0])
471594
);
472595

473-
mocked(fileExists).mockImplementation(p =>
596+
mocked(fileExists).mockImplementation((p) =>
474597
Promise.resolve(p == join('assets', 'example', 'hello.wav'))
475598
);
476599

packages/twilio-run/src/templating/filesystem.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import got from 'got';
55
import Listr, { ListrTask } from 'listr';
66
import path from 'path';
77
import { install, InstallResult } from 'pkg-install';
8+
import semver from 'semver';
9+
import { PackageJson } from 'type-fest';
810
import {
911
downloadFile,
1012
fileExists,
@@ -35,12 +37,12 @@ async function writeEnvFile(
3537
const newFlags = dotenv.parse(newContent);
3638

3739
const functionKeys = Object.keys(newFlags);
38-
const existingKeys = functionKeys.filter(key =>
40+
const existingKeys = functionKeys.filter((key) =>
3941
currentFlags.hasOwnProperty(key)
4042
);
4143
const updatedContent = newContent
4244
.split('\n')
43-
.map(line => {
45+
.map((line) => {
4446
const name = line.substr(0, line.indexOf('='));
4547
if (existingKeys.includes(name)) {
4648
return '# ' + line;
@@ -65,7 +67,24 @@ async function installDependencies(
6567
targetDir: string
6668
): Promise<InstallResult | undefined> {
6769
const pkgContent = await got(contentUrl, { json: true });
68-
const dependencies = pkgContent.body.dependencies;
70+
71+
const dependencies: PackageJson.Dependency = {};
72+
const exactDependencies: PackageJson.Dependency = {};
73+
Object.entries<string>(pkgContent.body.dependencies).forEach(
74+
([name, version]) => {
75+
if (Boolean(semver.parse(version))) {
76+
exactDependencies[name] = version;
77+
} else {
78+
dependencies[name] = version;
79+
}
80+
}
81+
);
82+
if (exactDependencies && Object.keys(exactDependencies).length > 0) {
83+
await install(exactDependencies, {
84+
cwd: targetDir,
85+
exact: true,
86+
});
87+
}
6988
if (dependencies && Object.keys(dependencies).length > 0) {
7089
return install(dependencies, {
7190
cwd: targetDir,
@@ -135,7 +154,7 @@ export async function writeFiles(
135154
}
136155

137156
const tasks = files
138-
.map(file => {
157+
.map((file) => {
139158
if (file.type === 'functions') {
140159
return {
141160
title: `Creating function: ${path.join(file.directory, file.name)}`,

0 commit comments

Comments
 (0)