diff --git a/.projenrc.ts b/.projenrc.ts index beeb4509..59fb1f45 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -9,6 +9,7 @@ import { InternalConsoleOptions } from 'projen/lib/vscode'; import { SourceFile } from 'ts-morph'; import { tagOnNpm } from './projenrc/release'; import { TypeScriptSourceFile } from './projenrc/TypeScriptSourceFile'; +import { Esbuild } from './src/esbuild-source'; const project = new awscdk.AwsCdkConstructLibrary({ projenrcTs: true, @@ -89,7 +90,7 @@ const project = new awscdk.AwsCdkConstructLibrary({ devDeps: [ '@aws-cdk/aws-synthetics-alpha@2.0.0-alpha.11', '@types/eslint', - 'esbuild@^0.15.0', + Esbuild.spec, 'jest-mock', 'ts-morph', ], @@ -252,7 +253,7 @@ launchConfig?.addOverride('configurations.0.cwd', '${workspaceFolder}'); // esbuild project.tryFindObjectFile('package.json')?.addOverride('optionalDependencies', { - esbuild: '^0.15.0', + [Esbuild.name]: Esbuild.version, }); new TypeScriptSourceFile(project, 'src/esbuild-types.ts', { diff --git a/API.md b/API.md index 1ece4ab7..7340610d 100644 --- a/API.md +++ b/API.md @@ -214,9 +214,9 @@ public readonly esbuildModulePath: string; - *Type:* `string` - *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) -Path used to import the esbuild module. +Absolute path to the esbuild module JS file. -Python, Go, .NET and Java should use an absolute path, because the jsii execution environment uses a temporary working directory. +E.g. "/home/user/.npm/node_modules/esbuild/lib/main.js" If not set, the module path will be determined in the following order: @@ -1088,9 +1088,9 @@ public readonly esbuildModulePath: string; - *Type:* `string` - *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) -Path used to import the esbuild module. +Absolute path to the esbuild module JS file. -Python, Go, .NET and Java should use an absolute path, because the jsii execution environment uses a temporary working directory. +E.g. "/home/user/.npm/node_modules/esbuild/lib/main.js" If not set, the module path will be determined in the following order: @@ -1256,9 +1256,9 @@ public readonly esbuildModulePath: string; - *Type:* `string` - *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) -Path used to import the esbuild module. +Absolute path to the esbuild module JS file. -Python, Go, .NET and Java should use an absolute path, because the jsii execution environment uses a temporary working directory. +E.g. "/home/user/.npm/node_modules/esbuild/lib/main.js" If not set, the module path will be determined in the following order: @@ -1389,9 +1389,9 @@ public readonly esbuildModulePath: string; - *Type:* `string` - *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) -Path used to import the esbuild module. +Absolute path to the esbuild module JS file. -Python, Go, .NET and Java should use an absolute path, because the jsii execution environment uses a temporary working directory. +E.g. "/home/user/.npm/node_modules/esbuild/lib/main.js" If not set, the module path will be determined in the following order: @@ -1453,9 +1453,9 @@ public readonly esbuildModulePath: string; - *Type:* `string` - *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) -Path used to import the esbuild module. +Absolute path to the esbuild module JS file. -Python, Go, .NET and Java should use an absolute path, because the jsii execution environment uses a temporary working directory. +E.g. "/home/user/.npm/node_modules/esbuild/lib/main.js" If not set, the module path will be determined in the following order: @@ -2058,9 +2058,9 @@ public readonly esbuildModulePath: string; - *Type:* `string` - *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) -Path used to import the esbuild module. +Absolute path to the esbuild module JS file. -Python, Go, .NET and Java should use an absolute path, because the jsii execution environment uses a temporary working directory. +E.g. "/home/user/.npm/node_modules/esbuild/lib/main.js" If not set, the module path will be determined in the following order: @@ -2191,9 +2191,9 @@ public readonly esbuildModulePath: string; - *Type:* `string` - *Default:* `CDK_ESBUILD_MODULE_PATH` or package resolution (see above) -Path used to import the esbuild module. +Absolute path to the esbuild module JS file. -Python, Go, .NET and Java should use an absolute path, because the jsii execution environment uses a temporary working directory. +E.g. "/home/user/.npm/node_modules/esbuild/lib/main.js" If not set, the module path will be determined in the following order: @@ -2448,6 +2448,105 @@ Determines whether this Code is inline code or not. --- +### EsbuildSource + +#### Initializers + +```typescript +import { EsbuildSource } from '@mrgrain/cdk-esbuild' + +new EsbuildSource() +``` + + + +#### Properties + +##### `auto`Required + +```typescript +public readonly auto: string; +``` + +- *Type:* `string` + +First try to find to module, then install it to a temporary location. + +--- + +##### `install`Required + +```typescript +public readonly install: string; +``` + +- *Type:* `string` + +Install the module to a temporary location. + +--- + +##### `nodeJs`Required + +```typescript +public readonly nodeJs: string; +``` + +- *Type:* `string` + +Require module by name, do not attempt to find it anywhere else. + +--- + +##### `platformDefault`Required + +```typescript +public readonly platformDefault: string; +``` + +- *Type:* `string` + +`EsbuildSource.nodeJs` for NodeJs, `EsbuildSource.auto` for all other languages. + +--- + +##### `anywhere`Optional + +```typescript +public readonly anywhere: string; +``` + +- *Type:* `string` + +Try to find the module in most common paths. + +--- + +##### `globalPaths`Optional + +```typescript +public readonly globalPaths: string; +``` + +- *Type:* `string` + +Try to find the module in common global installation paths. + +--- + +##### `default`Optional + +```typescript +public readonly default: string; +``` + +- *Type:* `string` + +Set the default mechanism to find the module The current default to find the module. + +--- + + ### InlineJavaScriptCode An implementation of `lambda.InlineCode` using the esbuild Transform API. Inline function code is limited to 4 KiB after transformation. diff --git a/examples/python-app/python_app/python_app_stack.py b/examples/python-app/python_app/python_app_stack.py index 6c2ea7d1..c2a136f9 100644 --- a/examples/python-app/python_app/python_app_stack.py +++ b/examples/python-app/python_app/python_app_stack.py @@ -8,6 +8,8 @@ InlineTypeScriptCode, TypeScriptCode, TypeScriptSource, + TransformerProps, + EsbuildSource, ) @@ -15,6 +17,8 @@ class PythonAppStack(Stack): def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: super().__init__(scope, construct_id, **kwargs) + EsbuildSource.default = EsbuildSource.global_paths + s3_deployment.BucketDeployment( self, "Website", @@ -22,6 +26,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: TypeScriptSource( "lambda-handler/index.ts", copy_dir="lambda-handler", + esbuild_module_path=None, # Use default ) ], destination_bucket=s3.Bucket( @@ -38,11 +43,13 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: code=TypeScriptCode( "lambda-handler/index.ts", build_options=BuildOptions( - format="esm", outfile="index.mjs", external=["aws-sdk"] + format="esm", + outfile="index.mjs", + external=["aws-sdk"], + log_level="verbose", ), - # Override the global setting with a specific path per Construct - # This can be useful if a Construct requires a different version of esbuild - esbuild_module_path="/project/node_modules/esbuild@13", + # Override the default setting with a specific path per Construct + esbuild_module_path=EsbuildSource.install, ), ) @@ -57,6 +64,10 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: export function handler() { console.log(hello); } - """ + """, + props=TransformerProps( + # Try to find the package anywhere, but don't install it + esbuild_module_path=EsbuildSource.anywhere + ), ), ) diff --git a/src/bundler.ts b/src/bundler.ts index c0da476d..ecbc59e7 100644 --- a/src/bundler.ts +++ b/src/bundler.ts @@ -6,8 +6,8 @@ import { FileSystem, ILocalBundling, } from 'aws-cdk-lib'; +import { EsbuildProvider } from './esbuild-provider'; import { BuildOptions } from './esbuild-types'; -import { detectEsbuildModulePath, esbuild, wrapWithEsbuildBinaryPath } from './esbuild-wrapper'; import { errorHasCode } from './utils'; /** @@ -99,9 +99,9 @@ export interface BundlerProps { readonly esbuildBinaryPath?: string; /** - * Path used to import the esbuild module. + * Absolute path to the esbuild module JS file. * - * Python, Go, .NET and Java should use an absolute path, because the jsii execution environment uses a temporary working directory. + * E.g. "/home/user/.npm/node_modules/esbuild/lib/main.js" * * If not set, the module path will be determined in the following order: * @@ -195,8 +195,10 @@ export class EsbuildBundler { } try { - const { buildFn = esbuild(detectEsbuildModulePath(props.esbuildModulePath)).buildSync } = this.props; - wrapWithEsbuildBinaryPath(buildFn, this.props.esbuildBinaryPath)({ + const buildFn = this.props.buildFn ?? EsbuildProvider.require(props.esbuildModulePath).buildSync; + const buildSync = EsbuildProvider.withEsbuildBinaryPath(buildFn, this.props.esbuildBinaryPath); + + buildSync({ entryPoints, color: process.env.NO_COLOR ? Boolean(process.env.NO_COLOR) : undefined, ...(this.props?.buildOptions || {}), diff --git a/src/dynamic-package.ts b/src/dynamic-package.ts new file mode 100644 index 00000000..b6102d53 --- /dev/null +++ b/src/dynamic-package.ts @@ -0,0 +1,152 @@ +import { execFileSync } from 'child_process'; +import { mkdtempSync } from 'fs'; +import { tmpdir } from 'os'; +import { join, resolve } from 'path'; +import { Lazy } from 'aws-cdk-lib'; + +export interface DynamicPackageProps { + /** + * If the package is installed, install into this directory + * + * @default - a temporary directory + */ + readonly installPath?: string; + + /** + * Additional paths to search for an existing package installation + * + * @default - a temporary directory + */ + readonly searchPaths?: string[]; +} + +export class DynamicPackage { + public readonly name: string; + + public readonly version?: string; + + public readonly installPath: string; + + public readonly searchPaths: string[]; + + public get spec(): string { + if (!this.version) { + return this.name; + } + + return `${this.name}@${this.version}`; + } + + public constructor( + /** + * Name of the npm package + * Version to install, or version constraint + * + * @default - no version constraint, install the latest version + */ + packageSpec: string, + props: DynamicPackageProps = {}, + ) { + const { name, version } = this.parsePackageSpec(packageSpec); + + this.name = name; + this.version = version; + this.installPath = + props.installPath || + mkdtempSync(join(tmpdir(), `cdk-dynamic-${this.spec}-`)); + this.searchPaths = props.searchPaths || []; + } + + protected tryResolve(paths?: string[]): string | undefined { + try { + return require.resolve(this.name, paths ? { paths } : undefined); + } catch (_) { + return; + } + } + + public auto() { + return this.tryResolve() || this.findInPaths() || this.install(); + } + + public nodeJs() { + return this.name; + } + + public findIn(paths: string[]) { + return this.tryResolve([...paths].filter(Boolean) as string[]); + } + + public findInPaths() { + return ( + this.findInSearchPaths() || + this.findInLocalPaths() || + this.findInGlobalPaths() + ); + } + + public findInSearchPaths() { + return this.findIn(this.searchPaths); + } + + public findInLocalPaths() { + this.findIn([process.cwd(), process.env.PWD].filter(Boolean) as string[]); + } + + public findInGlobalPaths() { + return this.findIn([ + process.execPath, + resolve(process.execPath, '../..'), + resolve(process.execPath, '../../lib'), + resolve(process.execPath, '../../node_modules'), + resolve(process.execPath, '../../lib/node_modules'), + ]); + } + + private static installedPackagePath = new Map(); + public install() { + return Lazy.string({ + produce: () => { + if (!DynamicPackage.installedPackagePath.has(this.spec)) { + const args = [ + 'install', + this.spec, + '--no-save', + '--prefix', + this.installPath, + ]; + + DynamicPackage.log(`Dynamically installing ${this.spec} into "${this.installPath}"...`, 'info'); + execFileSync('npm', args); + + DynamicPackage.installedPackagePath.set( + this.spec, + require.resolve(this.name, { + paths: [this.installPath], + }), + ); + } + + return DynamicPackage.installedPackagePath.get(this.spec); + }, + }); + } + + protected static log(message: string, _level: string = 'info') { + process.stderr.write(`⬥ ${message}\n`); + } + + private parsePackageSpec(spec: string) { + const hasScope = spec.startsWith('@'); + if (hasScope) { + spec = spec.substring(1); + } + const [module, ...version] = spec.split('@'); + const name = hasScope ? `@${module}` : module; + if (version.length == 0) { + return { name }; + } + + return { name, version: version?.join('@') }; + } +} diff --git a/src/esbuild-provider.ts b/src/esbuild-provider.ts new file mode 100644 index 00000000..d2372a4f --- /dev/null +++ b/src/esbuild-provider.ts @@ -0,0 +1,73 @@ +import { DefaultTokenResolver, StringConcat, Token, Tokenization } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { Esbuild, EsbuildSource } from './esbuild-source'; +import { analyzeMetafileSync, buildSync, transformSync, version } from './esbuild-types'; + +interface Esbuild { + buildSync: typeof buildSync; + transformSync: typeof transformSync; + analyzeMetafileSync: typeof analyzeMetafileSync; + version: typeof version; +} + +export class EsbuildProvider { + /** + * Load the esbuild module according to defined rules. + */ + public static require(path?: string): Esbuild { + const module = path || process.env.CDK_ESBUILD_MODULE_PATH || EsbuildSource.default || Esbuild.name; + + return this._require(this.resolve(module)); + } + + /** + * @internal + */ + public static _require(path: string): Esbuild { + // eslint-disable-next-line @typescript-eslint/no-require-imports + return require(path); + } + + /** + * Invoke a function with a specific `process.env.ESBUILD_BINARY_PATH` + * and restore the env var afterwards. + */ + public static withEsbuildBinaryPath(fn: T, esbuildBinaryPath?: string) { + if (!esbuildBinaryPath) { + return fn; + } + + return (...args: unknown[]) => { + const originalEsbuildBinaryPath = process.env.ESBUILD_BINARY_PATH; + if (esbuildBinaryPath) { + process.env.ESBUILD_BINARY_PATH = esbuildBinaryPath; + } + + const result = fn(...args); + + /** + * only reset `ESBUILD_BINARY_PATH` if it was explicitly set via the construct props + * since `esbuild` itself sometimes sets it (eg. when running in yarn 2 plug&play) + */ + if (esbuildBinaryPath) { + process.env.ESBUILD_BINARY_PATH = originalEsbuildBinaryPath; + } + + return result; + }; + } + + /** + * Resolve a token without context + */ + private static resolve(token: string): string { + if (!Token.isUnresolved(token)) { + return token; + } + + return Tokenization.resolve(token, { + scope: new Construct(undefined as any, ''), + resolver: new DefaultTokenResolver(new StringConcat()), + }); + } +} diff --git a/src/esbuild-source.ts b/src/esbuild-source.ts new file mode 100644 index 00000000..85ac8436 --- /dev/null +++ b/src/esbuild-source.ts @@ -0,0 +1,74 @@ +import { DynamicPackage } from './dynamic-package'; + +const dynamicEsbuild = new DynamicPackage('esbuild@^0.15.0'); + +export const Esbuild = { + name: dynamicEsbuild.name, + version: dynamicEsbuild.version, + spec: dynamicEsbuild.spec, +}; + +export class EsbuildSource { + private static dynamicPackage = dynamicEsbuild; + private static _default?: string; + + /** + * Set the default mechanism to find the module + */ + public static set default(path: string | undefined) { + this._default = path; + } + + /** + * The current default to find the module + */ + public static get default(): string | undefined { + return this._default ?? this.platformDefault; + } + + /** + * `EsbuildSource.nodeJs` for NodeJs, `EsbuildSource.auto` for all other languages + */ + public static get platformDefault() { + if (Boolean(process.env.JSII_AGENT)) { + return this.auto; + } + + return this.nodeJs; + } + + /** + * Try to find the module in most common paths. + */ + public static get anywhere() { + return this.dynamicPackage.findInPaths(); + } + + /** + * Try to find the module in common global installation paths. + */ + public static get globalPaths() { + return this.dynamicPackage.findInGlobalPaths(); + } + + /** + * Require module by name, do not attempt to find it anywhere else. + */ + public static get nodeJs() { + return this.dynamicPackage.nodeJs(); + } + + /** + * Install the module to a temporary location. + */ + public static get install() { + return this.dynamicPackage.install(); + } + + /** + * First try to find to module, then install it to a temporary location. + */ + public static get auto() { + return this.dynamicPackage.auto(); + } +} diff --git a/src/esbuild-wrapper.ts b/src/esbuild-wrapper.ts deleted file mode 100644 index 08253ce4..00000000 --- a/src/esbuild-wrapper.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { analyzeMetafileSync, buildSync, transformSync, version } from './esbuild-types'; - -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(modulePath); -} - -export function wrapWithEsbuildBinaryPath(fn: T, esbuildBinaryPath?: string) { - if (!esbuildBinaryPath) { - return fn; - } - - return (...args: unknown[]) => { - const originalEsbuildBinaryPath = process.env.ESBUILD_BINARY_PATH; - if (esbuildBinaryPath) { - process.env.ESBUILD_BINARY_PATH = esbuildBinaryPath; - } - - const result = fn(...args); - - /** - * only reset `ESBUILD_BINARY_PATH` if it was explicitly set via the construct props - * since `esbuild` itself sometimes sets it (eg. when running in yarn 2 plug&play) - */ - if (esbuildBinaryPath) { - process.env.ESBUILD_BINARY_PATH = originalEsbuildBinaryPath; - } - - return result; - }; -} - -export function detectEsbuildModulePath(esbuildBinaryPath?: string) { - return esbuildBinaryPath || process.env.CDK_ESBUILD_MODULE_PATH || 'esbuild'; -} diff --git a/src/index.ts b/src/index.ts index 9e2807e2..35850b80 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,10 @@ export { TransformOptions, } from './esbuild-types'; +export { + EsbuildSource, +} from './esbuild-source'; + export { EsbuildBundler, BundlerProps, diff --git a/src/inline-code.ts b/src/inline-code.ts index 2835597a..7810cae0 100644 --- a/src/inline-code.ts +++ b/src/inline-code.ts @@ -1,8 +1,8 @@ import { Lazy, Stack } from 'aws-cdk-lib'; import { CodeConfig, InlineCode } from 'aws-cdk-lib/aws-lambda'; import { Construct, Node } from 'constructs'; +import { EsbuildProvider } from './esbuild-provider'; import { TransformOptions, Loader } from './esbuild-types'; -import { detectEsbuildModulePath, esbuild, wrapWithEsbuildBinaryPath } from './esbuild-wrapper'; import { errorHasCode } from './utils'; /** @@ -40,9 +40,9 @@ export interface TransformerProps { readonly esbuildBinaryPath?: string; /** - * Path used to import the esbuild module. + * Absolute path to the esbuild module JS file. * - * Python, Go, .NET and Java should use an absolute path, because the jsii execution environment uses a temporary working directory. + * E.g. "/home/user/.npm/node_modules/esbuild/lib/main.js" * * If not set, the module path will be determined in the following order: * @@ -72,16 +72,13 @@ abstract class BaseInlineCode extends InlineCode { this.inlineCode = Lazy.string({ produce: () => { try { - const { - transformFn = esbuild(detectEsbuildModulePath(props.esbuildModulePath)).transformSync, - transformOptions = {}, - esbuildBinaryPath, - } = props; + const transformFn = props.transformFn ?? EsbuildProvider.require(props.esbuildModulePath).transformSync; + const transformSync = EsbuildProvider.withEsbuildBinaryPath(transformFn, props.esbuildBinaryPath); - const transformedCode = wrapWithEsbuildBinaryPath(transformFn, esbuildBinaryPath)(code, { + const transformedCode = transformSync(code, { color: process.env.NO_COLOR ? Boolean(process.env.NO_COLOR) : undefined, logLevel: 'warning', - ...transformOptions, + ...(props.transformOptions || {}), }); return transformedCode.code; diff --git a/test/bundler.test.ts b/test/bundler.test.ts index c8f1d90b..ee36f64e 100644 --- a/test/bundler.test.ts +++ b/test/bundler.test.ts @@ -1,15 +1,13 @@ import { FileSystem } from 'aws-cdk-lib'; +import * as esbuild from 'esbuild'; import { mocked } from 'jest-mock'; import { EsbuildBundler } from '../src/bundler'; +import { EsbuildProvider } from '../src/esbuild-provider'; import { BuildOptions, BuildResult } from '../src/esbuild-types'; -import { esbuild } from '../src/esbuild-wrapper'; -jest.mock('esbuild', () => ({ - buildSync: jest.fn(), -})); - -const buildSync = esbuild().buildSync; -const realEsbuild = jest.requireActual('esbuild'); +const providerSpy = jest.spyOn(EsbuildProvider, '_require'); +const buildSync = jest.fn(); +providerSpy.mockReturnValue({ buildSync } as any); describe('bundling', () => { describe('Given a project root path', () => { @@ -69,9 +67,7 @@ describe('bundling', () => { describe('Given an outdir and outfile', () => { beforeEach(() => { mocked(buildSync).mockImplementationOnce( - (options: BuildOptions): BuildResult => { - return realEsbuild.buildSync(options); - }, + (options: BuildOptions): BuildResult => esbuild.buildSync(options), ); }); afterEach(() => { diff --git a/test/code.test.ts b/test/code.test.ts index fb7abcab..db2ee028 100644 --- a/test/code.test.ts +++ b/test/code.test.ts @@ -6,13 +6,13 @@ import { } from '@aws-cdk/aws-synthetics-alpha'; import { Stack } from 'aws-cdk-lib'; import { Function, Runtime as LambdaRuntime } from 'aws-cdk-lib/aws-lambda'; +import * as esbuild from 'esbuild'; import { mocked } from 'jest-mock'; import { JavaScriptCode, TypeScriptCode } from '../src/code'; +import { EsbuildProvider } from '../src/esbuild-provider'; import { BuildOptions } from '../src/esbuild-types'; -import * as provider from '../src/esbuild-wrapper'; -const esbuildSpy = jest.spyOn(provider, 'esbuild'); -const buildSync = provider.esbuild().buildSync; +const providerSpy = jest.spyOn(EsbuildProvider, '_require'); describe('code', () => { describe('entrypoint is an absolute path', () => { @@ -36,7 +36,7 @@ describe('code', () => { describe('within the esbuild working dir', () => { it('should be fine and rewrite the entrypoint', () => { - const customBuild = jest.fn(buildSync); + const customBuild = jest.fn(esbuild.buildSync); expect(() => { const stack = new Stack(); @@ -123,7 +123,7 @@ describe('code', () => { describe('Given a custom build function', () => { it('should call my build function', () => { - const customBuild = jest.fn(buildSync); + const customBuild = jest.fn(esbuild.buildSync); expect(() => { const stack = new Stack(); @@ -154,7 +154,7 @@ describe('given a custom esbuildBinaryPath', () => { const mockLogger = jest.fn(); const customBuild = (options: BuildOptions) => { mockLogger(process.env.ESBUILD_BINARY_PATH); - return buildSync(options); + return esbuild.buildSync(options); }; expect(() => { @@ -181,11 +181,12 @@ describe('given a custom esbuildBinaryPath', () => { describe('with an esbuild module path from', () => { let stack: Stack; beforeEach(() => { - esbuildSpy.mockClear(); + providerSpy.mockClear(); + providerSpy.mockReturnValue(esbuild); stack = new Stack(); }); afterAll(() => { - esbuildSpy.mockRestore(); + providerSpy.mockRestore(); }); describe('the default', () => { @@ -200,8 +201,8 @@ describe('with an esbuild module path from', () => { code, }); - expect(esbuildSpy).toHaveBeenCalledTimes(1); - expect(esbuildSpy).toHaveBeenCalledWith('esbuild'); + expect(providerSpy).toHaveBeenCalledTimes(1); + expect(providerSpy).toHaveBeenCalledWith('esbuild'); }); }); @@ -209,7 +210,7 @@ describe('with an esbuild module path from', () => { 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', + esbuildModulePath: '/path/provided/by/prop', }); new Function(stack, 'MyFunction', { @@ -218,14 +219,14 @@ describe('with an esbuild module path from', () => { code, }); - expect(esbuildSpy).toHaveBeenCalledTimes(1); - expect(esbuildSpy).toHaveBeenCalledWith('../node_modules/esbuild'); + expect(providerSpy).toHaveBeenCalledTimes(1); + expect(providerSpy).toHaveBeenCalledWith('/path/provided/by/prop'); }); }); describe('`CDK_ESBUILD_MODULE_PATH` env var', () => { beforeEach(() => { - process.env.CDK_ESBUILD_MODULE_PATH = '../node_modules/esbuild'; + process.env.CDK_ESBUILD_MODULE_PATH = '/path/provided/by/env/var'; }); afterEach(() => { delete process.env.CDK_ESBUILD_MODULE_PATH; @@ -242,15 +243,15 @@ describe('with an esbuild module path from', () => { code, }); - expect(esbuildSpy).toHaveBeenCalledTimes(1); - expect(esbuildSpy).toHaveBeenCalledWith('../node_modules/esbuild'); + expect(providerSpy).toHaveBeenCalledTimes(1); + expect(providerSpy).toHaveBeenCalledWith('/path/provided/by/env/var'); }); 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', + esbuildModulePath: '/path/provided/by/prop', }); new Function(stack, 'MyFunction', { @@ -259,8 +260,8 @@ describe('with an esbuild module path from', () => { code, }); - expect(esbuildSpy).toHaveBeenCalledTimes(1); - expect(esbuildSpy).toHaveBeenCalledWith('../test/../node_modules/esbuild'); + expect(providerSpy).toHaveBeenCalledTimes(1); + expect(providerSpy).toHaveBeenCalledWith('/path/provided/by/prop'); }); }); }); diff --git a/test/inline-code.test.ts b/test/inline-code.test.ts index f68a43bc..d35bde5e 100644 --- a/test/inline-code.test.ts +++ b/test/inline-code.test.ts @@ -1,5 +1,6 @@ import { App, Stack } from 'aws-cdk-lib'; import { Function, Runtime } from 'aws-cdk-lib/aws-lambda'; +import * as esbuild from 'esbuild'; import { mocked } from 'jest-mock'; import { InlineJavaScriptCode, @@ -7,10 +8,9 @@ import { InlineTsxCode, InlineTypeScriptCode, } from '../src'; -import * as provider from '../src/esbuild-wrapper'; +import { EsbuildProvider } from '../src/esbuild-provider'; -const esbuildSpy = jest.spyOn(provider, 'esbuild'); -const transformSync = provider.esbuild().transformSync; +const providerSpy = jest.spyOn(EsbuildProvider, '_require'); describe('using transformOptions', () => { describe('given a banner code', () => { @@ -85,7 +85,7 @@ describe('using transformerProps', () => { it('should not do the work twice', () => { const processStdErrWriteSpy = jest.spyOn(process.stderr, 'write'); - const transformFn = jest.fn(transformSync); + const transformFn = jest.fn(esbuild.transformSync); const stack = new Stack(new App(), 'Stack'); const code = new InlineTypeScriptCode('let x: number = 1', { @@ -214,10 +214,11 @@ describe('using transformerProps', () => { describe('with an esbuild module path from', () => { beforeEach(() => { - esbuildSpy.mockClear(); + providerSpy.mockClear(); + providerSpy.mockReturnValue(esbuild); }); afterAll(() => { - esbuildSpy.mockRestore(); + providerSpy.mockRestore(); }); describe('the default', () => { @@ -225,26 +226,26 @@ describe('using transformerProps', () => { const code = new InlineTypeScriptCode('let x: number = 1'); code.bind(new Stack()); - expect(esbuildSpy).toHaveBeenCalledTimes(1); - expect(esbuildSpy).toHaveBeenCalledWith('esbuild'); + expect(providerSpy).toHaveBeenCalledTimes(1); + expect(providerSpy).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', + esbuildModulePath: '/expected/path/from/prop', }); code.bind(new Stack()); - expect(esbuildSpy).toHaveBeenCalledTimes(1); - expect(esbuildSpy).toHaveBeenCalledWith('../node_modules/esbuild'); + expect(providerSpy).toHaveBeenCalledTimes(1); + expect(providerSpy).toHaveBeenCalledWith('/expected/path/from/prop'); }); }); describe('`CDK_ESBUILD_MODULE_PATH` env var', () => { beforeEach(() => { - process.env.CDK_ESBUILD_MODULE_PATH = '../node_modules/esbuild'; + process.env.CDK_ESBUILD_MODULE_PATH = '/expected/path/from/env/var'; }); afterEach(() => { delete process.env.CDK_ESBUILD_MODULE_PATH; @@ -254,19 +255,19 @@ describe('using transformerProps', () => { const code = new InlineTypeScriptCode('let x: number = 1'); code.bind(new Stack()); - expect(esbuildSpy).toHaveBeenCalledTimes(1); - expect(esbuildSpy).toHaveBeenCalledWith('../node_modules/esbuild'); + expect(providerSpy).toHaveBeenCalledTimes(1); + expect(providerSpy).toHaveBeenCalledWith('/expected/path/from/env/var'); }); describe('and `esbuildModulePath` prop', () => { it('should prefer the path from prop', () => { const code = new InlineTypeScriptCode('let x: number = 1', { - esbuildModulePath: '../test/../node_modules/esbuild', + esbuildModulePath: '/expected/path/from/prop', }); code.bind(new Stack()); - expect(esbuildSpy).toHaveBeenCalledTimes(1); - expect(esbuildSpy).toHaveBeenCalledWith('../test/../node_modules/esbuild'); + expect(providerSpy).toHaveBeenCalledTimes(1); + expect(providerSpy).toHaveBeenCalledWith('/expected/path/from/prop'); }); }); }); @@ -275,7 +276,7 @@ describe('using transformerProps', () => { describe('with logLevel', () => { describe('not provided', () => { it('should default to "warning"', () => { - const transformFn = jest.fn(transformSync); + const transformFn = jest.fn(esbuild.transformSync); const code = new InlineJavaScriptCode("const fruit = 'banana';", { transformFn, }); @@ -289,7 +290,7 @@ describe('using transformerProps', () => { describe('provided', () => { it('should use the provided logLevel', () => { - const transformFn = jest.fn(transformSync); + const transformFn = jest.fn(esbuild.transformSync); const code = new InlineJavaScriptCode("const fruit = 'banana';", { transformFn, transformOptions: { @@ -323,7 +324,7 @@ describe('using transformerProps', () => { }); it(`should set the color option to "${derivedColor}"`, () => { - const transformFn = jest.fn(transformSync); + const transformFn = jest.fn(esbuild.transformSync); const code = new InlineTypeScriptCode('let x: number = 1', { transformFn, @@ -339,7 +340,7 @@ describe('using transformerProps', () => { }); it('should respect an explicit option', () => { - const transformFn = jest.fn(transformSync); + const transformFn = jest.fn(esbuild.transformSync); const code = new InlineTypeScriptCode('let x: number = 1', { transformFn, diff --git a/test/source.test.ts b/test/source.test.ts index 751e29f8..4cfa26c5 100644 --- a/test/source.test.ts +++ b/test/source.test.ts @@ -3,10 +3,10 @@ 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 { esbuild } from '../src/esbuild-wrapper'; +import { EsbuildProvider } from '../src/esbuild-provider'; import { JavaScriptSource, TypeScriptSource } from '../src/source'; -const buildSync = esbuild().buildSync; +const buildSync = EsbuildProvider.require().buildSync; describe('source', () => { describe('entrypoint is an absolute path', () => {