Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(custom-esbuild): expose current builder options and target to plugins #1878

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
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
27 changes: 23 additions & 4 deletions 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 All @@ -143,13 +143,13 @@ import type { Plugin, PluginBuild } from 'esbuild';

function defineEnv(pluginOptions: { stage: string }): Plugin {
return {
name: 'define-env',
name: 'define-env',
setup(build: PluginBuild) {
const buildOptions = build.initialOptions;
buildOptions.define.stage = JSON.stringify(pluginOptions.stage);
},
};
};
}

export default defineEnv;
```
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 Expand Up @@ -239,7 +258,7 @@ It is useful when you want to transform your `index.html` according to the build
`index-html-transformer.js`:

```js
module.exports = (indexHtml) => {
module.exports = indexHtml => {
const i = indexHtml.indexOf('</body>');
const content = `<p>Dynamically inserted content</p>`;
return `${indexHtml.slice(0, i)}
Expand Down
9 changes: 8 additions & 1 deletion packages/custom-esbuild/src/application/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,14 @@ 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
4 changes: 3 additions & 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,9 @@ export function executeCustomDevServerBuilder(
buildOptions.plugins,
workspaceRoot,
tsConfig,
context.logger
context.logger,
options,
context.target
);

const indexHtmlTransformer: IndexHtmlTransform = buildOptions.indexHtmlTransformer
Expand Down
49 changes: 43 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,54 @@ 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();
});
});
37 changes: 29 additions & 8 deletions packages/custom-esbuild/src/load-plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,46 @@ 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);
if (typeof pluginConfig === 'string') {
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 {
const pluginFactory = await loadModule<(...args: any[]) => Plugin>(path.join(workspaceRoot, pluginConfig.path), tsConfig, logger);
return pluginFactory(pluginConfig.options);
return pluginsOrFactory;
}

},
),
} else {
const pluginFactory = await loadModule<(...args: any[]) => Plugin>(
path.join(workspaceRoot, pluginConfig.path),
tsConfig,
logger
);
return pluginFactory(pluginConfig.options, builderOptions, target);
}
})
);

return plugins.flat();
Expand Down