Skip to content

feat: provide custom builder with esbuild plugins#2175

Open
spliffone wants to merge 1 commit into
mainfrom
feat/custom-builder
Open

feat: provide custom builder with esbuild plugins#2175
spliffone wants to merge 1 commit into
mainfrom
feat/custom-builder

Conversation

@spliffone

Copy link
Copy Markdown
Member

No description provided.

@spliffone spliffone requested review from a team as code owners June 17, 2026 18:59

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces custom Angular builders (application and dev-server) to the live-preview project to support custom esbuild plugins, updating angular.json and build scripts accordingly. Feedback highlights several critical issues: a typo in the builder replacement mapping that breaks ng serve, an application builder implementation that breaks watch mode and discards metadata, dead code and potential runtime failures in the dynamic plugin loading logic, and the use of a non-cross-platform cp command in package.json.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +8 to +10
const BUILDER_REPLACEMENTS: Record<string, string> = {
'@siemens/angular-builder:application': '@angular/build:application'
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

There is a typo in the BUILDER_REPLACEMENTS mapping. The key is set to '@siemens/angular-builder:application', but the custom builder package name defined in projects/live-preview/package.json is @siemens/live-preview, making the builder name @siemens/live-preview:application.

Because of this mismatch, executeDevServerBuilder will not recognize the custom builder as a supported application builder and will throw an error, completely breaking ng serve.

Suggested change
const BUILDER_REPLACEMENTS: Record<string, string> = {
'@siemens/angular-builder:application': '@angular/build:application'
};
const BUILDER_REPLACEMENTS: Record<string, string> = {
'@siemens/live-preview:application': '@angular/build:application'
};

Comment on lines +14 to +42
export default createBuilder(
async (options: ApplicationOptions, context: BuilderContext): Promise<BuilderOutput> => {
const plugins = options.plugins ? await loadPlugins(options.plugins, context) : [];

const buildOptions = { ...options };
delete buildOptions.plugins;

try {
for await (const output of buildApplication(
buildOptions as ApplicationBuilderOptions,
context,
{ codePlugins: plugins }
)) {
if (!output.success) {
return output;
}
}

return { success: true };
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
context.logger.error(`Build failed: ${errorMessage}`);
return {
success: false,
error: errorMessage
};
}
}
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of the application builder returns a Promise<BuilderOutput> and uses a for await...of loop to wait for the generator returned by buildApplication to complete.

This has two major issues:

  1. Breaks Watch Mode: In watch mode (--watch), buildApplication is an infinite async generator that yields new outputs on every file change. Because the builder returns a single Promise, it will never resolve (or will only resolve on the first failure), causing the build process to hang or fail to report subsequent successful builds.
  2. Discards Metadata: Returning { success: true } discards important metadata returned by buildApplication (such as outputPaths, configuration, etc.) which might be used by other builders or integration tools.

Recommendation:
Change the builder handler to an async generator (async function*) and use yield* to forward all outputs directly to the architect. This fully supports watch mode and preserves all builder output metadata.

Additionally, index.ts files should primarily serve as re-export barrels. Since this file contains builder execution logic, consider renaming it to a more descriptive name (e.g., builder.ts or main.ts) to clarify its purpose and avoid using index.ts for executing code.

export default createBuilder<ApplicationOptions>(
  async function* (options, context) {
    const plugins = options.plugins ? await loadPlugins(options.plugins, context) : [];

    const buildOptions = { ...options };
    delete buildOptions.plugins;

    try {
      yield* buildApplication(
        buildOptions as ApplicationBuilderOptions,
        context,
        { codePlugins: plugins }
      );
    } catch (error) {
      const errorMessage = error instanceof Error ? error.message : String(error);
      context.logger.error(`Build failed: ${errorMessage}`);
      yield {
        success: false,
        error: errorMessage
      };
    }
  }
);
References
  1. Do not introduce side-effect imports in index.ts files. index.ts files should primarily serve as re-export barrels. If an index.ts file needs to execute code, consider renaming it to a more descriptive name (e.g., main.ts or extract.ts) to clarify its purpose and avoid bypassing the intent of pure re-export barrels.

Comment on lines +24 to +32
let plugin: Plugin | Plugin[] | ((...args: unknown[]) => Plugin | Plugin[]);

if (module.default) {
plugin = module.default;
} else if (typeof module === 'function') {
plugin = module;
} else {
plugin = module;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The dynamic import(moduleUrl) statement always returns a Module Namespace Object, which is of type 'object'. Therefore, the check typeof module === 'function' is dead code and will never be true.

Furthermore, if module.default is not defined, the code falls back to setting plugin = module (the namespace object itself). Pushing a Module Namespace Object directly to the plugins array will cause esbuild to fail because it is not a valid plugin object (it lacks the required name and setup properties).

Since both ES modules (via export default) and CommonJS modules (via module.exports = ...) resolve their main export to module.default when dynamically imported in Node.js, we should strictly require and use module.default.

      const plugin = module.default;
      if (!plugin) {
        throw new Error(`Module ${pluginPath} does not have a default export.`);
      }

Comment thread package.json Outdated

@michael-smt michael-smt left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might be out of the loop, but what's the reason for this? 🙂

Comment on lines +7 to +14
"plugins": {
"type": "array",
"description": "List of paths to esbuild plugins. Each path should resolve to a module that exports an esbuild Plugin or Plugin array.",
"items": {
"type": "string"
},
"default": []
},

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand this is a copy of the Angular builder's schema extended with this section. Generating this file might be better, so we don't have to remember to keep this file up-to-date with Angular changes.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here as with the application schema, I think this would be better if copied at builder's build time.

@spliffone spliffone force-pushed the feat/custom-builder branch from b735a4d to 772d849 Compare June 18, 2026 07:59
@spliffone spliffone force-pushed the feat/custom-builder branch from 5e128a0 to a8230a8 Compare June 18, 2026 08:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants