Skip to content

[APPS] Use separate vite.build() for backend functions, drop Rollup backend support#292

Open
sdkennedy2 wants to merge 6 commits intomasterfrom
sdkennedy2/apps-vite-backend-build
Open

[APPS] Use separate vite.build() for backend functions, drop Rollup backend support#292
sdkennedy2 wants to merge 6 commits intomasterfrom
sdkennedy2/apps-vite-backend-build

Conversation

@sdkennedy2
Copy link
Collaborator

@sdkennedy2 sdkennedy2 commented Mar 20, 2026

Motivation

Backend functions were previously built by injecting virtual modules into the host bundler's build via resolveId/load hooks, with separate Rollup and Vite plugin implementations that duplicated logic. This approach was fragile — it coupled backend function bundling to the host build's configuration (plugins, aliases, output settings) and required maintaining two codepaths (Rollup + Vite).

This PR switches to a standalone vite.build() call that runs after the host build completes, giving full control over the backend build configuration. It also drops Rollup backend support entirely, simplifying to vite-only.

Changes

Replaced the "inject into host build" approach with a dedicated vite.build() for backend functions:

// In closeBundle hook (runs after host build finishes):
const result = await vite.build({
    configFile: false,       // Don't inherit user's vite plugins
    build: {
        write: true,
        outDir: tempDir,     // Isolated output directory
        rollupOptions: {
            input: virtualEntries,
            output: { format: 'es', entryFileNames: '[name].js' },
            treeshake: false,  // Preserve action-catalog bridges intact
        },
    },
    plugins: [/* virtual module resolver */],
});

Why a separate build? Backend functions need different build settings than the frontend — no minification, ES module format, preserved entry signatures, and no interference from the user's vite plugins. Running a standalone vite.build() with configFile: false provides this isolation cleanly.

Key changes:

  • Deleted backend/rollup.ts — Rollup backend support removed
  • Simplified backend/index.ts — removed resolveId/load hooks, now just delegates to the vite plugin
  • New buildBackendFunctions() in backend/vite/index.ts — runs a separate vite.build() via closeBundle hook, writes output to a temp directory, populates backendOutputs map for the upload plugin
  • Refactored virtual-entry.ts — extracted generateMainBody() helper for reuse
  • Updated shared.tsisActionCatalogInstalled() now accepts a fromDir parameter for correct resolution when the plugin is linked, fixed regex escaping in action-catalog bridge
  • Restricted backendSupportedBundlers from ['rollup', 'vite'] to ['vite']

QA Instructions

  1. Scaffold or use an existing high code app with backend functions
  2. Link the local vite plugin and run yarn dev in build-plugins
  3. Run a production build (npx vite build) and verify backend function .js files are generated and included in the upload archive
  4. Confirm the build works without the user's vite.config.ts plugins interfering with the backend build

Blast Radius

  • Only affects vite production builds when apps.backendDir is configured
  • Rollup users with backend functions will now see a warning that backend functions are not supported for their bundler (previously attempted but was untested)
  • No impact on webpack, esbuild, or rspack users

Documentation

…ackend support

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sdkennedy2 sdkennedy2 requested a review from yoannmoinet as a code owner March 20, 2026 20:38
Copy link
Collaborator Author

sdkennedy2 commented Mar 20, 2026

* Produces one standalone JS file per function in a temp directory.
*/
async function buildBackendFunctions(
vite: any,
Copy link
Member

Choose a reason for hiding this comment

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

Can't this be typed correctly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

My thought process initially was trying to avoid importing vite since I would need to add it as a dependency in our app's package.json.

The more I think about it we could add vite as a dev dependency if we are just importing the type though. One annoying thing is vite doesn't actually export very good types it only exports declarations for specific pieces of code.

So I think I'll need to do the following:

import type { build } from 'vite';

...
async function buildBackendFunctions(
    viteBuild: typeof build,
    functions: BackendFunction[],
    backendOutputs: Map<string, string>,
    buildRoot: string,
    log: Logger,
): Promise<void> {

Let me know if you are ok adding vite as dev dependency in the package.json

Copy link
Member

Choose a reason for hiding this comment

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

devDeps is ok I think, since it won't impact the user's experience.
And we're already depending on rollup for the same reason.
If the typeof solution works, let's go with it.

functions: BackendFunction[],
backendOutputs: Map<string, string>,
log: Logger,
pluginContext?: BackendPluginContext,
Copy link
Member

Choose a reason for hiding this comment

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

Why is this optional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good callout. It is not. I'm just going to inline the two properties within BackendPluginContext as parameters passed into getBackendPlugin as well. It seems arbitrary to have these two separated.

Comment on lines -26 to +28
const actionPath = actionId.replace(/^com\\\\.datadoghq\\\\./, '');
const actionPath = actionId.replace(/^com\\.datadoghq\\./, '');
Copy link
Member

Choose a reason for hiding this comment

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

Isn't this tested?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Just to clarify, you are calling out that the behavior captured within the string should also be tested?

Copy link
Member

Choose a reason for hiding this comment

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

I was wondering why did you have to change this.
And wondering if the output of it was actually tested in order to catch this kind of mistake before merge.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There was testing done, but looking at the test app it looks like it tested the more basic case of returning static data. It must have been a previous iteration that tested the action catalog support.

Since we don't have this functionality enabled for customers yet, I don't think this was an issue. We are working towards a point where we can do a true end to end test of developing an app and publishing. At the point we enable this for customers we should feel good about it.

I do think its a good callout that we should make sure we have get tests around the integration between the high code apps logic in different repros and between different libraries though.

export function generateVirtualEntryContent(
functionName: string,
entryPath: string,
projectRoot?: string,
Copy link
Member

Choose a reason for hiding this comment

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

Why is this optional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good callout I'll remove it.

// Backend build plugin — builds backend functions via a separate vite.build().
// Only supported for vite.
const backendSupportedBundlers = ['vite'];
if (hasBackend && backendSupportedBundlers.includes(context.bundler.name)) {
Copy link
Member

Choose a reason for hiding this comment

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

You don't need this part backendSupportedBundlers.includes(context.bundler.name) since you control this from the other side by giving the { vite: ... } part.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Good call 👍

@datadog-datadog-prod-us1
Copy link

datadog-datadog-prod-us1 bot commented Mar 26, 2026

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 90d9b77 | Docs | Datadog PR Page | Was this helpful? React with 👍/👎 or give us feedback!

* Build all backend functions using a separate vite.build() call.
* Produces one standalone JS file per function in a temp directory.
*/
export async function buildBackendFunctions(
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I put this here because the logic is largely coupled to vite versus the code in the backend folder.

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