Skip to content

Commit b33be2f

Browse files
committed
fix(packager): introduce unit tests for packagers, fix bugs found on testing
1 parent 23854b7 commit b33be2f

File tree

8 files changed

+600
-61
lines changed

8 files changed

+600
-61
lines changed

examples/complete/src/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import validateIsin from 'isin-validator';
22

3-
export function handler(event: any) {
3+
export function handler(event: string) {
44
const isInvalid = validateIsin(event);
55

66
return {

src/packagers/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export function get(cwd: string, packagerId?: keyof typeof registeredPackagers):
3535
const pkger = findPackager(cwd, packagerId);
3636

3737
if (!(pkger in registeredPackagers)) {
38-
const message = `Could not find packager '${packagerId}'`;
38+
const message = `Could not find packager '${pkger}'`;
3939
console.log(`ERROR: ${message}`);
4040
throw new Error(message);
4141
}

src/packagers/npm.ts

+28-30
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { any, isEmpty, replace } from 'ramda';
1+
import { any, isEmpty } from 'ramda';
22

33
import { JSONObject } from '../types';
44
import { SpawnError, spawnProcess } from '../utils';
@@ -32,7 +32,7 @@ export class NPM implements Packager {
3232
}
3333
}
3434

35-
getProdDependencies(cwd: string, depth: number) {
35+
getProdDependencies(cwd: string, depth?: number) {
3636
// Get first level dependency graph
3737
const command = /^win/.test(process.platform) ? 'npm.cmd' : 'npm';
3838
const args = [
@@ -48,32 +48,34 @@ export class NPM implements Packager {
4848
{ npmError: 'peer dep missing', log: true }
4949
];
5050

51+
let processOutput;
5152
try {
52-
const processOutput = spawnProcess(command, args, { cwd });
53-
const depJson = processOutput.stdout;
54-
55-
return JSON.parse(depJson);
53+
processOutput = spawnProcess(command, args, { cwd });
5654
} catch (err) {
57-
if (err instanceof SpawnError) {
58-
// Only exit with an error if we have critical npm errors for 2nd level inside
59-
const errors = err.stderr?.split('\n') ?? [];
60-
const failed = errors.reduce((f, error) => {
61-
if (f) {
62-
return true;
63-
}
64-
return (
65-
!isEmpty(error) &&
66-
!any(ignoredError => error.startsWith(`npm ERR! ${ignoredError.npmError}`), ignoredNpmErrors)
67-
);
68-
}, false);
69-
70-
if (!failed && !isEmpty(err.stdout)) {
71-
return { stdout: err.stdout };
55+
if (!(err instanceof SpawnError)) {
56+
throw err;
57+
}
58+
59+
// Only exit with an error if we have critical npm errors for 2nd level inside
60+
const errors = err.stderr?.split('\n') ?? [];
61+
const failed = errors.reduce((f, error) => {
62+
if (f) {
63+
return true;
7264
}
65+
return (
66+
!isEmpty(error) &&
67+
!any(ignoredError => error.startsWith(`npm ERR! ${ignoredError.npmError}`), ignoredNpmErrors)
68+
);
69+
}, false);
70+
71+
if (failed || isEmpty(err.stdout)) {
72+
throw err;
7373
}
7474

75-
throw err;
75+
processOutput = { stdout: err.stdout };
7676
}
77+
78+
return JSON.parse(processOutput.stdout);
7779
}
7880

7981
/**
@@ -90,7 +92,7 @@ export class NPM implements Packager {
9092

9193
if (lockfile.dependencies) {
9294
for (const lockedDependency in lockfile.dependencies) {
93-
this.rebaseLockfile(pathToPackageRoot, lockedDependency);
95+
this.rebaseLockfile(pathToPackageRoot, lockfile.dependencies[lockedDependency]);
9496
}
9597
}
9698

@@ -114,17 +116,13 @@ export class NPM implements Packager {
114116
runScripts(cwd, scriptNames) {
115117
const command = /^win/.test(process.platform) ? 'npm.cmd' : 'npm';
116118

117-
scriptNames.forEach(scriptName => {
118-
const args = ['run', scriptName];
119-
120-
spawnProcess(command, args, { cwd });
121-
});
119+
scriptNames.forEach(scriptName => spawnProcess(command, ['run', scriptName], { cwd }));
122120
}
123121

124122
private rebaseFileReferences(pathToPackageRoot: string, moduleVersion: string) {
125123
if (/^file:[^/]{2}/.test(moduleVersion)) {
126-
const filePath = replace(/^file:/, '', moduleVersion);
127-
return replace(/\\/g, '/', `file:${pathToPackageRoot}/${filePath}`);
124+
const filePath = moduleVersion.replace(/^file:/, '');
125+
return `file:${pathToPackageRoot}/${filePath}`.replace(/\\/g, '/');
128126
}
129127

130128
return moduleVersion;

src/packagers/yarn.ts

+38-29
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { any, head, isEmpty, join, pathOr, reduce, replace, split, startsWith, tail } from 'ramda';
1+
import { any, head, isEmpty, join, pathOr, split, tail } from 'ramda';
22

33
import { JSONObject } from '../types';
4-
import { SpawnError, spawnProcess } from '../utils';
4+
import { safeJsonParse, SpawnError, spawnProcess, splitLines } from '../utils';
55
import { Packager } from './packager';
66

77
/**
@@ -36,7 +36,7 @@ export class Yarn implements Packager {
3636
}
3737
}
3838

39-
getProdDependencies(cwd, depth) {
39+
getProdDependencies(cwd: string, depth?: number) {
4040
const command = /^win/.test(process.platform) ? 'yarn.cmd' : 'yarn';
4141
const args = ['list', `--depth=${depth || 1}`, '--json', '--production'];
4242

@@ -47,33 +47,36 @@ export class Yarn implements Packager {
4747
try {
4848
processOutput = spawnProcess(command, args, { cwd });
4949
} catch (err) {
50-
if (err instanceof SpawnError) {
51-
// Only exit with an error if we have critical npm errors for 2nd level inside
52-
const errors = err.stderr?.split('\n') ?? [];
53-
const failed = errors.reduce((f, error) => {
54-
if (f) {
55-
return true;
56-
}
57-
return (
58-
!isEmpty(error) &&
59-
!any(ignoredError => error.startsWith(`npm ERR! ${ignoredError.npmError}`), ignoredYarnErrors)
60-
);
61-
}, false);
62-
63-
if (!failed && !isEmpty(err.stdout)) {
64-
return { stdout: err.stdout };
50+
if (!(err instanceof SpawnError)) {
51+
throw err;
52+
}
53+
54+
// Only exit with an error if we have critical npm errors for 2nd level inside
55+
const errors = err.stderr?.split('\n') ?? [];
56+
const failed = errors.reduce((f, error) => {
57+
if (f) {
58+
return true;
6559
}
60+
return (
61+
!isEmpty(error) &&
62+
!any(ignoredError => error.startsWith(`npm ERR! ${ignoredError.npmError}`), ignoredYarnErrors)
63+
);
64+
}, false);
65+
66+
if (failed || isEmpty(err.stdout)) {
67+
throw err;
6668
}
6769

68-
throw err;
70+
processOutput = { stdout: err.stdout };
6971
}
7072

71-
const depJson = processOutput.stdout;
72-
const parsedTree = JSON.parse(depJson);
73-
const convertTrees = reduce((__, tree: JSONObject) => {
73+
const lines = splitLines(processOutput.stdout);
74+
const parsedLines = lines.map(safeJsonParse);
75+
const parsedTree = parsedLines.find(line => line && line.type === 'tree');
76+
const convertTrees = ts => ts.reduce((__, tree: JSONObject) => {
7477
const splitModule = split('@', tree.name);
7578
// If we have a scoped module we have to re-add the @
76-
if (startsWith('@', tree.name)) {
79+
if (tree.name.startsWith('@')) {
7780
splitModule.splice(0, 1);
7881
splitModule[0] = '@' + splitModule[0];
7982
}
@@ -101,28 +104,34 @@ export class Yarn implements Packager {
101104
while ((match = fileVersionMatcher.exec(lockfile)) !== null) {
102105
replacements.push({
103106
oldRef: match[1],
104-
newRef: replace(/\\/g, '/', `${pathToPackageRoot}/${match[1]}`)
107+
newRef: `${pathToPackageRoot}/${match[1]}`.replace(/\\/g, '/')
105108
});
106109
}
107110

108111
// Replace all lines in lockfile
109-
return reduce((__, replacement) => replace(__, replacement.oldRef, replacement.newRef), lockfile, replacements);
112+
return replacements.reduce((__, replacement) => __.replace(replacement.oldRef, replacement.newRef), lockfile);
110113
}
111114

112-
install(cwd) {
115+
install(cwd: string, packagerOptions?) {
113116
const command = /^win/.test(process.platform) ? 'yarn.cmd' : 'yarn';
114-
const args = ['install', '--frozen-lockfile', '--non-interactive'];
117+
const args = [ 'install', '--frozen-lockfile', '--non-interactive' ];
118+
119+
// Convert supported packagerOptions
120+
if (packagerOptions.ignoreScripts) {
121+
args.push('--ignore-scripts');
122+
}
115123

116124
spawnProcess(command, args, { cwd });
117125
}
118126

119127
// "Yarn install" prunes automatically
120-
prune(cwd) {
121-
return this.install(cwd);
128+
prune(cwd: string, packagerOptions?) {
129+
return this.install(cwd, packagerOptions);
122130
}
123131

124132
runScripts(cwd, scriptNames: string[]) {
125133
const command = /^win/.test(process.platform) ? 'yarn.cmd' : 'yarn';
134+
126135
scriptNames.forEach(scriptName => spawnProcess(command, ['run', scriptName], { cwd }));
127136
}
128137
}

src/utils.ts

+12
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,18 @@ export function spawnProcess(command: string, args: string[], options: childProc
3333
return { stdout, stderr };
3434
}
3535

36+
export function safeJsonParse(str: string) {
37+
try {
38+
return JSON.parse(str);
39+
} catch (e) {
40+
return null;
41+
}
42+
}
43+
44+
export function splitLines(str: string) {
45+
return str.split(/\r?\n/);
46+
}
47+
3648
/**
3749
* Extracts the file name from handler string.
3850
*/

tests/packagers/index.test.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Unit tests for packagers/index
3+
*/
4+
5+
import { get } from '../../src/packagers';
6+
import { NPM } from '../../src/packagers/npm';
7+
import * as Utils from '../../src/utils';
8+
9+
const getCurrentPackager = jest.spyOn(Utils, 'getCurrentPackager');
10+
11+
describe('packagers factory', () => {
12+
it('should throw on unknown packagers', () => {
13+
getCurrentPackager.mockReset().mockReturnValue('unknown' as never);
14+
expect(() => get('.')).toThrowError(/Could not find packager 'unknown'/);
15+
});
16+
17+
it('should return npm packager', () => {
18+
const npm = get('.', 'npm');
19+
expect(npm).toBeInstanceOf(NPM);
20+
});
21+
});

0 commit comments

Comments
 (0)