Skip to content

Commit

Permalink
feat(custom-esbuild): add support for plugin configuration
Browse files Browse the repository at this point in the history
  • Loading branch information
spike-rabbit committed Mar 4, 2024
1 parent ca8c2ed commit cefcaf9
Show file tree
Hide file tree
Showing 17 changed files with 211 additions and 16 deletions.
40 changes: 36 additions & 4 deletions examples/custom-esbuild/sanity-esbuild-app-esm/angular.json
Expand Up @@ -17,7 +17,15 @@
"build": {
"builder": "@angular-builders/custom-esbuild:application",
"options": {
"plugins": ["esbuild/define-text-plugin.js"],
"plugins": [
"esbuild/define-text-plugin.js",
{
"path": "esbuild/define-text-by-option-plugin.js",
"options": {
"title": "sanity-esbuild-app-esm optionTitle (compilation provided)"
}
}
],
"outputPath": "dist/sanity-esbuild-app-esm",
"index": "src/index.html",
"browser": "src/main.ts",
Expand Down Expand Up @@ -45,13 +53,37 @@
"outputHashing": "all"
},
"esm": {
"plugins": ["esbuild/define-text-plugin.js"]
"plugins": [
"esbuild/define-text-plugin.js",
{
"path": "esbuild/define-text-by-option-plugin.js",
"options": {
"title": "sanity-esbuild-app-esm optionTitle (compilation provided)"
}
}
]
},
"cjs": {
"plugins": ["esbuild/define-text-plugin.cjs"]
"plugins": [
"esbuild/define-text-plugin.cjs",
{
"path": "esbuild/define-text-by-option-plugin.cjs",
"options": {
"title": "sanity-esbuild-app-esm optionTitle (compilation provided)"
}
}
]
},
"tsEsm": {
"plugins": ["esbuild/define-text-plugin.ts"]
"plugins": [
"esbuild/define-text-plugin.ts",
{
"path": "esbuild/define-text-by-option-plugin.ts",
"options": {
"title": "sanity-esbuild-app-esm optionTitle (compilation provided)"
}
}
]
}
},
"defaultConfiguration": "production"
Expand Down
@@ -0,0 +1,11 @@
function defineTitleByOptionPlugin(pluginOptions) {
return {
name: 'define-title',
setup(build) {
const options = build.initialOptions;
options.define.titleByOption = pluginOptions.title;
},
};
};

module.exports = defineTitleByOptionPlugin;
@@ -0,0 +1,11 @@
function defineTitleByOptionPlugin(pluginOptions) {
return {
name: 'define-title',
setup(build) {
const options = build.initialOptions;
options.define.titleByOption = pluginOptions.title;
},
};
};

export default defineTitleByOptionPlugin;
@@ -0,0 +1,13 @@
import type { Plugin, PluginBuild } from 'esbuild';

function defineTitleByOptionPlugin(pluginOptions: {title: string}): Plugin {
return {
name: 'define-title',
setup(build: PluginBuild) {
const options = build.initialOptions;
options.define!['titleByOption'] = pluginOptions.title;
},
};
};

export default defineTitleByOptionPlugin;
@@ -1,2 +1,3 @@
<h1>{{ title }}</h1>
<h2>{{ subtitle }}</h2>
<h3>{{ titleByOption }}</h3>
Expand Up @@ -2,6 +2,7 @@ import { Component } from '@angular/core';

declare const title: string;
declare const subtitle: string;
declare const titleByOption: string;

@Component({
selector: 'app-root',
Expand All @@ -12,9 +13,11 @@ declare const subtitle: string;
export class AppComponent {
title: string;
subtitle: string;
titleByOption: string;

constructor() {
this.title = typeof title !== 'undefined' ? title : 'sanity-esbuild-app-esm';
this.subtitle = typeof subtitle !== 'undefined' ? subtitle : 'sanity-esbuild-app-esm subtitle';
this.titleByOption = typeof titleByOption !== 'undefined' ? titleByOption : 'sanity-esbuild-app-esm optionTitle';
}
}
30 changes: 27 additions & 3 deletions examples/custom-esbuild/sanity-esbuild-app/angular.json
Expand Up @@ -17,7 +17,15 @@
"build": {
"builder": "@angular-builders/custom-esbuild:application",
"options": {
"plugins": ["esbuild/define-text-plugin.js"],
"plugins": [
"esbuild/define-text-plugin.js",
{
"path": "esbuild/define-text-by-option-plugin.js",
"options": {
"title": "sanity-esbuild-app optionTitle (compilation provided)"
}
}
],
"outputPath": "dist/sanity-esbuild-app",
"index": "src/index.html",
"browser": "src/main.ts",
Expand Down Expand Up @@ -45,10 +53,26 @@
"outputHashing": "all"
},
"esm": {
"plugins": ["esbuild/define-text-plugin.mjs"]
"plugins": [
"esbuild/define-text-plugin.mjs",
{
"path": "esbuild/define-text-by-option-plugin.mjs",
"options": {
"title": "sanity-esbuild-app optionTitle (compilation provided)"
}
}
]
},
"cjs": {
"plugins": ["esbuild/define-text-plugin.js"]
"plugins": [
"esbuild/define-text-plugin.js",
{
"path": "esbuild/define-text-by-option-plugin.js",
"options": {
"title": "sanity-esbuild-app optionTitle (compilation provided)"
}
}
]
}
},
"defaultConfiguration": "production"
Expand Down
@@ -0,0 +1,11 @@
function defineTitleByOptionPlugin(pluginOptions) {
return {
name: 'define-title',
setup(build) {
const options = build.initialOptions;
options.define.titleByOption = pluginOptions.title;
},
};
};

module.exports = defineTitleByOptionPlugin;
@@ -0,0 +1,11 @@
function defineTitleByOptionPlugin(pluginOptions) {
return {
name: 'define-title',
setup(build) {
const options = build.initialOptions;
options.define.titleByOption = pluginOptions.title;
},
};
};

export default defineTitleByOptionPlugin;
@@ -1,2 +1,3 @@
<h1>{{ title }}</h1>
<h2>{{ subtitle }}</h2>
<h3>{{ titleByOption }}</h3>
Expand Up @@ -2,6 +2,7 @@ import { Component } from '@angular/core';

declare const title: string;
declare const subtitle: string;
declare const titleByOption: string;

@Component({
selector: 'app-root',
Expand All @@ -12,9 +13,11 @@ declare const subtitle: string;
export class AppComponent {
title: string;
subtitle: string;
titleByOption: string;

constructor() {
this.title = typeof title !== 'undefined' ? title : 'sanity-esbuild-app';
this.subtitle = typeof subtitle !== 'undefined' ? subtitle : 'sanity-esbuild-app subtitle';
this.titleByOption = typeof titleByOption !== 'undefined' ? titleByOption : 'sanity-esbuild-app optionTitle';
}
}
23 changes: 21 additions & 2 deletions packages/custom-esbuild/README.md
Expand Up @@ -100,7 +100,7 @@ Builder options:
"build": {
"builder": "@angular-builders/custom-esbuild:application",
"options": {
"plugins": ["./esbuild/plugins.ts", "./esbuild/plugin-2.js"],
"plugins": ["./esbuild/plugins.ts", { "path": "./esbuild/plugin-2.js", "options": { "key": "value" } }],
"indexHtmlTransformer": "./esbuild/index-html-transformer.js",
"outputPath": "dist/my-cool-client",
"index": "src/index.html",
Expand All @@ -112,7 +112,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:
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`:
```ts
// esbuild/plugins.ts
Expand All @@ -129,6 +129,25 @@ const defineTextPlugin: Plugin = {
export default defineTextPlugin;
```
OR:
```ts
// esbuild/plugins.ts
import type { Plugin, PluginBuild } from 'esbuild';

function defineRewritePathPlugin(options: { text: string }): Plugin {
return {
name: 'define-text',
setup(build: PluginBuild) {
const options = build.initialOptions;
options.define.buildText = JSON.stringify(options.text);
},
};
};

export default defineTextPlugin;
```
Or:
```ts
Expand Down
3 changes: 2 additions & 1 deletion packages/custom-esbuild/package.json
Expand Up @@ -32,7 +32,8 @@
"scripts": {
"prebuild": "yarn clean",
"build": "yarn prebuild && tsc && ts-node ../../merge-schemes.ts && yarn postbuild",
"postbuild": "yarn run e2e",
"postbuild": "yarn test && yarn run e2e",
"test": "jest --config ../../jest-ut.config.js",
"e2e": "jest --config ../../jest-e2e.config.js",
"clean": "rimraf dist",
"ci": "./scripts/ci.sh"
Expand Down
20 changes: 19 additions & 1 deletion packages/custom-esbuild/src/application/schema.ext.json
Expand Up @@ -8,7 +8,25 @@
"description": "A list of paths to ESBuild plugins",
"default": [],
"items": {
"type": "string",
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"path": {
"type": "string"
},
"options": {
"type": "object"
}
},
"required": [
"path"
]
}
],
"uniqueItems": true
}
},
Expand Down
2 changes: 2 additions & 0 deletions packages/custom-esbuild/src/custom-esbuild-schema.ts
@@ -1,5 +1,7 @@
import { ApplicationBuilderOptions, DevServerBuilderOptions } from '@angular-devkit/build-angular';

export type PluginConfig = string | { path: string; options?: Record<string, unknown> };

export type CustomEsbuildApplicationSchema = ApplicationBuilderOptions & {
plugins?: string[];
indexHtmlTransformer?: string;
Expand Down
26 changes: 26 additions & 0 deletions packages/custom-esbuild/src/load-plugin.spec.ts
@@ -0,0 +1,26 @@
import { loadPlugins } from './load-plugins';

describe('loadPlugin', () => {
beforeEach(() => {
jest.resetModules();
jest.clearAllMocks();
});

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

expect(pluginFactory).not.toHaveBeenCalled();
expect(plugin).toBeDefined();
});

it('should load a plugin with configuration', async () => {
const pluginFactory = jest.fn();
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);

expect(pluginFactory).toHaveBeenCalledWith({ test: 'test' });
expect(plugin).toBeDefined();
});
});
18 changes: 13 additions & 5 deletions packages/custom-esbuild/src/load-plugins.ts
Expand Up @@ -2,17 +2,25 @@ 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';

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

},
),
);

return plugins.flat();
Expand Down

0 comments on commit cefcaf9

Please sign in to comment.