Skip to content

Commit

Permalink
feat: add support for .mjs file output
Browse files Browse the repository at this point in the history
  • Loading branch information
codingnuclei committed Jul 18, 2022
1 parent 2c1d2ac commit b799b34
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ See [example folder](examples) for some example configurations.
| `nativeZip` | Uses the system's `zip` executable to create archives. _NOTE_: This will produce non-deterministic archives which causes a Serverless deployment update on every deploy. | `false` |
| `outputBuildFolder` | The output folder for Esbuild builds within the work folder. | `'.build'` |
| `outputWorkFolder` | The output folder for this plugin where all the bundle preparation is done. | `'.esbuild'` |
| `outputFileExtension` | The file extension used for the bundled output file. This will override the esbuild `outExtension` option | `'.js'` |
| `packagePath` | Path to the `package.json` file for `external` dependency resolution. | `'./package.json'` |
| `packager` | Packager to use for `external` dependency resolution. Values: `npm`, `yarn`, `pnpm` | `'npm'` |
| `packagerOptions` | Extra options for packagers for `external` dependency resolution. | [Packager Options](#packager-options) |
Expand Down
27 changes: 26 additions & 1 deletion src/bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,29 @@ export async function bundle(this: EsbuildServerlessPlugin, incremental = false)
plugins: this.plugins,
};

if (
this.buildOptions.platform === 'neutral' &&
this.buildOptions.outputFileExtension === '.cjs'
) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Serverless typings (as of v3.0.2) are incorrect
throw new this.serverless.classes.Error(
'ERROR: platform "neutral" should not output a file with extension ".cjs".'
);
}

if (this.buildOptions.platform === 'node' && this.buildOptions.outputFileExtension === '.mjs') {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore Serverless typings (as of v3.0.2) are incorrect
throw new this.serverless.classes.Error(
'ERROR: platform "node" should not output a file with extension ".mjs".'
);
}

if (this.buildOptions.outputFileExtension !== '.js') {
config.outExtension = { '.js': this.buildOptions.outputFileExtension };
}

// esbuild v0.7.0 introduced config options validation, so I have to delete plugin specific options from esbuild config.
delete config['concurrency'];
delete config['exclude'];
Expand All @@ -38,10 +61,12 @@ export async function bundle(this: EsbuildServerlessPlugin, incremental = false)
delete config['packagerOptions'];
delete config['installExtraArgs'];
delete config['disableIncremental'];
delete config['outputFileExtension'];

/** Build the files */
const bundleMapper = async (entry: string): Promise<FileBuildResult> => {
const bundlePath = entry.slice(0, entry.lastIndexOf('.')) + '.js';
const bundlePath =
entry.slice(0, entry.lastIndexOf('.')) + this.buildOptions.outputFileExtension;

// check cache
if (this.buildCache) {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ class EsbuildServerlessPlugin implements ServerlessPlugin {
keepOutputDirectory: false,
packagerOptions: {},
platform: 'node',
outputFileExtension: '.js',
};

const runtimeMatcher = providerRuntimeMatcher[this.serverless.service.provider.name];
Expand Down
313 changes: 312 additions & 1 deletion src/tests/bundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { PartialDeep } from 'type-fest';
import EsbuildServerlessPlugin from '..';
import { bundle } from '../bundle';
import { build } from 'esbuild';
import { FunctionBuildResult, FunctionEntry } from '../types';
import { Configuration, FunctionBuildResult, FunctionEntry } from '../types';
import pMap from 'p-map';
import { mocked } from 'ts-jest/utils';

Expand All @@ -16,6 +16,9 @@ const esbuildPlugin = (override?: Partial<EsbuildServerlessPlugin>): EsbuildServ
cli: {
log: jest.fn(),
},
classes: {
Error: Error,
},
},
buildOptions: {
concurrency: Infinity,
Expand All @@ -30,6 +33,7 @@ const esbuildPlugin = (override?: Partial<EsbuildServerlessPlugin>): EsbuildServ
keepOutputDirectory: false,
packagerOptions: {},
platform: 'node',
outputFileExtension: '.js',
},
plugins: [],
buildDirPath: '/workdir/.esbuild',
Expand Down Expand Up @@ -194,3 +198,310 @@ it('should filter out non esbuild options', async () => {
target: 'node12',
});
});

describe('buildOption platform node', () => {
it('should set buildResults buildPath after compilation is complete with default extension', async () => {
const functionEntries: FunctionEntry[] = [
{
entry: 'file1.ts',
func: {
events: [],
handler: 'file1.handler',
},
functionAlias: 'func1',
},
{
entry: 'file2.ts',
func: {
events: [],
handler: 'file2.handler',
},
functionAlias: 'func2',
},
];

const expectedResults: FunctionBuildResult[] = [
{
bundlePath: 'file1.js',
func: { events: [], handler: 'file1.handler' },
functionAlias: 'func1',
},
{
bundlePath: 'file2.js',
func: { events: [], handler: 'file2.handler' },
functionAlias: 'func2',
},
];

const plugin = esbuildPlugin({ functionEntries });

await bundle.call(plugin);

expect(plugin.buildResults).toStrictEqual(expectedResults);
});

it('should set buildResults buildPath after compilation is complete with ".cjs" extension', async () => {
const functionEntries: FunctionEntry[] = [
{
entry: 'file1.ts',
func: {
events: [],
handler: 'file1.handler',
},
functionAlias: 'func1',
},
{
entry: 'file2.ts',
func: {
events: [],
handler: 'file2.handler',
},
functionAlias: 'func2',
},
];

const buildOptions: Partial<Configuration> = {
concurrency: Infinity,
bundle: true,
target: 'node12',
external: [],
exclude: ['aws-sdk'],
nativeZip: false,
packager: 'npm',
installExtraArgs: [],
watch: {},
keepOutputDirectory: false,
packagerOptions: {},
platform: 'node',
outputFileExtension: '.cjs',
};

const expectedResults: FunctionBuildResult[] = [
{
bundlePath: 'file1.cjs',
func: { events: [], handler: 'file1.handler' },
functionAlias: 'func1',
},
{
bundlePath: 'file2.cjs',
func: { events: [], handler: 'file2.handler' },
functionAlias: 'func2',
},
];

const plugin = esbuildPlugin({ functionEntries, buildOptions: buildOptions as any });

await bundle.call(plugin);

expect(plugin.buildResults).toStrictEqual(expectedResults);
});

it('should error when trying to use ".mjs" extension', async () => {
const functionEntries: FunctionEntry[] = [
{
entry: 'file1.ts',
func: {
events: [],
handler: 'file1.handler',
},
functionAlias: 'func1',
},
{
entry: 'file2.ts',
func: {
events: [],
handler: 'file2.handler',
},
functionAlias: 'func2',
},
];

const buildOptions: Partial<Configuration> = {
concurrency: Infinity,
bundle: true,
target: 'node12',
external: [],
exclude: ['aws-sdk'],
nativeZip: false,
packager: 'npm',
installExtraArgs: [],
watch: {},
keepOutputDirectory: false,
packagerOptions: {},
platform: 'node',
outputFileExtension: '.mjs',
};

const plugin = esbuildPlugin({ functionEntries, buildOptions: buildOptions as any });

const expectedError = 'ERROR: platform "node" should not output a file with extension ".mjs".';

try {
await bundle.call(plugin);
} catch (error) {
expect(error).toHaveProperty('message', expectedError);
}
});
});

describe('buildOption platform neutral', () => {
it('should set buildResults buildPath after compilation is complete with default extension', async () => {
const functionEntries: FunctionEntry[] = [
{
entry: 'file1.ts',
func: {
events: [],
handler: 'file1.handler',
},
functionAlias: 'func1',
},
{
entry: 'file2.ts',
func: {
events: [],
handler: 'file2.handler',
},
functionAlias: 'func2',
},
];

const buildOptions: Partial<Configuration> = {
concurrency: Infinity,
bundle: true,
target: 'node12',
external: [],
exclude: ['aws-sdk'],
nativeZip: false,
packager: 'npm',
installExtraArgs: [],
watch: {},
keepOutputDirectory: false,
packagerOptions: {},
platform: 'neutral',
outputFileExtension: '.js',
};

const expectedResults: FunctionBuildResult[] = [
{
bundlePath: 'file1.js',
func: { events: [], handler: 'file1.handler' },
functionAlias: 'func1',
},
{
bundlePath: 'file2.js',
func: { events: [], handler: 'file2.handler' },
functionAlias: 'func2',
},
];

const plugin = esbuildPlugin({ functionEntries, buildOptions: buildOptions as any });

await bundle.call(plugin);

expect(plugin.buildResults).toStrictEqual(expectedResults);
});

it('should set buildResults buildPath after compilation is complete with ".mjs" extension', async () => {
const functionEntries: FunctionEntry[] = [
{
entry: 'file1.ts',
func: {
events: [],
handler: 'file1.handler',
},
functionAlias: 'func1',
},
{
entry: 'file2.ts',
func: {
events: [],
handler: 'file2.handler',
},
functionAlias: 'func2',
},
];

const buildOptions: Partial<Configuration> = {
concurrency: Infinity,
bundle: true,
target: 'node12',
external: [],
exclude: ['aws-sdk'],
nativeZip: false,
packager: 'npm',
installExtraArgs: [],
watch: {},
keepOutputDirectory: false,
packagerOptions: {},
platform: 'neutral',
outputFileExtension: '.mjs',
};

const expectedResults: FunctionBuildResult[] = [
{
bundlePath: 'file1.mjs',
func: { events: [], handler: 'file1.handler' },
functionAlias: 'func1',
},
{
bundlePath: 'file2.mjs',
func: { events: [], handler: 'file2.handler' },
functionAlias: 'func2',
},
];

const plugin = esbuildPlugin({ functionEntries, buildOptions: buildOptions as any });

await bundle.call(plugin);

expect(plugin.buildResults).toStrictEqual(expectedResults);
});

it('should error when trying to use ".cjs" extension', async () => {
const functionEntries: FunctionEntry[] = [
{
entry: 'file1.ts',
func: {
events: [],
handler: 'file1.handler',
},
functionAlias: 'func1',
},
{
entry: 'file2.ts',
func: {
events: [],
handler: 'file2.handler',
},
functionAlias: 'func2',
},
];

const buildOptions: Partial<Configuration> = {
concurrency: Infinity,
bundle: true,
target: 'node12',
external: [],
exclude: ['aws-sdk'],
nativeZip: false,
packager: 'npm',
installExtraArgs: [],
watch: {},
keepOutputDirectory: false,
packagerOptions: {},
platform: 'neutral',
outputFileExtension: '.cjs',
};

const plugin = esbuildPlugin({ functionEntries, buildOptions: buildOptions as any });

const expectedError =
'ERROR: platform "neutral" should not output a file with extension ".cjs".';

try {
await bundle.call(plugin);
} catch (error) {
expect(error).toHaveProperty('message', expectedError);
}
});
});
Loading

0 comments on commit b799b34

Please sign in to comment.