[APPS-2792] Add: wire local execution into the real dev server - #481
Conversation
|
✅ All CI checks and tests passed. 🎉 All green!🧪 All tests passed 🔗 Commit SHA: fbddcb4 | Docs | View more details | Give us feedback! |
6e85225 to
ae53df1
Compare
…function body server.ssrLoadModule(func.absolutePath) goes through the same transform hook (vite/index.ts) that rewrites *.backend.ts into the client-side RPC-proxy stub — so local execution's "real" import can actually still be the proxy stub, which crashes since globalThis.DD_APPS_RUNTIME doesn't exist server-side. Every existing test here mocks loadModule directly, so none of them exercise the real transform pipeline and would catch this. Append the same query-suffix marker introduced in #481 (matching Vite's own ?raw/?url convention) so the shared transform hook can recognize this specific request and skip proxy generation for it. The transform-hook side of this fix lives in #481, since that's where local execution is actually wired to a real, plugin-registered dev server — this PR only needs its own call site and mocks to stay consistent with that contract so the two branches reconcile cleanly whichever merges first.
7cbbeec to
d976f85
Compare
…function body server.ssrLoadModule(func.absolutePath) goes through the same transform hook (vite/index.ts) that rewrites *.backend.ts into the client-side RPC-proxy stub — so local execution's "real" import can actually still be the proxy stub, which crashes since globalThis.DD_APPS_RUNTIME doesn't exist server-side. Every existing test here mocks loadModule directly, so none of them exercise the real transform pipeline and would catch this. Append the same query-suffix marker introduced in #481 (matching Vite's own ?raw/?url convention) so the shared transform hook can recognize this specific request and skip proxy generation for it. The transform-hook side of this fix lives in #481, since that's where local execution is actually wired to a real, plugin-registered dev server — this PR only needs its own call site and mocks to stay consistent with that contract so the two branches reconcile cleanly whichever merges first.
d976f85 to
1900a78
Compare
1900a78 to
a0bcc4f
Compare
a0bcc4f to
dc33400
Compare
There was a problem hiding this comment.
Pull request overview
Friend, this PR wires backend functions into Vite’s in-process local execution path while retaining cloud execution separately.
Changes:
- Routes
/__dd/executeActionlocally and adds the cloud-specific endpoint. - Preserves real backend source during local Vite loading.
- Adds action connection forwarding and regression coverage.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
packages/tests/src/_jest/helpers/mocks.ts |
Adds a shared module resolver mock. |
packages/plugins/apps/src/constants.ts |
Defines the local-load marker. |
packages/plugins/apps/src/vite/local-execution.ts |
Loads marked backend modules. |
packages/plugins/apps/src/vite/local-execution.test.ts |
Updates module-loading tests. |
packages/plugins/apps/src/vite/index.ts |
Configures SSR loading and middleware. |
packages/plugins/apps/src/vite/index.test.ts |
Tests transforms and SSR configuration. |
packages/plugins/apps/src/vite/dev-server.ts |
Splits local and cloud execution. |
packages/plugins/apps/src/vite/dev-server.test.ts |
Tests both execution routes. |
packages/plugins/apps/src/vite/dev-server.integration.test.ts |
Exercises real Vite module loading. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
4578c5c to
c2676bc
Compare
6878e2a to
1563928
Compare
094c394 to
9507db8
Compare
6fc021f to
d5751a4
Compare
05c85bc to
8a73a78
Compare
This comment was marked as outdated.
This comment was marked as outdated.
|
@codex review |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 28 out of 30 changed files in this pull request and generated 3 comments.
Suppressed comments (2)
packages/plugins/apps/src/vite/index.ts:185
- This set makes the local-execution context persist by module ID instead of by module load. Helpers are deliberately left unsuffixed, so after one local traversal any later unrelated SSR load from the same helper is treated as local and receives real backend code; in the reverse order, Vite can reuse the helper's already-cached SSR transform that points at the RPC proxy, breaking local execution. Keep the context in distinct suffixed module IDs throughout the local subgraph (or otherwise scope it to one resolution graph) rather than storing bare helper IDs globally.
if (!BACKEND_FILE_RE.test(resolved.id)) {
suffixedSubgraphImporters.add(resolved.id);
return resolved;
packages/plugins/apps/src/vite/dev-server.ts:415
- This priming call evaluates arbitrary customer top-level code before
executeScriptLocallyenters its serialization queue. Concurrent requests for different cold entries can therefore interleave top-level side effects, and a timed-out priming promise can continue evaluating during a later execution without an epoch guard. Move the priming load and graph collection inside the same serialized execution boundary as the function invocation.
const entrySpecifier = func.absolutePath + LOCAL_EXECUTION_LOAD_SUFFIX;
const primingLoadPromise = loadCustomerModuleEntry(loadModule, entrySpecifier);
const primedModule = await withTimeout(
primingLoadPromise,
DEFAULT_TIMEOUT_MS,
`Loading "${displayName}"`,
);
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c1cab6a900
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| source = await readFile(node.file, 'utf-8'); | ||
| } catch (error) { | ||
| const reason = error instanceof Error ? error.message : String(error); | ||
| throw unsupportedModuleGraphDependency( |
There was a problem hiding this comment.
Analyze the source produced by Vite plugins
When a custom Vite load or transform hook rewrites an app-local TypeScript module, ssrLoadModule executes that rewritten source, but this collector analyzes the original file from disk. For example, an action-catalog call or import inserted by a transform is absent from the resulting connection allowlist and is then rejected during local execution; a load hook serving a synthetic filesystem ID can instead fail here as unreadable. The production collector avoids this mismatch by analyzing post-transform moduleInfo.code, so the dev collector also needs to consume source from the Vite plugin pipeline rather than readFile(node.file).
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed real and customer-reachable — a customer's own vite.config.ts is a real, hand-editable file that can add any Vite plugin. It fails safe though: an undercollected allowlist causes a runtime rejection, not a leak, and it requires a customer's own plugin to specifically rewrite a backend-reachable file with new action-catalog-relevant code. A proper fix means teaching collectActionCatalogImports to also parse Vite's SSR-rewritten __vite_ssr_import__ call syntax (confirmed via server.transformRequest's actual output), not just plain ImportDeclaration — real parser work, not a mechanical change. Tracked as a deferred follow-up in the PR description's Out of Scope table rather than folded into this pass.
rollupConfig.mjs's `external` list only matched a dependency's bare specifier exactly, so `rollup/parseAst` (needed by the new dev-server module-graph collector) got bundled instead of externalized despite `rollup` being declared as a dependency. The bundled copy pulls in Rollup's own native-binary platform loader, which throws through `@rollup/plugin-commonjs`'s dynamic-require interop the moment a consumer's real dev server calls it — breaking every published plugin package for real users, not just this repo's own tests. Switched the `external` option to a function that also matches subpath imports (`id === name || id.startsWith(name + '/')`) for every declared dependency and peer dependency. Also drops a stray blank line introduced between a comment and the test it documents in local-execution.test.ts.
The priming loadModule call (needed to populate the module graph for connection-ID collection before the real execution starts) evaluates the entry's real top-level code and runs before executeScriptLocally installs its own hang-detection timeout — so it had no bound of its own. A customer module with a hanging top-level await would wedge the request forever. Wraps it in the same withTimeout helper the rest of this file already uses, now exported for this cross-module use.
…rver output The SSR transform Vite actually runs for a dev-server-only load rewrites every import into a __vite_ssr_import__(...) call and resolves specifiers to absolute paths, which the plain-ImportDeclaration AST search built for a real Rollup build can't parse. Read each module's original source from disk and strip TS/JSX with esbuild in isolation instead, so the parser sees the same untransformed import syntax the production build path already trusts.
…ound total execution time collectModuleGraphFromServer silently fell back to a static import's raw specifier text when Vite's resolveId failed to resolve it, instead of failing closed like every sibling module-graph error path — a connectionId-scoped action call behind an unresolvable import would silently drop out of the allowlist instead of the request failing loudly. /__dd/executeAction ran a customer's real backend code with no auth check upfront, only lazily inside a $.Actions call — unlike production, which authenticates before any query/execution logic runs (app-builder-api's PreviewAsyncQueryHandler). A function that never calls $.Actions was a loophole around the same requirement production always enforces. Checked upfront as a local credential-presence check, not a network call, so it costs no latency on the local dev loop. guardedExecuteAction's hang-detection pause can't distinguish a customer function genuinely awaiting a slow $.Actions call from one that fired a call without awaiting it and then hung on something unrelated — an unawaited call masked a real hang for up to MAX_ACTION_CALL_TIMEOUT_MS (10 minutes). A second, independent absolute ceiling now bounds one execution's total wall-clock time regardless of pendingActionCalls, set just above pollQueryExecution's own ~300s worst-case long-poll budget so a legitimate slow call still always finishes. Also fixes a comment narrating this PR's own before/after history and an embedded milestone number, both against repo convention.
makeExecuteActionRemotely's own inner auth check and doc comment
("no auth needed until a call is actually made") went stale once the
caller started requiring auth upfront — its only caller is only ever
reached after that upfront check already passed, making the inner
check unreachable and the comment actively contradictory. Narrows both
functions' doAuthenticatedRequest parameter to required, matching how
the sibling /__dd/executeActionViaCloud path already types it.
Removes the now-redundant "no auth + calls $.Actions" test, fully
subsumed by the upfront-check test right after it — no test exercises
the removed lazy check anymore since nothing can reach it.
…eScriptLocally's own load handleExecuteAction's priming load is the only place a customer module's top-level code actually runs (Vite caches the module, so executeScriptLocally's own load below just reuses the resolved object) — but it called loadModule directly instead of going through customerModuleLoadContext, so a customer module reaching for $ during its own top-level evaluation silently resolved to whatever $ a prior execution left behind instead of throwing the same way it does inside executeScriptLocally. Extracts the scoping into loadCustomerModuleEntry, shared by both call sites. Also corrects the startup warning logged when auth isn't configured: it still described only $.Actions calls as failing, but the earlier auth-upfront hardening rejects the whole /__dd/executeAction endpoint before any backend-function code runs, regardless of whether it calls $.Actions.
…violations found in round-6 review - Add test coverage for the startup auth-warning log message (dev-server.ts) — no test asserted its exact wording, which is how a stale claim about which endpoints fail escaped an earlier review round. - guardedExecuteAction is async, so its not-current branch's `return Promise.reject(...)` was needless wrapping — use `throw` instead. - Extract three inlined function-call arguments (withTimeout's first argument at two dev-server.ts call sites, executeAction's result in local-execution.ts) into named locals, matching this file's own existing convention at every other withTimeout call site. - Replace a bare `as any` cast (with an eslint-disable to suppress the rule that would flag it) in dev-server-module-graph.test.ts with a narrower `as unknown as ViteDevServer`. - Move two same-line comments onto their own line. - Reword four regression-test comments in dev-server.integration.test.ts that narrated "before the fix, X happened" — restated as the present-tense invariant each test guards.
…istration caching handleExecuteAction wrapped loadModule in a fresh closure every request to reuse an already-primed entry, but local-execution.ts's action-catalog and apps-backend registration caches are keyed on loadModule's own identity — so every request looked like a fresh, unregistered loadModule and re-ran SDK registration in full instead of hitting the cache. executeScriptLocally now takes the primed entry as its own parameter, so loadModule itself stays the stable reference the registration caches expect.
…ot the whole dev server resolveId's suffixedSubgraphImporters Set lived for the dev server's entire process lifetime, with no distinction between "this helper is part of the local execution currently running" and "this helper was part of some past execution." A later, unrelated SSR resolution of the same helper module (e.g. from an app that also does its own ordinary SSR) would inherit the marker and serve real backend code instead of the frontend RPC-proxy stub. Scopes the Set via AsyncLocalStorage instead, established in loadCustomerModuleEntry alongside the existing customerModuleLoadContext — every caller that loads a customer entry, real dev-server request or test harness alike, funnels through that one function, so the fix reaches every path uniformly rather than only whichever loadModule happened to be wrapped.
The external matcher exists specifically to externalize a dependency's subpath imports (e.g. rollup/parseAst) alongside its bare specifier, fixing a real crash this PR's Motivation calls out — but had no test coverage. Runs the real .mjs module in a node subprocess since ts-jest compiles this test file to CommonJS, which can't import a real ES module directly.
…ViaCloud auth gate Adds regression tests for unreadable/unparseable module source and a self-referential import cycle in collectModuleGraphFromServer, plus a no-auth-configured case for /__dd/executeActionViaCloud mirroring the existing /__dd/executeAction coverage.
Motivation
/__dd/executeActionstill bundles and calls the full cloud round trip. This PR is what actually makesnpm run devfast: it swaps the customer-facing endpoint over to the in-process path and drops the bundling step from it entirely.$.Actionscalls now threadconnectionIdend-to-end throughExecuteAction→makeExecuteActionRemotely→ the single-actionpreview-asyncquery spec, so an action naming a specific connection can reach it.rollup/parseAstimport movedrollupfrom a devDependency to a real dependency of every published package.rollupConfig.mjs'sexternallist only matched a dependency's exact bare specifier, not a subpath import of it, sorollup/parseAstgot bundled instead of externalized — pulling Rollup's own native-binary loader into the published output and crashing any real consumer'svite.config.tsload. Fixed at the source (subpath-aware externalization) rather than avoiding therollup/parseAstimport.Architecture
createDevServerMiddlewarenow routes the two execution endpoints down genuinely different paths — one bundle-free and in-process, one bundling and cloud-bound — that only reconverge at the sharedsubmitQuery/pollQueryExecutionhelpers once an$.Actionscall needs to reach the real Datadog API:The
executeActionpath never bundles at all —executeScriptLocallyimports the customer's real file directly vialoadModule(Vite's ownssrLoadModule, so it gets the same TS-transform/resolve rules and HMR-aware module cache a real request gets) and runs the exported function in this process. Auth is checked upfront for the whole endpoint, matching production's auth-before-execution ordering, so a function that never calls$.Actionsisn't a loophole around that requirement; when a function does call$.Actions, that call becomes its own direct single-actionpreview-asyncquery viamakeExecuteActionRemotely, rather than being wrapped in a whole-script query. TheexecuteActionViaCloudpath is the unchanged production round trip: bundle the whole function with Rollup, wrap it as ajsFunctionWithActionsquery, and submit/poll it the same way. See the RFC's Proposed Solution for the design-level version of this split.server.ssrLoadModuleshares the same transform pipeline as every other module Vite serves — includingvite/index.ts's own.backend.ts→ RPC-proxy transform, which exists for frontend imports of the same file. Local execution'sloadModulecall marks its own request with a query suffix (LOCAL_EXECUTION_LOAD_SUFFIX, matching Vite's own?raw/?urlconvention) so the transform hook can skip proxy generation specifically for that request, rather than for every SSR-context load of a.backend.tsfile (which would also silently affect any unrelated future feature hitting the same hook).Changes
/__dd/executeActionnow looks up the requested function and runs it directly viaexecuteScriptLocally— no bundling on this path at all./__dd/debugBundleand the cloud round trip (/__dd/executeActionViaCloud) are unchanged and still bundle.makeExecuteActionRemotelynow forwardsconnectionIdinto the single-actionpreview-asyncquery spec ({fqn, inputs, connectionId}) instead of silently dropping it.createDevServerMiddlewaretakes a newloadModule: LoadModuleparameter, threaded fromvite/index.ts'sconfigureServer(server)asserver.ssrLoadModule.bind(server)— the real Vite dev server's own module loader, giving the local path the same TS-transform/resolve rules and HMR-aware module cache a real request gets.config()hook returningssr: { noExternal: [...] }for@datadog/apps-backend/@datadog/action-catalog. Found while testing: both ship ESM-only, and Vite's dev server externalizesnode_modulesby default (a plainrequire(), for speed) — which throwsCannot use import statement outside a modulethe first time a customer's function actually uses either SDK locally.noExternalforces Vite's SSR transform pipeline to handle them instead, matching how the production bundling path already inlines every dependency.LOCAL_EXECUTION_LOAD_SUFFIX/LOCAL_EXECUTION_LOAD_RE: local execution'sloadModulecall marks its request with this suffix so the transform hook can tell it apart from a normal frontend import of the same file and skip generating the RPC-proxy stub, which would otherwise crash server-side by calling a browser-only global.loadModuletest double — previously hand-rolled separately in this file and in #480/#484'slocal-execution.test.ts— into a sharedmoduleResolverForhelper.createServer, middleware mode, no port bound) rooted at the sameapps_backend_projectfixture, and lets its realssrLoadModuleimport a real.backend.tsfile directly — no mocked bundler, no mockedloadModule. Confirms a real@datadog/apps-backendtyped import resolves$.Sourcecorrectly through this exact path.$.Actions), a clear error when a function does call$.Actionswith no auth configured, the single-actionpreview-asyncrequest-body shape now includingconnectionId, and the newconfig()hook'sssr.noExternalcontract. Existing cloud-path tests unchanged aside from the newloadModuleparameter threaded through everycreateDevServerMiddlewarecall.getAllowedConnectionIds'scollectModuleGraphFromServercall looks up the entry node by its fully-resolved (suffixed) id, matching whatloadModuleactually resolved, whileextractConnectionIdsFromModuleGraphstill receives the bare id to match its records map's keys. Regression test drives the real middleware for a cold entry with no priming import.bundle()'sexternaloption is now a matcher function instead of a plain string array, so a dependency's subpath imports (e.g.rollup/parseAst) are externalized the same as its bare specifier. Affects every published package's build, not justvite-plugin.handleExecuteAction's module-graph priming load now goes through a newloadCustomerModuleEntryhelper (shared withexecuteScriptLocally's own load) instead of callingloadModuledirectly. The priming load is the only place a customer module's top-level code actually runs — Vite caches the module, so the later load insideexecuteScriptLocallyjust reuses the resolved object — so it needs the samecustomerModuleLoadContextscoping, or a customer module reaching for$during its own top-level evaluation would silently resolve to whatever$a prior execution left behind instead of theundefineda fresh top-level access should see.withTimeoutcall site), replaced a bareas anycast in a test file with a narroweras unknown as ViteDevServer, and added a test asserting the exact startup auth-warning wording.handleExecuteActionpasses the priming load's resolved module intoexecuteScriptLocallyas its ownprimedEntryparameter instead of wrappingloadModulein a per-request closure — keepsloadModulethe same stable referencelocal-execution.ts's once-ever SDK registration caches key on.dev-server-module-graph.tsreads module source via the shared@dd/core/helpers/fsreadFile, matching every other file in this package, instead of importingnode:fs/promisesdirectly.resolveIduses to propagateLOCAL_EXECUTION_LOAD_SUFFIXthrough nested backend imports is now scoped to one local execution viaAsyncLocalStorage(established inloadCustomerModuleEntry, alongside the existingcustomerModuleLoadContext), instead of a single Set shared for the dev server's whole lifetime.bundle()'s subpath-awareexternalmatcher (dependency, peer dependency, Node built-in, explicit config entry, subpath import, and same-prefix-but-not-subpath false positive),collectModuleGraphFromServer's unreadable/unparseable source and self-referential import cycle handling, and a no-auth-configured case for/__dd/executeActionViaCloudmirroring the existing/__dd/executeActioncoverage.pollQueryExecution's outputs check is an explicitattrs.outputs === undefined || attrs.outputs === null, not a bare falsy check — a real action result of0,false, or''would otherwise be misclassified as "no outputs" and thrown as an error.QA Instructions
yarn test:unit packages/plugins/apps # Expected: Test Suites: 27 passed / Tests: 402 passed ✅ VERIFIEDyarn workspace @dd/apps-plugin run typecheck # Expected: no output, clean exit ✅ VERIFIEDnpx eslint packages/plugins/apps/src/vite/dev-server.ts packages/plugins/apps/src/vite/dev-server.test.ts packages/plugins/apps/src/vite/dev-server.integration.test.ts packages/plugins/apps/src/vite/index.ts packages/plugins/apps/src/vite/index.test.ts packages/plugins/apps/src/vite/local-execution.ts packages/plugins/apps/src/vite/local-execution.test.ts packages/plugins/apps/src/constants.ts packages/tests/src/_jest/helpers/mocks.ts --quiet # Expected: no output, clean exit ✅ VERIFIEDManual QA — real scaffolded app, real dev server (local + staging)
(
<hash>is the SHA-256-encoded query nameencodeQueryNamegenerates per function — read it off the generated frontend RPC-proxy stub, e.g.curl -s http://localhost:5173/src/functions.backend.ts.)Re-verified against the current tip: step 2's
npm linkcurrently hits the same pre-existing packaging issue noted in the driver section below (a workspace-linked source import —packages/factory/src/validate— that Node's native ESM loader can't resolve through the packageddist/), unrelated to this PR. Re-ran the equivalent checks through the direct-source driver instead (realcreateServer, realcreateDevServerMiddleware, realssrLoadModule— no mocks), extended with a case exercising this round's own fix:The third case is a customer module that reads
$during its own top-level evaluation (not inside the exported function), on a cold entry Vite hasn't loaded before in this process — confirms the priming load'scustomerModuleLoadContextscoping resolves$toundefinedthrough the realssrLoadModulepath, matchingdollarGetter's spec-correct semantics (an unresolvable$reads asundefined, never throws). ✅ VERIFIEDStaging (real
dd-auth --domain dd.datad0g.comcredentials, realpreview-asyncrequest toapi.datad0g.com, via the same direct-source driver wired withgetAuthenticatedRequest('apiKey', ...)instead of a stub):A fake action ID was used deliberately — the point is confirming the whole pipeline (auth headers, request submission, response parsing, error surfacing) reaches the real API and round-trips a real error correctly, not exercising a specific action. ✅ VERIFIED (this session, current tip — supersedes the stale reference to #473, which is now closed)
Manual QA —
getAllowedConnectionIdsmodule-graph wiring, direct drivernpm linkabove goes through@datadog/vite-plugin's packageddist/output, which bundles rollup and hits a pre-existing, unrelated native-binary resolution issue (documented in the Confluence QA guide) when loaded this way — unrelated to this PR, but it blocks using the scaffolded app above to testgetAllowedConnectionIdson a cold entry specifically. This driver imports straight from this branch's TS source instead, sidestepping that packaging layer entirely while still exercising the realcreateDevServerMiddleware/collectModuleGraphFromServercode:A durable writeup of this QA flow (including the local↔staging↔app-builder-code architecture) is in the Confluence QA guide.
Blast Radius
npm run dev's/__dd/executeActionnow executes locally by direct import, with no bundling step, instead of round-tripping to the cloud. Still gated behind this whole stack not being released yet (no version bump, nobump.yamltrigger in this PR)./__dd/executeActionViaCloud) — nothing currently calling/__dd/executeActionin production exists yet (this endpoint isn't released), so there's no live caller to break.ssr.noExternalconfig change affects every Vite dev-server session this plugin runs in, not just the local-execution path — low risk in practice (it only forces two specific, already-known-to-this-plugin packages through the transform pipeline instead of externalizing them), but worth noting as a config-surface change.LOCAL_EXECUTION_LOAD_SUFFIXtransform-hook change only special-cases requests carrying that exact marker — no behavior change for any existing frontend import of a.backend.tsfile.rollupConfig.mjsexternalization fix touches the build of all five published packages (esbuild-plugin,rollup-plugin,rspack-plugin,vite-plugin,webpack-plugin), not justvite-plugin— it's strictly more correct (a declared dependency's subpath imports are now externalized like its bare specifier already was) andyarn build:allplus the fullrollupConfig.test.tsbundling suite pass clean for every package after the change.esbuildis now a real (not dev) dependency of all five published packages —dev-server-module-graph.tsusesesbuild.transformto strip TS/JSX from a module's source read fresh off disk, since neither Vite's client transform nor its SSR transform result is usable for that purpose during an SSR-only load.LOCAL_EXECUTION_LOAD_SUFFIXcall-site change, so stacking lets that shared history reconcile once via rebase instead of as a merge conflict whichever PR landed second.Out of Scope / Follow-ups
npm run dev:verifyCLI (mode-aware routing to/__dd/executeActionViaCloud, web-ui template changes)npm link @datadog/vite-pluginagainst a real scaffolded app currently fails (ERR_MODULE_NOT_FOUNDon a workspace-linked source import,packages/factory/src/validate, that Node's native ESM loader can't resolve through the packageddist/)getAllowedConnectionIdsdriver section below; worth a dedicated fix so the scaffolded-app QA path in this PR's own instructions works again@datadog/action-catalogfixture package for a typed-import e2e test$.Actionsroutingvite.config.ts— a real, hand-editable file, not something App Builder generates or hides) can register aload/transformhook that rewrites a.backend.ts-reachable file;dev-server-module-graph.ts's connection-ID collector reads that file fresh off disk plus an isolatedesbuild.transform, not through Vite's full plugin pipeline, so a call the plugin's rewrite injects is invisible to the allowlist calculationcollectActionCatalogImportsto also parse Vite's SSR-rewritten__vite_ssr_import__call syntax (server.transformRequest's actual output), not just plainImportDeclaration— real parser work, not a mechanical change, so tracked as a follow-up rather than folded into this passDocumentation