Skip to content

[APPS] Add vite dev server middleware for backend functions#291

Open
sdkennedy2 wants to merge 2 commits intosdkennedy2/apps-vite-backend-buildfrom
sdkennedy2/apps-vite-dev-server
Open

[APPS] Add vite dev server middleware for backend functions#291
sdkennedy2 wants to merge 2 commits intosdkennedy2/apps-vite-backend-buildfrom
sdkennedy2/apps-vite-dev-server

Conversation

@sdkennedy2
Copy link
Collaborator

@sdkennedy2 sdkennedy2 commented Mar 17, 2026

Motivation

Developers building high code apps with backend functions need a way to test those functions locally during vite dev without deploying. This PR adds a Vite dev server middleware that bundles and executes backend functions on-the-fly, using the same vite.build() approach established in #292.

Changes

Added a Vite dev server middleware that registers via configureServer and intercepts two endpoints:

  • POST /__dd/debugBundle — returns the bundled script for inspection
  • POST /__dd/executeAction — bundles the function, sends it to Datadog's query preview-async API, polls for the result, and returns it

Why vite.build() instead of the dev server's transform pipeline?

Vite's dev server (transformRequest, ssrLoadModule) is designed for serving individual modules on-demand — it doesn't produce bundled output. But Datadog's execution API needs a complete, self-contained script with all dependencies resolved and inlined into one file. vite.build({ write: false }) runs a mini Rollup build in memory, producing exactly that.

Why the hacky import('vite')?

The dev server middleware needs to call vite.build(), but we can't add vite as a dependency of the apps plugin. The apps plugin is bundler-agnostic — it's compiled into @datadog/webpack-plugin, @datadog/esbuild-plugin, etc. Adding vite as a dependency would force every bundler's published package to pull in vite.

Instead, we use a variable indirection to make the import opaque to Rollup's static analysis:

const viteModule = 'vite';
const { build: viteBuild } = await import(/* @vite-ignore */ viteModule);

This is safe because: the code only runs inside configureServer, which is a Vite-only hook — vite is guaranteed to be installed.

Key implementation details:

  • configFile: false prevents recursion (user's vite.config.ts includes the Datadog plugin)
  • treeshake: false + inlineDynamicImports: true keeps action-catalog bridges and argument passing fully intact
  • generateDevVirtualEntryContent() inlines args as JSON (vs the production template expression ${backendFunctionArgs})
  • Long-polling with up to 10 retries (~30s server-side timeout each) for execution results

New files:

  • backend/vite/dev-server.ts — middleware, Vite-based bundling, Datadog API integration
  • backend/vite/dev-server.test.ts — unit tests

Modified files:

  • backend/virtual-entry.ts — added generateDevVirtualEntryContent() for dev mode
  • backend/vite/index.ts — registers middleware via configureServer hook
  • backend/index.ts — added auth to BackendPluginContext
  • src/index.ts — passes auth credentials to the backend plugin

QA Instructions

  1. Scaffold or use an existing high code app with a backend/ directory containing a function
  2. Link the local build-plugins vite plugin
  3. Run yarn dev in build-plugins, then start the app: NODE_TLS_REJECT_UNAUTHORIZED=0 dd-auth --domain="dd.datad0g.com" --actions-api -- npx vite
  4. Test debug bundle: curl -X POST http://localhost:5173/__dd/debugBundle -H "Content-Type: application/json" -d '{"functionName": "yourFunction", "args": []}'
  5. Test execution: curl -X POST http://localhost:5173/__dd/executeAction -H "Content-Type: application/json" -d '{"functionName": "yourFunction", "args": ["test"]}'

Blast Radius

  • Only affects the vite dev server when apps.backendDir is configured and backend functions are discovered
  • No impact on production builds or non-vite bundlers
  • The middleware only activates when auth credentials are available

Documentation

Copy link
Collaborator Author

sdkennedy2 commented Mar 17, 2026

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@sdkennedy2 sdkennedy2 changed the title [APPS] Add vite dev server middleware for backend functions using host bundler [APPS] Add vite dev server middleware for backend functions Mar 17, 2026
@sdkennedy2 sdkennedy2 requested a review from sarenji March 17, 2026 14:49
@sdkennedy2 sdkennedy2 marked this pull request as ready for review March 17, 2026 14:50
@sdkennedy2 sdkennedy2 requested a review from yoannmoinet as a code owner March 17, 2026 14:50
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-vite-dev-server branch from a1630ef to 4f85ed9 Compare March 17, 2026 18:02
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-backend-functions-upload-v2 branch from 264286b to 2f9f167 Compare March 17, 2026 18:02
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-vite-dev-server branch from 7e20d87 to e465412 Compare March 18, 2026 15:53
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-backend-functions-upload-v2 branch from 2f9f167 to d682d52 Compare March 18, 2026 15:53
Base automatically changed from sdkennedy2/apps-backend-functions-upload-v2 to master March 19, 2026 15:43
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 on this

Comment on lines +69 to +70
// Dynamic import — vite is guaranteed available when bundler is vite.
// Use a variable to prevent Rollup from statically analyzing and bundling vite.
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we do dependency injection somehow, by passing a vite build function to this middleware, avoiding the hassle of importing Vite within createDevServerMiddleware?

We could also externalize vite if necessary.

log: Logger,
): Promise<unknown> {
const endpoint = `https://${auth.site}/api/v2/app-builder/queries/execution-long-polling/${receiptId}`;
// Each long-poll request waits server-side (~30s). Max retries provides a safety net.
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider using exponential backoff, plus a jitter, for cases where the API has gone down and is trying to get back up

I also see async-retry usage here, and maybe doRequest in general is a method we can reuse.

const res = createMockResponse();

middleware(req, res, jest.fn());
await new Promise((resolve) => setTimeout(resolve, 100));
Copy link
Contributor

Choose a reason for hiding this comment

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

This seems like a potential source of flakiness and test slowdown. Can we be more exact on what we are waiting for?

Comment on lines +282 to +286
const code = await bundleBackendFunction(func, args, projectRoot, log);

res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end(code);
Copy link
Contributor

Choose a reason for hiding this comment

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

Fundamentally I do not understand this because Connect middleware is not async so it's confusing to me how this is working.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sdkennedy2 sdkennedy2 changed the base branch from master to graphite-base/291 March 20, 2026 20:38
@sdkennedy2 sdkennedy2 force-pushed the sdkennedy2/apps-vite-dev-server branch from 95203d8 to a0932f3 Compare March 20, 2026 20:38
@sdkennedy2 sdkennedy2 changed the base branch from graphite-base/291 to sdkennedy2/apps-vite-backend-build March 20, 2026 20:52
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