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
16 changes: 16 additions & 0 deletions packages/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import type { IConstruct } from 'constructs';
Expand Down Expand Up @@ -286,9 +287,17 @@ export class Bundling implements cdk.BundlingOptions {
const isPnpm = this.packageManager.lockFile === LockFile.PNPM;
const isBun = this.packageManager.lockFile === LockFile.BUN_LOCK || this.packageManager.lockFile === LockFile.BUN;

// Copy .npmrc for pnpm to inherit private registry auth. Uses base64 to handle monorepos
// where .npmrc is outside projectRoot (inaccessible in Docker) and to avoid shell escaping issues.
const npmrcFilePath = isPnpm ? findUp('.npmrc', this.projectRoot) : undefined;
const npmrcBase64 = npmrcFilePath && fs.existsSync(npmrcFilePath)
? Buffer.from(fs.readFileSync(npmrcFilePath, 'utf-8')).toString('base64')
: undefined;

// Create dummy package.json, copy lock file if any and then install
depsCommand = chain([
isPnpm ? osCommand.write(pathJoin(options.outputDir, 'pnpm-workspace.yaml'), ''): '', // Ensure node_modules directory is installed locally by creating local 'pnpm-workspace.yaml' file
npmrcBase64 ? osCommand.writeBase64(pathJoin(options.outputDir, '.npmrc'), npmrcBase64) : '', // Write .npmrc for pnpm to inherit registry authentication
osCommand.writeJson(pathJoin(options.outputDir, 'package.json'), { dependencies }),
osCommand.copy(lockFilePath, pathJoin(options.outputDir, this.packageManager.lockFile)),
osCommand.changeDirectory(options.outputDir),
Expand Down Expand Up @@ -386,6 +395,13 @@ class OsCommand {
return this.write(filePath, stringifiedData);
}

public writeBase64(filePath: string, base64Data: string): string {
if (this.osPlatform === 'win32') {
return `powershell -Command "[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${base64Data}')) | Set-Content -Path '${filePath}' -NoNewline"`;
}
return `echo '${base64Data}' | base64 -d > "${filePath}"`;
}

public copy(src: string, dest: string): string {
if (this.osPlatform === 'win32') {
return `copy "${src}" "${dest}"`;
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk-lib/aws-lambda-nodejs/test/.testnpmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@myorg:registry=https://registry.example.com/
107 changes: 107 additions & 0 deletions packages/aws-cdk-lib/aws-lambda-nodejs/test/bundling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,113 @@ test('Detects pnpm-lock.yaml', () => {
});
});

test('pnpm with nodeModules writes .npmrc using base64 for private registry authentication', () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit. Can you please add coverage for the win32 platform?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Added and re-based.

const pnpmLock = '/project/pnpm-lock.yaml';
const npmrcFixture = path.join(__dirname, '.testnpmrc');
const npmrcBase64 = Buffer.from(fs.readFileSync(npmrcFixture, 'utf-8')).toString('base64');

const originalFindUp = util.findUp;
jest.spyOn(util, 'findUp').mockImplementation((name, dir) =>
name === '.npmrc' ? npmrcFixture : originalFindUp(name, dir),
);

Bundling.bundle(stack, {
entry: __filename,
projectRoot,
depsLockFilePath: pnpmLock,
runtime: STANDARD_RUNTIME,
architecture: Architecture.X86_64,
nodeModules: ['delay'],
forceDockerBundling: true,
});

expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(pnpmLock), {
assetHashType: AssetHashType.OUTPUT,
bundling: expect.objectContaining({
command: expect.arrayContaining([
expect.stringContaining(`echo '${npmrcBase64}' | base64 -d`),
]),
}),
});
});

test('Local bundling pnpm with .npmrc uses PowerShell base64 on win32', () => {
// Simulate Windows environment
const osPlatformMock = jest.spyOn(os, 'platform').mockReturnValue('win32');

// Mock spawnSync to capture the command that tryBundle() executes
const spawnSyncMock = jest.spyOn(child_process, 'spawnSync').mockReturnValue({
status: 0,
stderr: Buffer.from('stderr'),
stdout: Buffer.from('stdout'),
pid: 123,
output: ['stdout', 'stderr'],
signal: null,
});

const pnpmLock = path.join(__dirname, '..', 'pnpm-lock.yaml');
const npmrcFixture = path.join(__dirname, '.testnpmrc');
const npmrcBase64 = Buffer.from(fs.readFileSync(npmrcFixture, 'utf-8')).toString('base64');

const originalFindUp = util.findUp;
jest.spyOn(util, 'findUp').mockImplementation((name, dir) =>
name === '.npmrc' ? npmrcFixture : originalFindUp(name, dir),
);

const bundler = new Bundling(stack, {
entry: __filename,
projectRoot: path.dirname(pnpmLock),
depsLockFilePath: pnpmLock,
runtime: STANDARD_RUNTIME,
architecture: Architecture.X86_64,
nodeModules: ['delay'],
});

// Call tryBundle() directly to test local bundling.
// Local bundling uses os.platform() to determine the shell (cmd on win32, bash on Linux).
// This is different from Docker bundling (forceDockerBundling: true) which always uses bash.
bundler.local?.tryBundle('/outdir', { image: STANDARD_RUNTIME.bundlingImage });

// Verify the command uses PowerShell to decode base64 on Windows
expect(spawnSyncMock).toHaveBeenCalledWith(
'cmd',
['/c', expect.stringContaining(`[System.Convert]::FromBase64String('${npmrcBase64}')`)],
expect.anything(),
);

osPlatformMock.mockRestore();
spawnSyncMock.mockRestore();
});

test('pnpm with nodeModules does not write .npmrc when none exists', () => {
const pnpmLock = '/project/pnpm-lock.yaml';

// Mock findUp to return undefined for .npmrc (simulating no .npmrc file found)
const originalFindUp = util.findUp;
jest.spyOn(util, 'findUp').mockImplementation((name, dir) =>
name === '.npmrc' ? undefined : originalFindUp(name, dir),
);

Bundling.bundle(stack, {
entry: __filename,
projectRoot,
depsLockFilePath: pnpmLock,
runtime: STANDARD_RUNTIME,
architecture: Architecture.X86_64,
nodeModules: ['delay'],
forceDockerBundling: true,
});

// Verify the bundling command was generated
expect(Code.fromAsset).toHaveBeenCalledWith(path.dirname(pnpmLock), expect.anything());

// Extract the command and verify it does NOT contain base64 decoding for .npmrc
const call = (Code.fromAsset as jest.Mock).mock.calls[0];
const bundlingCommand = call[1].bundling.command[2]; // The shell command string
expect(bundlingCommand).not.toContain('base64 -d');
expect(bundlingCommand).not.toContain('.npmrc');
});

test('Detects bun.lockb', () => {
const bunLock = path.join(__dirname, '..', 'bun.lockb');
Bundling.bundle(stack, {
Expand Down
Loading