Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/aws-cdk-lib/aws-lambda-nodejs/lib/function.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import * as fs from 'fs';
import * as path from 'path';
import type { Construct } from 'constructs';
import { Bundling } from './bundling';
import { LockFile } from './package-manager';
import { Bundling } from './private/bundling';
import { LockFile } from './private/package-manager';
import { callsites, findUpMultiple, isSdkV2Runtime } from './private/util';
import type { BundlingOptions } from './types';
import { callsites, findUpMultiple, isSdkV2Runtime } from './util';
import { Architecture } from '../../aws-lambda';
import * as lambda from '../../aws-lambda';
import { Annotations, FeatureFlags, ValidationError } from '../../core';
Expand Down
90 changes: 0 additions & 90 deletions packages/aws-cdk-lib/aws-lambda-nodejs/lib/package-manager.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import * as path from 'path';
import type { IConstruct } from 'constructs';
import { PackageInstallation } from './package-installation';
import { LockFile, PackageManager } from './package-manager';
import type { BundlingOptions } from './types';
import { OutputFormat, SourceMapMode } from './types';
import type { BundlingOptions } from '../types';
import { OutputFormat, SourceMapMode } from '../types';
import { exec, extractDependencies, findUp, getTsconfigCompilerOptionsArray, isSdkV2Runtime } from './util';
import type { Architecture, AssetCode } from '../../aws-lambda';
import { Code, Runtime } from '../../aws-lambda';
import * as cdk from '../../core';
import { ValidationError } from '../../core';
import { LAMBDA_NODEJS_SDK_V3_EXCLUDE_SMITHY_PACKAGES } from '../../cx-api';
import type { Architecture, AssetCode } from '../../../aws-lambda';
import { Code, Runtime } from '../../../aws-lambda';
import * as cdk from '../../../core';
import { ValidationError } from '../../../core';
import { LAMBDA_NODEJS_SDK_V3_EXCLUDE_SMITHY_PACKAGES } from '../../../cx-api';

const ESBUILD_MAJOR_VERSION = '0';
const ESBUILD_DEFAULT_VERSION = '0.21';
Expand Down Expand Up @@ -178,7 +178,7 @@ export class Bundling implements cdk.BundlingOptions {

// Docker bundling
const shouldBuildImage = props.forceDockerBundling || !Bundling.esbuildInstallation;
this.image = shouldBuildImage ? props.dockerImage ?? cdk.DockerImage.fromBuild(path.join(__dirname, '..', 'lib'),
this.image = shouldBuildImage ? props.dockerImage ?? cdk.DockerImage.fromBuild(path.join(__dirname, '..', '..', 'lib'),
{
buildArgs: {
...props.buildArgs ?? {},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export abstract class PackageInstallation {
}
}

/**
* Whether the binary is found in the current project's `package.json`.
*
* - If `true`, it is in the current project's `package.json`.
* - If `false`, it is otherwise found on the $PATH and can be executed directly.
*/
public abstract readonly isLocal: boolean;
public abstract readonly version: string;
}
166 changes: 166 additions & 0 deletions packages/aws-cdk-lib/aws-lambda-nodejs/lib/private/package-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { AssumptionError } from '../../../core';
import { LogLevel } from '../types';

interface PackageManagerProps {
readonly lockFile: string;
readonly installCommand: string[];
readonly runCommand?: string[];
readonly directFromSubdirectory?: string;
readonly argsSeparator?: string;
}

export enum LockFile {
NPM = 'package-lock.json',
YARN = 'yarn.lock',
BUN = 'bun.lockb',
BUN_LOCK = 'bun.lock',
PNPM = 'pnpm-lock.yaml',
}

/**
* A node package manager
*/
export class PackageManager {
/**
* Use a lock file path to determine the package manager to use. Optionally, specify a log level to
* control its verbosity.
* @param lockFilePath Path of the lock file
* @param logLevel optional log level @default LogLevel.INFO
* @returns the right PackageManager for that lock file
*/
public static fromLockFile(lockFilePath: string, logLevel?: LogLevel): PackageManager {
const lockFile = path.basename(lockFilePath);

switch (lockFile) {
case LockFile.YARN: {
// Either Yarn Classic (1.x) or Yarn Berry (2.x+)
//
// It is non-trivial to distinguish between Yarn Classic and Yarn Berry;
// the easiest way is to check the metadata in the lockfile.
//
// - Yarn Classic: `yarn run esbuild` adds 150ms. We can try the direct execution trick though
// because binaries are installed to `node_modules/.bin`.
//
// - Yarn Berry: packages are installed off to the side; `yarn bin` gives a path to the wrapper script
// that won't work without the package resolution handlers being installed so we cannot do direct
// execution. `yarn run esbuild` adds ~500ms which is not skippable.
//
// In both cases: `esbuild` ships as a dispatch script to the actual runtime binary, and has a `postinstall`
// script to swap out the dispatch script for the actual binary at install time. Neither version of yarn seems to
// properly execute that postinstall script, which means the dispatch script adds 100ms on every invocation
// regardless.
const installCommand = logLevel && logLevel !== LogLevel.INFO ? ['yarn', 'install', '--no-immutable', '--silent'] : ['yarn', 'install', '--no-immutable'];

let isYarnV1 = false;
try {
const lockFileContents = fs.readFileSync(lockFilePath, 'utf-8');
isYarnV1 = lockFileContents.includes('# yarn lockfile v1');
} catch (e: any) {
if (e.code !== 'ENOENT') {
throw e;
}
}

if (isYarnV1 && !isWindows()) {
// Yarn Classic
return new PackageManager({
lockFile: LockFile.YARN,
installCommand,
// NPM compatible, save a single yarn dispatch (~150ms)
directFromSubdirectory: 'node_modules/.bin',
});
}

// Yarn Berry
return new PackageManager({
lockFile: LockFile.YARN,
installCommand,
runCommand: ['yarn', 'run'],
});
}

case LockFile.PNPM:
return new PackageManager({
lockFile: LockFile.PNPM,
installCommand: logLevel && logLevel !== LogLevel.INFO ? ['pnpm', 'install', '--reporter', 'silent', '--config.node-linker=hoisted', '--config.package-import-method=clone-or-copy', '--no-prefer-frozen-lockfile'] : ['pnpm', 'install', '--config.node-linker=hoisted', '--config.package-import-method=clone-or-copy', '--no-prefer-frozen-lockfile'],
// --config.node-linker=hoisted to create flat node_modules without symlinks
// --config.package-import-method=clone-or-copy to avoid hardlinking packages from the store
// --no-prefer-frozen-lockfile (works the same as yarn's --no-immutable) Disable --frozen-lockfile that is enabled by default in CI environments (https://github.com/pnpm/pnpm/issues/1994).
runCommand: ['pnpm', 'exec'],
argsSeparator: '--',
});
case LockFile.BUN:
case LockFile.BUN_LOCK:
return new PackageManager({
lockFile,
// Bun's default is to not force `--frozen-lockfile`, so it's not specified here. If they ever add a
// flag to explicitly disable it, we should add it here. https://github.com/oven-sh/bun/issues/16387
installCommand: logLevel && logLevel !== LogLevel.INFO ? ['bun', 'install', '--backend', 'copyfile', '--silent'] : ['bun', 'install', '--backend', 'copyfile'],
runCommand: ['bun', 'run'],
});
default: {
const installCommand = logLevel ? ['npm', 'ci', '--loglevel', logLevel] : ['npm', 'ci'];

if (!isWindows()) {
return new PackageManager({
lockFile: LockFile.NPM,
installCommand,
// We could use `npx` but it adds ~400-500ms on every invocation, so do direct execution instead.
directFromSubdirectory: 'node_modules/.bin',
});
}
// On WIndows, fall back to using `npx`, otherwise we need to go figure out if the target binary is
// a `.cmd` or `.bat` file and run it through the shell if it is. Much easier to leave
// all that to `npx`.
return new PackageManager({
lockFile: LockFile.NPM,
installCommand,
runCommand: ['npx', '--no-install'],
});
}
}
}

public readonly lockFile: string;
public readonly installCommand: string[];
public readonly runCommand?: string[];
public readonly directFromSubdirectory?: string;
public readonly argsSeparator?: string;

constructor(props: PackageManagerProps) {
this.lockFile = props.lockFile;
this.installCommand = props.installCommand;
this.runCommand = props.runCommand;
this.directFromSubdirectory = props.directFromSubdirectory;
this.argsSeparator = props.argsSeparator;

if (!!props.runCommand == !!props.directFromSubdirectory) {
throw new AssumptionError('MutexArguments', 'Exactly one of runCommand and runFromSubdirectory must be supplied');
}
}

public runBinCommand(bin: string): string[] {
if (this.runCommand) {
const [runCommand, ...runArgs] = this.runCommand;
return [
isWindows() ? `${runCommand}.cmd` : runCommand,
...runArgs,
...(this.argsSeparator ? [this.argsSeparator] : []),
bin,
];
} else {
if (!this.directFromSubdirectory) {
throw new AssumptionError('AlreadyValidated', 'Should have been validated in the constructor');
}

return [`${this.directFromSubdirectory}/${bin}`];
}
}
}

function isWindows() {
return os.platform() === 'win32';
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { SpawnSyncOptions } from 'child_process';
import { spawnSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { Runtime } from '../../aws-lambda';
import { UnscopedValidationError } from '../../core';
import { Runtime } from '../../../aws-lambda';
import { UnscopedValidationError } from '../../../core';

export interface CallSite {
getThis(): any;
Expand Down
14 changes: 11 additions & 3 deletions packages/aws-cdk-lib/aws-lambda-nodejs/test/bundling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ import { version as delayVersion } from 'delay/package.json';
import { Annotations } from '../../assertions';
import { Architecture, Code, Runtime, RuntimeFamily } from '../../aws-lambda';
import { App, AssetHashType, BundlingFileAccess, DockerImage, Stack } from '../../core';
import { Bundling } from '../lib/bundling';
import { PackageInstallation } from '../lib/package-installation';
import { Bundling } from '../lib/private/bundling';
import { PackageInstallation } from '../lib/private/package-installation';
import * as util from '../lib/private/util';
import { Charset, LogLevel, OutputFormat, SourceMapMode } from '../lib/types';
import * as util from '../lib/util';

const STANDARD_RUNTIME = Runtime.NODEJS_20_X;
const STANDARD_TARGET = 'node20';
Expand Down Expand Up @@ -39,6 +39,14 @@ beforeEach(() => {
run: () => {},
toJSON: () => 'built-image',
});

const origReadFileSync = fs.readFileSync;
jest.spyOn(fs, 'readFileSync').mockImplementation((p, options) => {
if (p === '/project/yarn.lock') {
return 'adsf';
}
return origReadFileSync(p, options);
});
});

let projectRoot = '/project';
Expand Down
8 changes: 4 additions & 4 deletions packages/aws-cdk-lib/aws-lambda-nodejs/test/function.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { Code, Runtime } from '../../aws-lambda';
import { App, Stack } from '../../core';
import { LAMBDA_NODEJS_USE_LATEST_RUNTIME } from '../../cx-api';
import { NodejsFunction } from '../lib';
import { Bundling } from '../lib/bundling';
import { Bundling } from '../lib/private/bundling';

jest.mock('../lib/bundling', () => {
jest.mock('../lib/private/bundling', () => {
return {
Bundling: {
bundle: jest.fn().mockReturnValue({
Expand All @@ -29,8 +29,8 @@ jest.mock('../lib/bundling', () => {
});

const mockCallsites = jest.fn();
jest.mock('../lib/util', () => ({
...jest.requireActual('../lib/util'),
jest.mock('../lib/private/util', () => ({
...jest.requireActual('../lib/private/util'),
callsites: () => mockCallsites(),
}));

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as child_process from 'child_process';
import { PackageInstallation } from '../lib/package-installation';
import * as util from '../lib/util';
import { PackageInstallation } from '../lib/private/package-installation';
import * as util from '../lib/private/util';

// eslint-disable-next-line @typescript-eslint/no-require-imports
const version = require('esbuild/package.json').version;
Expand Down
Loading
Loading