Skip to content

Commit f8834f3

Browse files
committed
feat(packager): implement a way to determine a package manager
Package manager is now determined by project preference or running environment, npm has priority issue #2
1 parent f46c7b6 commit f8834f3

File tree

8 files changed

+146
-28
lines changed

8 files changed

+146
-28
lines changed

src/index.ts

+25-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as es from 'esbuild';
44
import * as path from 'path';
55
import { mergeRight, union, without } from 'ramda';
66

7-
import { packExternalModules } from './packExternalModules';
7+
import { packExternalModules } from './pack-externals';
88
import { extractFileName, findProjectRoot, nodeMajorVersion } from './utils';
99

1010
/**
@@ -46,6 +46,13 @@ export interface NodejsFunctionProps extends lambda.FunctionOptions {
4646
*/
4747
readonly exclude?: string[];
4848

49+
/**
50+
* Whether to use package manager to pack external modules or explicit name of a well known packager.
51+
*
52+
* @default = true // Determined based on what preference is set, and whether it's currently running in a yarn/npm script
53+
*/
54+
readonly packager?: Packager | boolean;
55+
4956
/**
5057
* The esbuild bundler specific options.
5158
*
@@ -54,6 +61,14 @@ export interface NodejsFunctionProps extends lambda.FunctionOptions {
5461
readonly esbuildOptions?: es.BuildOptions;
5562
}
5663

64+
/**
65+
* Package manager to pack external modules.
66+
*/
67+
export enum Packager {
68+
NPM = 'npm',
69+
YARN = 'yarn',
70+
}
71+
5772
const BUILD_FOLDER = '.build';
5873
const DEFAULT_BUILD_OPTIONS: es.BuildOptions = {
5974
bundle: true,
@@ -77,6 +92,7 @@ export class NodejsFunction extends lambda.Function {
7792
const withDefaultOptions = mergeRight(DEFAULT_BUILD_OPTIONS);
7893
const buildOptions = withDefaultOptions<es.BuildOptions>(props.esbuildOptions ?? {});
7994
const exclude = union(props.exclude || [], ['aws-sdk']);
95+
const packager = props.packager ?? true;
8096
const handler = props.handler ?? 'index.handler';
8197
const defaultRunTime = nodeMajorVersion() >= 12
8298
? lambda.Runtime.NODEJS_12_X
@@ -92,7 +108,14 @@ export class NodejsFunction extends lambda.Function {
92108
platform: 'node',
93109
});
94110

95-
packExternalModules(without(exclude, buildOptions.external || []), projectRoot, path.join(projectRoot, BUILD_FOLDER));
111+
if (packager) {
112+
packExternalModules(
113+
without(exclude, buildOptions.external || []),
114+
projectRoot,
115+
path.join(projectRoot, BUILD_FOLDER),
116+
packager !== true ? packager : undefined,
117+
);
118+
}
96119

97120
super(scope, id, {
98121
...props,

src/packExternalModules.ts renamed to src/pack-externals.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ function getProdModules(externalModules: { external: string }[], packageJsonPath
116116
* This will utilize the npm cache at its best and give us the needed results
117117
* and performance.
118118
*/
119-
export function packExternalModules(externals: string[], cwd: string, compositeModulePath: string) {
119+
export function packExternalModules(externals: string[], cwd: string, compositeModulePath: string, pkger?: 'npm' | 'yarn') {
120120
if (!externals || !externals.length) {
121121
return;
122122
}
@@ -125,7 +125,7 @@ export function packExternalModules(externals: string[], cwd: string, compositeM
125125
const packageJsonPath = path.join(cwd, 'package.json');
126126

127127
// Determine and create packager
128-
const packager = Packagers.get(Packagers.Installer.NPM);
128+
const packager = Packagers.get(cwd, pkger);
129129

130130
// Fetch needed original package.json sections
131131
const packageJson = fs.readJsonSync(packageJsonPath);

src/packagers/index.ts

+38-8
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,56 @@
2020
import { Packager } from './packager';
2121
import { NPM } from './npm';
2222
import { Yarn } from './yarn';
23+
import { getCurrentPackager, getPackagerFromLockfile } from '../utils';
2324

2425
const registeredPackagers = {
2526
npm: new NPM(),
2627
yarn: new Yarn()
2728
};
2829

29-
export enum Installer {
30-
NPM = 'npm',
31-
YARN = 'yarn',
32-
}
33-
3430
/**
3531
* Factory method.
3632
* @param {string} packagerId - Well known packager id.
3733
*/
38-
export function get(packagerId: Installer): Packager {
39-
if (!(packagerId in registeredPackagers)) {
34+
export function get(cwd: string, packagerId?: keyof typeof registeredPackagers): Packager {
35+
const pkger = findPackager(cwd, packagerId);
36+
37+
if (!(pkger in registeredPackagers)) {
4038
const message = `Could not find packager '${packagerId}'`;
4139
console.log(`ERROR: ${message}`);
4240
throw new Error(message);
4341
}
44-
return registeredPackagers[packagerId];
42+
43+
return registeredPackagers[pkger];
44+
}
45+
46+
/**
47+
* Determine what package manager to use based on what preference is set,
48+
* and whether it's currently running in a yarn/npm script
49+
*
50+
* @export
51+
* @param {InstallConfig} config
52+
* @returns {SupportedPackageManagers}
53+
*/
54+
function findPackager(cwd: string, prefer?: keyof typeof registeredPackagers): keyof typeof registeredPackagers {
55+
let pkgManager: keyof typeof registeredPackagers | null = prefer || getCurrentPackager();
56+
57+
if (!pkgManager) {
58+
pkgManager = getPackagerFromLockfile(cwd);
59+
}
60+
61+
if (!pkgManager) {
62+
for (const pkg in registeredPackagers) {
63+
if (registeredPackagers[pkg].isManagerInstalled(cwd)) {
64+
pkgManager = pkg as keyof typeof registeredPackagers;
65+
break;
66+
}
67+
}
68+
}
69+
70+
if (!pkgManager) {
71+
throw new Error('No supported package manager found');
72+
}
73+
74+
return pkgManager;
4575
}

src/packagers/npm.ts

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

33
import { JSONObject } from '../types';
44
import { SpawnError, spawnProcess } from '../utils';
@@ -20,6 +20,18 @@ export class NPM implements Packager {
2020
return true;
2121
}
2222

23+
isManagerInstalled(cwd: string) {
24+
const command = /^win/.test(process.platform) ? 'npm.cmd' : 'npm';
25+
const args = ['--version'];
26+
27+
try {
28+
spawnProcess(command, args, { cwd });
29+
return true;
30+
} catch (_e) {
31+
return false;
32+
}
33+
}
34+
2335
getProdDependencies(cwd: string, depth: number) {
2436
// Get first level dependency graph
2537
const command = /^win/.test(process.platform) ? 'npm.cmd' : 'npm';
@@ -44,7 +56,7 @@ export class NPM implements Packager {
4456
} catch (err) {
4557
if (err instanceof SpawnError) {
4658
// Only exit with an error if we have critical npm errors for 2nd level inside
47-
const errors = split('\n', err.stderr);
59+
const errors = err.stderr?.split('\n') ?? [];
4860
const failed = errors.reduce((f, error) => {
4961
if (f) {
5062
return true;
@@ -64,15 +76,6 @@ export class NPM implements Packager {
6476
}
6577
}
6678

67-
_rebaseFileReferences(pathToPackageRoot: string, moduleVersion: string) {
68-
if (/^file:[^/]{2}/.test(moduleVersion)) {
69-
const filePath = replace(/^file:/, '', moduleVersion);
70-
return replace(/\\/g, '/', `file:${pathToPackageRoot}/${filePath}`);
71-
}
72-
73-
return moduleVersion;
74-
}
75-
7679
/**
7780
* We should not be modifying 'package-lock.json'
7881
* because this file should be treated as internal to npm.
@@ -82,7 +85,7 @@ export class NPM implements Packager {
8285
*/
8386
rebaseLockfile(pathToPackageRoot: string, lockfile: JSONObject) {
8487
if (lockfile.version) {
85-
lockfile.version = this._rebaseFileReferences(pathToPackageRoot, lockfile.version);
88+
lockfile.version = this.rebaseFileReferences(pathToPackageRoot, lockfile.version);
8689
}
8790

8891
if (lockfile.dependencies) {
@@ -117,4 +120,13 @@ export class NPM implements Packager {
117120
spawnProcess(command, args, { cwd });
118121
});
119122
}
123+
124+
private rebaseFileReferences(pathToPackageRoot: string, moduleVersion: string) {
125+
if (/^file:[^/]{2}/.test(moduleVersion)) {
126+
const filePath = replace(/^file:/, '', moduleVersion);
127+
return replace(/\\/g, '/', `file:${pathToPackageRoot}/${filePath}`);
128+
}
129+
130+
return moduleVersion;
131+
}
120132
}

src/packagers/packager.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export interface Packager {
44
lockfileName: string;
55
copyPackageSectionNames: Array<string>;
66
mustCopyModules: boolean;
7+
isManagerInstalled(cwd: string): boolean;
78
getProdDependencies(cwd: string, depth: number): JSONObject;
89
rebaseLockfile(pathToPackageRoot: string, lockfile: JSONObject): void;
910
install(cwd: string): void;

src/packagers/yarn.ts

+13-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,18 @@ export class Yarn implements Packager {
2424
return false;
2525
}
2626

27+
isManagerInstalled(cwd: string) {
28+
const command = /^win/.test(process.platform) ? 'yarn.cmd' : 'yarn';
29+
const args = ['--version'];
30+
31+
try {
32+
spawnProcess(command, args, { cwd });
33+
return true;
34+
} catch (_e) {
35+
return false;
36+
}
37+
}
38+
2739
getProdDependencies(cwd, depth) {
2840
const command = /^win/.test(process.platform) ? 'yarn.cmd' : 'yarn';
2941
const args = ['list', `--depth=${depth || 1}`, '--json', '--production'];
@@ -37,7 +49,7 @@ export class Yarn implements Packager {
3749
} catch (err) {
3850
if (err instanceof SpawnError) {
3951
// Only exit with an error if we have critical npm errors for 2nd level inside
40-
const errors = err.stderr.split('\n');
52+
const errors = err.stderr?.split('\n') ?? [];
4153
const failed = errors.reduce((f, error) => {
4254
if (f) {
4355
return true;

src/utils.ts

+42-2
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ export class SpawnError extends Error {
2323
*/
2424
export function spawnProcess(command: string, args: string[], options: childProcess.SpawnOptionsWithoutStdio) {
2525
const child = childProcess.spawnSync(command, args, options);
26-
const stdout = child.stdout.toString('utf8');
27-
const stderr = child.stderr.toString('utf8');
26+
const stdout = child.stdout?.toString('utf8');
27+
const stderr = child.stderr?.toString('utf8');
2828

2929
if (child.status !== 0) {
3030
throw new SpawnError(`${command} ${join(' ', args)} failed with code ${child.status}`, stdout, stderr);
@@ -92,3 +92,43 @@ export function findProjectRoot(rootDir?: string): string | undefined {
9292
export function nodeMajorVersion(): number {
9393
return parseInt(process.versions.node.split('.')[0], 10);
9494
}
95+
96+
/**
97+
* Returns the package manager currently active if the program is executed
98+
* through an npm or yarn script like:
99+
* ```bash
100+
* yarn run example
101+
* npm run example
102+
* ```
103+
*/
104+
export function getCurrentPackager() {
105+
const userAgent = process.env.npm_config_user_agent;
106+
if (!userAgent) {
107+
return null;
108+
}
109+
110+
if (userAgent.startsWith('npm')) {
111+
return 'npm';
112+
}
113+
114+
if (userAgent.startsWith('yarn')) {
115+
return 'yarn';
116+
}
117+
118+
return null;
119+
}
120+
121+
/**
122+
* Checks for the presence of package-lock.json or yarn.lock to determine which package manager is being used
123+
*/
124+
export function getPackagerFromLockfile(cwd: string) {
125+
if (fs.existsSync(path.join(cwd, 'package-lock.json'))) {
126+
return 'npm';
127+
}
128+
129+
if (fs.existsSync(path.join(cwd, 'yarn.lock'))) {
130+
return 'yarn';
131+
}
132+
133+
return null;
134+
}

tests/index.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
jest.mock('esbuild');
2-
jest.mock('../src/packExternalModules');
2+
jest.mock('../src/pack-externals');
33

44
import '@aws-cdk/assert/jest';
55

0 commit comments

Comments
 (0)