diff --git a/API.md b/API.md index 0b7fd9c3..4f8547f2 100644 --- a/API.md +++ b/API.md @@ -129,7 +129,7 @@ public readonly buildFn: any; ``` - *Type:* `any` -- *Default:* esbuild.buildSync +- *Default:* `esbuild.buildSync` Escape hatch to provide the bundler with a custom build function. @@ -205,6 +205,28 @@ This is the same as setting the ESBUILD_BINARY_PATH environment variable. --- +##### `esbuildModulePath`Optional + +```typescript +public readonly esbuildModulePath: string; +``` + +- *Type:* `string` +- *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) + +Path used to import the esbuild module. + +If not set, the module path will be determined in the following order: + +- Use a path from the `CDK_ESBUILD_MODULE_PATH` environment variable +- In TypeScript, fallback to the default Node.js package resolution mechanism +- All other languages (Python, Go, .NET, Java) use an automatic "best effort" resolution mechanism. \ + The exact algorithm of this mechanism is considered an implementation detail and should not be relied on. + If `esbuild` cannot be found, it might be installed dynamically to a temporary location. + To opt-out of this behavior, set either `esbuildModulePath` or `CDK_ESBUILD_MODULE_PATH` env variable. + +--- + ##### `entryPoints`Required ```typescript @@ -979,7 +1001,7 @@ public readonly buildFn: any; ``` - *Type:* `any` -- *Default:* esbuild.buildSync +- *Default:* `esbuild.buildSync` Escape hatch to provide the bundler with a custom build function. @@ -1055,6 +1077,28 @@ This is the same as setting the ESBUILD_BINARY_PATH environment variable. --- +##### `esbuildModulePath`Optional + +```typescript +public readonly esbuildModulePath: string; +``` + +- *Type:* `string` +- *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) + +Path used to import the esbuild module. + +If not set, the module path will be determined in the following order: + +- Use a path from the `CDK_ESBUILD_MODULE_PATH` environment variable +- In TypeScript, fallback to the default Node.js package resolution mechanism +- All other languages (Python, Go, .NET, Java) use an automatic "best effort" resolution mechanism. \ + The exact algorithm of this mechanism is considered an implementation detail and should not be relied on. + If `esbuild` cannot be found, it might be installed dynamically to a temporary location. + To opt-out of this behavior, set either `esbuildModulePath` or `CDK_ESBUILD_MODULE_PATH` env variable. + +--- + ### CodeConfig Result of binding `Code` into a `Function`. @@ -1123,7 +1167,7 @@ public readonly buildFn: any; ``` - *Type:* `any` -- *Default:* esbuild.buildSync +- *Default:* `esbuild.buildSync` Escape hatch to provide the bundler with a custom build function. @@ -1199,6 +1243,28 @@ This is the same as setting the ESBUILD_BINARY_PATH environment variable. --- +##### `esbuildModulePath`Optional + +```typescript +public readonly esbuildModulePath: string; +``` + +- *Type:* `string` +- *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) + +Path used to import the esbuild module. + +If not set, the module path will be determined in the following order: + +- Use a path from the `CDK_ESBUILD_MODULE_PATH` environment variable +- In TypeScript, fallback to the default Node.js package resolution mechanism +- All other languages (Python, Go, .NET, Java) use an automatic "best effort" resolution mechanism. \ + The exact algorithm of this mechanism is considered an implementation detail and should not be relied on. + If `esbuild` cannot be found, it might be installed dynamically to a temporary location. + To opt-out of this behavior, set either `esbuildModulePath` or `CDK_ESBUILD_MODULE_PATH` env variable. + +--- + ##### `assetHash`Optional ```typescript @@ -1232,7 +1298,7 @@ public readonly buildFn: any; ``` - *Type:* `any` -- *Default:* esbuild.buildSync +- *Default:* `esbuild.buildSync` Escape hatch to provide the bundler with a custom build function. @@ -1308,6 +1374,28 @@ This is the same as setting the ESBUILD_BINARY_PATH environment variable. --- +##### `esbuildModulePath`Optional + +```typescript +public readonly esbuildModulePath: string; +``` + +- *Type:* `string` +- *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) + +Path used to import the esbuild module. + +If not set, the module path will be determined in the following order: + +- Use a path from the `CDK_ESBUILD_MODULE_PATH` environment variable +- In TypeScript, fallback to the default Node.js package resolution mechanism +- All other languages (Python, Go, .NET, Java) use an automatic "best effort" resolution mechanism. \ + The exact algorithm of this mechanism is considered an implementation detail and should not be relied on. + If `esbuild` cannot be found, it might be installed dynamically to a temporary location. + To opt-out of this behavior, set either `esbuildModulePath` or `CDK_ESBUILD_MODULE_PATH` env variable. + +--- + ##### `assetHash`Optional ```typescript @@ -1348,6 +1436,28 @@ This is the same as setting the ESBUILD_BINARY_PATH environment variable. --- +##### `esbuildModulePath`Optional + +```typescript +public readonly esbuildModulePath: string; +``` + +- *Type:* `string` +- *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) + +Path used to import the esbuild module. + +If not set, the module path will be determined in the following order: + +- Use a path from the `CDK_ESBUILD_MODULE_PATH` environment variable +- In TypeScript, fallback to the default Node.js package resolution mechanism +- All other languages (Python, Go, .NET, Java) use an automatic "best effort" resolution mechanism. \ + The exact algorithm of this mechanism is considered an implementation detail and should not be relied on. + If `esbuild` cannot be found, it might be installed dynamically to a temporary location. + To opt-out of this behavior, set either `esbuildModulePath` or `CDK_ESBUILD_MODULE_PATH` env variable. + +--- + ##### `transformFn`Optional ```typescript @@ -1355,7 +1465,7 @@ public readonly transformFn: any; ``` - *Type:* `any` -- *Default:* esbuild.transformSync +- *Default:* `esbuild.transformSync` Escape hatch to provide the bundler with a custom transform function. @@ -1853,7 +1963,7 @@ public readonly buildFn: any; ``` - *Type:* `any` -- *Default:* esbuild.buildSync +- *Default:* `esbuild.buildSync` Escape hatch to provide the bundler with a custom build function. @@ -1929,6 +2039,28 @@ This is the same as setting the ESBUILD_BINARY_PATH environment variable. --- +##### `esbuildModulePath`Optional + +```typescript +public readonly esbuildModulePath: string; +``` + +- *Type:* `string` +- *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) + +Path used to import the esbuild module. + +If not set, the module path will be determined in the following order: + +- Use a path from the `CDK_ESBUILD_MODULE_PATH` environment variable +- In TypeScript, fallback to the default Node.js package resolution mechanism +- All other languages (Python, Go, .NET, Java) use an automatic "best effort" resolution mechanism. \ + The exact algorithm of this mechanism is considered an implementation detail and should not be relied on. + If `esbuild` cannot be found, it might be installed dynamically to a temporary location. + To opt-out of this behavior, set either `esbuildModulePath` or `CDK_ESBUILD_MODULE_PATH` env variable. + +--- + ##### `assetHash`Optional ```typescript @@ -1962,7 +2094,7 @@ public readonly buildFn: any; ``` - *Type:* `any` -- *Default:* esbuild.buildSync +- *Default:* `esbuild.buildSync` Escape hatch to provide the bundler with a custom build function. @@ -2038,6 +2170,28 @@ This is the same as setting the ESBUILD_BINARY_PATH environment variable. --- +##### `esbuildModulePath`Optional + +```typescript +public readonly esbuildModulePath: string; +``` + +- *Type:* `string` +- *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) + +Path used to import the esbuild module. + +If not set, the module path will be determined in the following order: + +- Use a path from the `CDK_ESBUILD_MODULE_PATH` environment variable +- In TypeScript, fallback to the default Node.js package resolution mechanism +- All other languages (Python, Go, .NET, Java) use an automatic "best effort" resolution mechanism. \ + The exact algorithm of this mechanism is considered an implementation detail and should not be relied on. + If `esbuild` cannot be found, it might be installed dynamically to a temporary location. + To opt-out of this behavior, set either `esbuildModulePath` or `CDK_ESBUILD_MODULE_PATH` env variable. + +--- + ##### `assetHash`Optional ```typescript diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index de88d7d3..d36b8ece 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,14 +13,14 @@ Like every Construct, *cdk-esbuild* is a [jsii](https://github.com/aws/jsii) pro Sometimes it is required to test these generated packages in a real life environment. All paths in the instructions below, will assume you are testing with one of the examples. -### NodeJs +### Node.js **Option 1:** *This is the preferred approach, as it is more consistent and closer to how npm would behave for a real user.* - `pj build` -- The NodeJS package can be found in `dist/js` +- The Node.js package can be found in `dist/js` - In your Python app, run `npm install ../../dist/js/cdk-esbuild@0.0.0.jsii.tgz` (path to the file in dist) - `npx cdk synth` will use the locally build version diff --git a/src/bundler.ts b/src/bundler.ts index 7260f367..9668cc7c 100644 --- a/src/bundler.ts +++ b/src/bundler.ts @@ -7,7 +7,7 @@ import { ILocalBundling, } from 'aws-cdk-lib'; import { BuildOptions } from './esbuild-types'; -import { buildSync, wrapWithEsbuildBinaryPath } from './esbuild-wrapper'; +import { detectEsbuildModulePath, esbuild, wrapWithEsbuildBinaryPath } from './esbuild-wrapper'; /** * A path or list or map of paths to the entry points of your code. @@ -72,7 +72,7 @@ export interface BundlerProps { * * @stability stable */ - readonly copyDir?: string | string[] | Record; + readonly copyDir?: string | string[] | Record; /** @@ -84,7 +84,7 @@ export interface BundlerProps { * @type esbuild.buildSync * @returns esbuild.BuildResult * @throws esbuild.BuildFailure - * @default esbuild.buildSync + * @default `esbuild.buildSync` */ readonly buildFn?: any; @@ -96,6 +96,23 @@ export interface BundlerProps { * @stability experimental */ readonly esbuildBinaryPath?: string; + + /** + * Path used to import the esbuild module. + * + * If not set, the module path will be determined in the following order: + * + * - Use a path from the `CDK_ESBUILD_MODULE_PATH` environment variable + * - In TypeScript, fallback to the default Node.js package resolution mechanism + * - All other languages (Python, Go, .NET, Java) use an automatic "best effort" resolution mechanism. \ + * The exact algorithm of this mechanism is considered an implementation detail and should not be relied on. + * If `esbuild` cannot be found, it might be installed dynamically to a temporary location. + * To opt-out of this behavior, set either `esbuildModulePath` or `CDK_ESBUILD_MODULE_PATH` env variable. + * + * @stability experimental + * @default - `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) + */ + readonly esbuildModulePath?: string; } /** @@ -162,7 +179,7 @@ export class EsbuildBundler { this.props?.buildOptions?.absWorkingDir ?? process.cwd(), src, ); - const destDir = resolve(outputDir, dest) ; + const destDir = resolve(outputDir, dest); const destToOutput = relative(outputDir, destDir); if (destToOutput.startsWith('..') || isAbsolute(destToOutput)) { @@ -175,7 +192,7 @@ export class EsbuildBundler { } try { - const { buildFn = buildSync } = this.props; + const { buildFn = esbuild(detectEsbuildModulePath(props.esbuildModulePath)).buildSync } = this.props; wrapWithEsbuildBinaryPath(buildFn, this.props.esbuildBinaryPath)({ entryPoints, color: process.env.NO_COLOR ? Boolean(process.env.NO_COLOR) : undefined, diff --git a/src/esbuild-wrapper.ts b/src/esbuild-wrapper.ts index 58dbd7ad..5d2eca0f 100644 --- a/src/esbuild-wrapper.ts +++ b/src/esbuild-wrapper.ts @@ -1,13 +1,15 @@ -/* eslint-disable import/no-extraneous-dependencies */ +import { analyzeMetafileSync, buildSync, transformSync, version } from './esbuild-types'; -function esbuild() { +export function esbuild(modulePath: string = 'esbuild'): { + buildSync: typeof buildSync; + transformSync: typeof transformSync; + analyzeMetafileSync: typeof analyzeMetafileSync; + version: typeof version; +} { // eslint-disable-next-line @typescript-eslint/no-require-imports - return require('esbuild'); + return require(modulePath); } -export const buildSync = esbuild().buildSync; -export const transformSync = esbuild().transformSync; - export function wrapWithEsbuildBinaryPath(fn: T, esbuildBinaryPath?: string) { if (!esbuildBinaryPath) { return fn; @@ -31,4 +33,8 @@ export function wrapWithEsbuildBinaryPath(fn: T, esb return result; }; +} + +export function detectEsbuildModulePath(esbuildBinaryPath?: string) { + return esbuildBinaryPath || process.env.CDK_ESBUILD_MODULE_PATH || 'esbuild'; } \ No newline at end of file diff --git a/src/inline-code.ts b/src/inline-code.ts index 168a0e55..aaabeebb 100644 --- a/src/inline-code.ts +++ b/src/inline-code.ts @@ -2,7 +2,7 @@ import { Lazy, Stack } from 'aws-cdk-lib'; import { CodeConfig, InlineCode } from 'aws-cdk-lib/aws-lambda'; import { Construct, Node } from 'constructs'; import { TransformOptions, Loader } from './esbuild-types'; -import { transformSync, wrapWithEsbuildBinaryPath } from './esbuild-wrapper'; +import { detectEsbuildModulePath, esbuild, wrapWithEsbuildBinaryPath } from './esbuild-wrapper'; /** * @stability experimental @@ -25,7 +25,7 @@ export interface TransformerProps { * @type esbuild.transformSync * @returns esbuild.TransformResult * @throws esbuild.TransformFailure - * @default esbuild.transformSync + * @default `esbuild.transformSync` */ readonly transformFn?: any; @@ -37,6 +37,23 @@ export interface TransformerProps { * @stability experimental */ readonly esbuildBinaryPath?: string; + + /** + * Path used to import the esbuild module. + * + * If not set, the module path will be determined in the following order: + * + * - Use a path from the `CDK_ESBUILD_MODULE_PATH` environment variable + * - In TypeScript, fallback to the default Node.js package resolution mechanism + * - All other languages (Python, Go, .NET, Java) use an automatic "best effort" resolution mechanism. \ + * The exact algorithm of this mechanism is considered an implementation detail and should not be relied on. + * If `esbuild` cannot be found, it might be installed dynamically to a temporary location. + * To opt-out of this behavior, set either `esbuildModulePath` or `CDK_ESBUILD_MODULE_PATH` env variable. + * + * @stability experimental + * @default - `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) + */ + readonly esbuildModulePath?: string; } abstract class BaseInlineCode extends InlineCode { @@ -53,7 +70,7 @@ abstract class BaseInlineCode extends InlineCode { produce: () => { try { const { - transformFn = transformSync, + transformFn = esbuild(detectEsbuildModulePath(props.esbuildModulePath)).transformSync, transformOptions = {}, esbuildBinaryPath, } = props; @@ -87,6 +104,7 @@ function instanceOfTransformerProps(object: any): object is TransformerProps { 'transformOptions', 'transformFn', 'esbuildBinaryPath', + 'esbuildModulePath', ].reduce( (isTransformerProps: boolean, propToCheck: string): boolean => (isTransformerProps || (propToCheck in object)), diff --git a/test/bundler.test.ts b/test/bundler.test.ts index d32bb71b..c8f1d90b 100644 --- a/test/bundler.test.ts +++ b/test/bundler.test.ts @@ -2,12 +2,13 @@ import { FileSystem } from 'aws-cdk-lib'; import { mocked } from 'jest-mock'; import { EsbuildBundler } from '../src/bundler'; import { BuildOptions, BuildResult } from '../src/esbuild-types'; -import { buildSync } from '../src/esbuild-wrapper'; +import { esbuild } from '../src/esbuild-wrapper'; jest.mock('esbuild', () => ({ buildSync: jest.fn(), })); +const buildSync = esbuild().buildSync; const realEsbuild = jest.requireActual('esbuild'); describe('bundling', () => { diff --git a/test/code.test.ts b/test/code.test.ts index e682b9cc..fb7abcab 100644 --- a/test/code.test.ts +++ b/test/code.test.ts @@ -9,7 +9,10 @@ import { Function, Runtime as LambdaRuntime } from 'aws-cdk-lib/aws-lambda'; import { mocked } from 'jest-mock'; import { JavaScriptCode, TypeScriptCode } from '../src/code'; import { BuildOptions } from '../src/esbuild-types'; -import { buildSync } from '../src/esbuild-wrapper'; +import * as provider from '../src/esbuild-wrapper'; + +const esbuildSpy = jest.spyOn(provider, 'esbuild'); +const buildSync = provider.esbuild().buildSync; describe('code', () => { describe('entrypoint is an absolute path', () => { @@ -146,7 +149,6 @@ describe('code', () => { }); }); - describe('given a custom esbuildBinaryPath', () => { it('should set the ESBUILD_BINARY_PATH env variable', () => { const mockLogger = jest.fn(); @@ -176,6 +178,94 @@ describe('given a custom esbuildBinaryPath', () => { }); }); +describe('with an esbuild module path from', () => { + let stack: Stack; + beforeEach(() => { + esbuildSpy.mockClear(); + stack = new Stack(); + }); + afterAll(() => { + esbuildSpy.mockRestore(); + }); + + describe('the default', () => { + it('should call the esbuild provider with "esbuild"', () => { + const code = new TypeScriptCode('fixtures/handlers/ts-handler.ts', { + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + new Function(stack, 'MyFunction', { + runtime: LambdaRuntime.NODEJS_14_X, + handler: 'index.handler', + code, + }); + + expect(esbuildSpy).toHaveBeenCalledTimes(1); + expect(esbuildSpy).toHaveBeenCalledWith('esbuild'); + }); + }); + + describe('`esbuildModulePath` prop', () => { + it('should use the path from the prop', () => { + const code = new TypeScriptCode('fixtures/handlers/ts-handler.ts', { + buildOptions: { absWorkingDir: resolve(__dirname) }, + esbuildModulePath: '../node_modules/esbuild', + }); + + new Function(stack, 'MyFunction', { + runtime: LambdaRuntime.NODEJS_14_X, + handler: 'index.handler', + code, + }); + + expect(esbuildSpy).toHaveBeenCalledTimes(1); + expect(esbuildSpy).toHaveBeenCalledWith('../node_modules/esbuild'); + }); + }); + + describe('`CDK_ESBUILD_MODULE_PATH` env var', () => { + beforeEach(() => { + process.env.CDK_ESBUILD_MODULE_PATH = '../node_modules/esbuild'; + }); + afterEach(() => { + delete process.env.CDK_ESBUILD_MODULE_PATH; + }); + + it('should use the path from the env var', () => { + const code = new TypeScriptCode('fixtures/handlers/ts-handler.ts', { + buildOptions: { absWorkingDir: resolve(__dirname) }, + }); + + new Function(stack, 'MyFunction', { + runtime: LambdaRuntime.NODEJS_14_X, + handler: 'index.handler', + code, + }); + + expect(esbuildSpy).toHaveBeenCalledTimes(1); + expect(esbuildSpy).toHaveBeenCalledWith('../node_modules/esbuild'); + }); + + describe('and `esbuildModulePath` prop', () => { + it('should prefer the path from prop', () => { + const code = new TypeScriptCode('fixtures/handlers/ts-handler.ts', { + buildOptions: { absWorkingDir: resolve(__dirname) }, + esbuildModulePath: '../test/../node_modules/esbuild', + }); + + new Function(stack, 'MyFunction', { + runtime: LambdaRuntime.NODEJS_14_X, + handler: 'index.handler', + code, + }); + + expect(esbuildSpy).toHaveBeenCalledTimes(1); + expect(esbuildSpy).toHaveBeenCalledWith('../test/../node_modules/esbuild'); + }); + }); + }); +}); + describe('AWS Lambda', () => { describe('TypeScriptCode can be used in Lambda Function', () => { it('should not throw', () => { diff --git a/test/inline-code.test.ts b/test/inline-code.test.ts index 0b0fd278..97740f48 100644 --- a/test/inline-code.test.ts +++ b/test/inline-code.test.ts @@ -1,13 +1,16 @@ import { App, Stack } from 'aws-cdk-lib'; import { Function, Runtime } from 'aws-cdk-lib/aws-lambda'; import { mocked } from 'jest-mock'; -import { transformSync } from '../lib/esbuild-wrapper'; import { InlineJavaScriptCode, InlineJsxCode, InlineTsxCode, InlineTypeScriptCode, } from '../src'; +import * as provider from '../src/esbuild-wrapper'; + +const esbuildSpy = jest.spyOn(provider, 'esbuild'); +const transformSync = provider.esbuild().transformSync; describe('using transformOptions', () => { describe('given a banner code', () => { @@ -209,6 +212,65 @@ describe('using transformerProps', () => { }); }); + describe('with an esbuild module path from', () => { + beforeEach(() => { + esbuildSpy.mockClear(); + }); + afterAll(() => { + esbuildSpy.mockRestore(); + }); + + describe('the default', () => { + it('should call the esbuild provider with "esbuild"', () => { + const code = new InlineTypeScriptCode('let x: number = 1'); + code.bind(new Stack()); + + expect(esbuildSpy).toHaveBeenCalledTimes(1); + expect(esbuildSpy).toHaveBeenCalledWith('esbuild'); + }); + }); + + describe('`esbuildModulePath` prop', () => { + it('should use the path from the prop', () => { + const code = new InlineTypeScriptCode('let x: number = 1', { + esbuildModulePath: '../node_modules/esbuild', + }); + code.bind(new Stack()); + + expect(esbuildSpy).toHaveBeenCalledTimes(1); + expect(esbuildSpy).toHaveBeenCalledWith('../node_modules/esbuild'); + }); + }); + + describe('`CDK_ESBUILD_MODULE_PATH` env var', () => { + beforeEach(() => { + process.env.CDK_ESBUILD_MODULE_PATH = '../node_modules/esbuild'; + }); + afterEach(() => { + delete process.env.CDK_ESBUILD_MODULE_PATH; + }); + + it('should use the path from the env var', () => { + const code = new InlineTypeScriptCode('let x: number = 1'); + code.bind(new Stack()); + + expect(esbuildSpy).toHaveBeenCalledTimes(1); + expect(esbuildSpy).toHaveBeenCalledWith('../node_modules/esbuild'); + }); + + describe('and `esbuildModulePath` prop', () => { + it('should prefer the path from prop', () => { + const code = new InlineTypeScriptCode('let x: number = 1', { + esbuildModulePath: '../test/../node_modules/esbuild', + }); + code.bind(new Stack()); + + expect(esbuildSpy).toHaveBeenCalledTimes(1); + expect(esbuildSpy).toHaveBeenCalledWith('../test/../node_modules/esbuild'); + }); + }); + }); + }); describe('with logLevel', () => { describe('not provided', () => { diff --git a/test/source.test.ts b/test/source.test.ts index f54f28e4..751e29f8 100644 --- a/test/source.test.ts +++ b/test/source.test.ts @@ -3,9 +3,11 @@ import { RemovalPolicy, Stack } from 'aws-cdk-lib'; import { Bucket } from 'aws-cdk-lib/aws-s3'; import { BucketDeployment } from 'aws-cdk-lib/aws-s3-deployment'; import { mocked } from 'jest-mock'; -import { buildSync } from '../src/esbuild-wrapper'; +import { esbuild } from '../src/esbuild-wrapper'; import { JavaScriptSource, TypeScriptSource } from '../src/source'; +const buildSync = esbuild().buildSync; + describe('source', () => { describe('entrypoint is an absolute path', () => { describe('outside of the esbuild working dir', () => {