Skip to content

Commit

Permalink
feat(custom-esbuild): expose current builder options and target to pl…
Browse files Browse the repository at this point in the history
…ugins
  • Loading branch information
mattlewis92 committed Nov 26, 2024
1 parent 21e3b6e commit b5080b3
Show file tree
Hide file tree
Showing 5 changed files with 55 additions and 12 deletions.
21 changes: 20 additions & 1 deletion packages/custom-esbuild/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ Builder options:
In the above example, we specify the list of `plugins` that should implement the ESBuild plugin schema. These plugins are custom user plugins and are added to the original ESBuild Angular configuration. Additionally, the `indexHtmlTransformer` property is used to specify the path to the file that exports the function used to modify the `index.html`.
The plugin file can export either a single plugin or a list of plugins. If a plugin accepts configuration then the config should be provided in `angular.json`:
The plugin file can export either a single plugin, a list of plugins or a factory function that returns a plugin or list of plugins. If a plugin accepts configuration then the config should be provided in `angular.json`:
```ts
// esbuild/plugins.ts
Expand Down Expand Up @@ -180,6 +180,25 @@ const updateExternalPlugin: Plugin = {
export default [defineTextPlugin, updateExternalPlugin];
```
Or:
```ts
// esbuild/plugins.ts
import type { Plugin, PluginBuild } from 'esbuild';
import type { ApplicationBuilderOptions } from '@angular-devkit/build-angular';
import type { Target } from '@angular-devkit/architect';

export default (builderOptions: ApplicationBuilderOptions, target: Target): Plugin => {
return {
name: 'define-text',
setup(build: PluginBuild) {
const options = build.initialOptions;
options.define.currentProject = JSON.stringify(target.project);
},
}
};
```
## Custom ESBuild `dev-server`
The `@angular-builders/custom-esbuild:dev-server` is an enhanced version of the `@angular-devkit/build-angular:dev-server` builder that allows the specification of `middlewares` (Vite's `Connect` functions). It also obtains `plugins` and `indexHtmlTransformer` from the `:application` configuration to run the Vite server with all the necessary configuration applied.
Expand Down
2 changes: 1 addition & 1 deletion packages/custom-esbuild/src/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export function buildCustomEsbuildApplication(
const tsConfig = path.join(workspaceRoot, options.tsConfig);

return defer(async () => {
const codePlugins = await loadPlugins(options.plugins, workspaceRoot, tsConfig, context.logger);
const codePlugins = await loadPlugins(options.plugins, workspaceRoot, tsConfig, context.logger, options, context.target);

const indexHtmlTransformer = options.indexHtmlTransformer
? await loadModule(
Expand Down
2 changes: 1 addition & 1 deletion packages/custom-esbuild/src/dev-server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function executeCustomDevServerBuilder(
buildOptions.plugins,
workspaceRoot,
tsConfig,
context.logger
context.logger, options, context.target
);

const indexHtmlTransformer: IndexHtmlTransform = buildOptions.indexHtmlTransformer
Expand Down
28 changes: 22 additions & 6 deletions packages/custom-esbuild/src/load-plugin.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { loadPlugins } from './load-plugins';
import { Target } from '@angular-devkit/architect';
import {Plugin} from 'esbuild'
import { CustomEsbuildApplicationSchema } from './custom-esbuild-schema';

describe('loadPlugin', () => {
beforeEach(() => {
Expand All @@ -7,20 +10,33 @@ describe('loadPlugin', () => {
});

it('should load a plugin without configuration', async () => {
const pluginFactory = jest.fn();
const mockPlugin = {name: 'mock'} as Plugin;
jest.mock('test/test-plugin.js', () => mockPlugin, { virtual: true });
const plugin = await loadPlugins(['test-plugin.js'], './test', './tsconfig.json', null as any, {} as any, {} as any);

expect(plugin).toEqual([mockPlugin]);
});

it('should load a plugin factory without configuration and pass options and target', async () => {
const mockPlugin = {name: 'mock'} as Plugin;
const pluginFactory = jest.fn().mockReturnValue(mockPlugin);
const mockOptions = {tsConfig: './tsconfig.json'} as CustomEsbuildApplicationSchema;
const mockTarget = { target: 'test' } as Target;
jest.mock('test/test-plugin.js', () => pluginFactory, { virtual: true });
const plugin = await loadPlugins(['test-plugin.js'], './test', './tsconfig.json', null as any);
const plugin = await loadPlugins(['test-plugin.js'], './test', './tsconfig.json', null as any, mockOptions, mockTarget);

expect(pluginFactory).not.toHaveBeenCalled();
expect(plugin).toBeDefined();
expect(pluginFactory).toHaveBeenCalledWith(mockOptions, mockTarget);
expect(plugin).toEqual([mockPlugin]);
});

it('should load a plugin with configuration', async () => {
const pluginFactory = jest.fn();
const mockOptions = {tsConfig: './tsconfig.json'} as CustomEsbuildApplicationSchema;
const mockTarget = { target: 'test' } as Target;
jest.mock('test/test-plugin.js', () => pluginFactory, { virtual: true });
const plugin = await loadPlugins([{ path: 'test-plugin.js', options: { test: 'test' } }], './test', './tsconfig.json', null as any);
const plugin = await loadPlugins([{ path: 'test-plugin.js', options: { test: 'test' } }], './test', './tsconfig.json', null as any, mockOptions, mockTarget);

expect(pluginFactory).toHaveBeenCalledWith({ test: 'test' });
expect(pluginFactory).toHaveBeenCalledWith({ test: 'test' }, mockOptions, mockTarget);
expect(plugin).toBeDefined();
});
});
14 changes: 11 additions & 3 deletions packages/custom-esbuild/src/load-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,29 @@ import * as path from 'node:path';
import type { Plugin } from 'esbuild';
import type { logging } from '@angular-devkit/core';
import { loadModule } from '@angular-builders/common';
import { PluginConfig } from './custom-esbuild-schema';
import { CustomEsbuildApplicationSchema, CustomEsbuildDevServerSchema, PluginConfig } from './custom-esbuild-schema';
import { Target } from '@angular-devkit/architect';

export async function loadPlugins(
pluginConfig: PluginConfig[] | undefined,
workspaceRoot: string,
tsConfig: string,
logger: logging.LoggerApi,
builderOptions: CustomEsbuildApplicationSchema | CustomEsbuildDevServerSchema,
target: Target,
): Promise<Plugin[]> {
const plugins = await Promise.all(
(pluginConfig || []).map(async pluginConfig => {
if (typeof pluginConfig === 'string') {
return loadModule<Plugin | Plugin[]>(path.join(workspaceRoot, pluginConfig), tsConfig, logger);
const pluginsOrFactory = await loadModule<Plugin | Plugin[] | ((options: CustomEsbuildApplicationSchema | CustomEsbuildDevServerSchema, target: Target) => Plugin | Plugin[])>(path.join(workspaceRoot, pluginConfig), tsConfig, logger);
if (typeof pluginsOrFactory === 'function') {
return pluginsOrFactory(builderOptions, target);
} else{
return pluginsOrFactory;
}
} else {
const pluginFactory = await loadModule<(...args: any[]) => Plugin>(path.join(workspaceRoot, pluginConfig.path), tsConfig, logger);
return pluginFactory(pluginConfig.options);
return pluginFactory(pluginConfig.options, builderOptions, target);
}

},
Expand Down

0 comments on commit b5080b3

Please sign in to comment.