Skip to content

[APPS] Inject backend functions as host build entry points (Phase 1: Rollup/Vite)#289

Merged
sarenji merged 1 commit intomasterfrom
sdkennedy2/apps-backend-functions-upload-v2
Mar 19, 2026
Merged

[APPS] Inject backend functions as host build entry points (Phase 1: Rollup/Vite)#289
sarenji merged 1 commit intomasterfrom
sdkennedy2/apps-backend-functions-upload-v2

Conversation

@sdkennedy2
Copy link
Collaborator

@sdkennedy2 sdkennedy2 commented Mar 16, 2026

Motivation

The apps plugin needs to bundle backend functions (one standalone JS file per function) and include them in the upload archive. The previous approach (sdkennedy2/apps-backend-functions-upload) programmatically invoked bundlers in asyncTrueEnd — a separate build per function. This was heavy, required bundler-specific code, and added esbuild as a dependency.

This PR injects backend functions as additional entry points into the host build, so the user's bundler processes them alongside frontend code in a single build pass. Since the Action Platform sandbox is pure JS (no Node APIs, no network), there's no target conflict.

Changes

Phase 1 implements Rollup/Vite support with shared infrastructure for future bundler support.

Backend Function Upload Lifecycle (High-level)

  1. Backend discovery in plugin init

    • getPlugins() discovers backend functions from backendDir before build starts.
  2. Backend virtual entries are injected

    • The backend plugin exposes shared Unplugin hooks (resolveId, load) to provide virtual entry modules.
  3. Backend chunks are created during build

    • Rollup/Vite adapter hooks (buildStart, writeBundle) emit backend chunks and capture their output file paths.
  4. Upload includes backend chunks in the archive

    • asyncTrueEnd receives the captured backend chunk map and packages those chunk outputs under a backend/ prefix in the upload zip.

Archive structure

app-upload.zip
├── frontend/
│   ├── index.html
│   └── assets/...
└── backend/
    └── hello.js          # standalone file with export { main }

QA Instructions

  1. Run yarn typecheck:all — no type errors
  2. Run yarn test:unit packages/plugins/apps — all 59 tests pass
  3. Manual test with a Vite app that has a backend/ directory containing function modules — verify the archive contains both frontend/ and backend/ directories with standalone backend bundles

Blast Radius

  • Only affects apps plugin (@dd/apps-plugin) when backendDir contains function files
  • No changes to existing behavior when no backend functions are present
  • Phase 1 only covers Rollup/Vite; webpack/rspack/esbuild support in follow-up PRs

Copy link
Collaborator Author

sdkennedy2 commented Mar 16, 2026

@sdkennedy2 sdkennedy2 changed the title Inject backend functions as host build entry points (Phase 1: Rollup/Vite) [APPS] Inject backend functions as host build entry points (Phase 1: Rollup/Vite) Mar 16, 2026
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-backend-functions-upload-v2 branch from 550d72c to 788c479 Compare March 16, 2026 18:12
@sdkennedy2 sdkennedy2 changed the base branch from master to graphite-base/289 March 16, 2026 19:12
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-backend-functions-upload-v2 branch from 788c479 to 7b19d11 Compare March 16, 2026 19:12
@sdkennedy2 sdkennedy2 changed the base branch from graphite-base/289 to sdkennedy2/apps-plugin-e2e-test March 16, 2026 19:12
@datadog-prod-us1-4
Copy link

datadog-prod-us1-4 bot commented Mar 16, 2026

✅ Tests

🎉 All green!

❄️ No new flaky tests detected
🧪 All tests passed

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

@sdkennedy2 sdkennedy2 requested a review from sarenji March 16, 2026 19:51
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-backend-functions-upload-v2 branch 3 times, most recently from d03ec27 to 254c878 Compare March 17, 2026 00:00
@sdkennedy2 sdkennedy2 marked this pull request as ready for review March 17, 2026 01:14
@sdkennedy2 sdkennedy2 requested a review from yoannmoinet as a code owner March 17, 2026 01:14
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-backend-functions-upload-v2 branch from 264286b to 2f9f167 Compare March 17, 2026 18:02
Base automatically changed from sdkennedy2/apps-plugin-e2e-test to master March 17, 2026 19:01
log.error(`Backend function "${funcName}" not found.`);
return null;
}
return generateVirtualEntryContent(func.name, func.entryPath);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We generate the virtual entry points here

throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
});

const result = discoverBackendFunctions(backendDir, log);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We discover all the backend functions here.

Copy link
Contributor

@sarenji sarenji left a comment

Choose a reason for hiding this comment

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

Some initial thoughts!


test.each(cases)('Should $description', ({ input, expected }) => {
const plugin = getBackendPlugin(functions, new Map(), log);
const resolveId = plugin.resolveId as Function;
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is plugin.resolveId and plugin.load being typecasted? Can we avoid that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These casts are test-only — resolveId and load on PluginOptions are typed as unplugin's Hook<> wrapper (a union of function + object-with-handler), not plain callables. We'd need custom type definitions to eliminate these, which felt like more machinery than warranted for 3 test call sites. Happy to revisit if you feel strongly.

Comment on lines +45 to +51
if (Array.isArray(outputOptions)) {
for (const out of outputOptions) {
guardManualChunks(out);
}
} else if (outputOptions) {
guardManualChunks(outputOptions);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this mutating the options? I'd love to avoid that if the expectation is that these are immutable. I've had this result in subtle bugs on different projects.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

outputOptions is Rollup's designated hook for modifying the output config. The expected pattern is to mutate or replace properties on the object and return it. A shallow clone could introduce its own issues here since the array case (multiple output configs) can share nested references. Happy to add a comment in the code clarifying the mutation is intentional if that helps.

const allAssets: Asset[] = assets.map((asset) => ({
// Exclude backend output files from frontend assets if backend is active.
const frontendOnly = hasBackend
? assets.filter((a) => !new Set(backendOutputs.values()).has(a.absolutePath))
Copy link
Contributor

Choose a reason for hiding this comment

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

Is filtering on absolutePath the right condition? Don't we already have a very convenient constant for knowing whether something is a backend function?

Copy link
Collaborator Author

@sdkennedy2 sdkennedy2 Mar 18, 2026

Choose a reason for hiding this comment

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

BACKEND_VIRTUAL_PREFIX is the constant you may be thinking of, but it only applies to virtual module IDs during build-time resolution/loading (\0dd-backend:myHandler). By the time we reach upload, we are working with resolved output file paths (e.g., /dist/backend/myHandler.js) - the virtual prefix is gone. So filtering against backendOutputs values is the right mechanism here. I did hoist the Set construction out of the filter loop though.

@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-backend-functions-upload-v2 branch from 2f9f167 to d682d52 Compare March 18, 2026 15:53
@sarenji sarenji merged commit 8800dab into master Mar 19, 2026
4 checks passed
@sarenji sarenji deleted the sdkennedy2/apps-backend-functions-upload-v2 branch March 19, 2026 15:43
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