[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: b7cc3d7 | 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
094c394 to
9507db8
Compare
6fc021f to
d5751a4
Compare
05c85bc to
8a73a78
Compare
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.
There was a problem hiding this comment.
Acknowledged as a known, accepted gap for now — left a comment at the readFile call in dev-server-module-graph.ts explaining why: transformRequest's own output already includes Vite's SSR import-rewrite (vite_ssr_import(...)) that this file's own AST parser can't read, so it can't be substituted in directly without first undoing that rewrite. Reading from disk misses a custom project-level load/transform hook's rewrites, same as noted here. Happy to revisit if this becomes a real-world blocker.
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
Codex Review: Didn't find any major issues. Swish! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
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". |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2eb3e316ce
ℹ️ 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".
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: efb6682dc8
ℹ️ 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".
There was a problem hiding this comment.
🟡 Changes recommended
Timeout budgeting, static-check ordering, resource-query handling, empty connection IDs, and function-valued external matchers contain unresolved defects.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 31/33 changed files
- Comments generated: 5
- Review effort level: Balanced
|
@codex review |
There was a problem hiding this comment.
🟡 Changes recommended
The execution ceiling breaks valid multi-action functions, and wrapped Rollup external callbacks lose required context arguments.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 34/36 changed files
- Comments generated: 2
- Review effort level: Balanced
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f05b74feb0
ℹ️ 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".
Also forward Rollup's full external(id, importer, isResolved) signature through rollupConfig.mjs's function-valued matcher wrapper instead of only id. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ng a copy DEFAULT_LONG_POLLING_CONFIG now calls validate.ts's resolveLongPolling directly, so the two can't silently drift apart. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
/merge |
|
View all feedbacks in Devflow UI.
The expected merge time in
|
Motivation
/__dd/executeActionstill bundled and round-tripped to the cloud./__dd/executeActionover to the in-process path and drops bundling from it entirely — this is what makesnpm run devfast.connectionIdend-to-end throughExecuteAction→makeExecuteActionRemotely→ the single-actionpreview-asyncquery spec, so an$.Actionscall naming a specific connection can reach it.rollupConfig.mjs'sexternalmatching bug:rollup/parseAst(now a real dependency of every published package via the module-graph collector) got bundled instead of externalized.vite.config.tsload.Architecture
createDevServerMiddlewarenow routes the two execution endpoints down genuinely different paths:submitQuery/pollQueryExecutionhelpers once an$.Actionscall needs to reach the real Datadog API.executeActionnever bundles:executeScriptLocallyimports the customer's 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 — a function that never calls
$.Actionsisn't a loophole around that.A
$.Actionscall inside the function becomes its own direct single-actionpreview-asyncquery viamakeExecuteActionRemotely, rather than being wrapped in a whole-script query.executeActionViaCloudis the unchanged production round trip: bundle the whole function with Rollup, wrap it as ajsFunctionWithActionsquery, submit/poll 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's
loadModulecall marks its request with a query suffix (LOCAL_EXECUTION_LOAD_SUFFIX, matching Vite's own?raw/?urlconvention) so the transform hook skips proxy generation for that request specifically.Scoping it to the request — rather than every SSR-context load of a
.backend.tsfile — avoids affecting any unrelated future feature that hits the same hook.Changes
48 changes across 36 files
/__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.node_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.loadModulecall marks its request with the existingLOCAL_EXECUTION_LOAD_SUFFIX/LOCAL_EXECUTION_LOAD_REconstants 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.collectModuleGraphFromServerruns the production build's backend static checks (banned Node built-ins, restricted globals) against every module it collects, not just connection-ID extraction -- a helper module with a banned import previously ran fine locally and was only rejected once published.loadModuletest double — previously hand-rolled separately across multiple test files in this stack — 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.@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.loadModuleparameter 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.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.vite-plugin.handleExecuteAction's module-graph priming load now goes through a newloadCustomerModuleEntryhelper (shared withexecuteScriptLocally's own load) instead of callingloadModuledirectly.executeScriptLocallyjust 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.normalizeDevServerModuleId(renamed fromnormalizeViteModuleId— same name asbackend-module-graph-collector.ts's own function but different behavior, so kept distinct) now strips only the local-execution marker by exact suffix match instead of everything after the first?, then fails closed if a real Vite resource query (e.g.?raw,?url,?worker) survives on a module that would otherwise be parsed as source — that query means the module's runtime value isn't the file's plain code, so parsing it as such could hide or fabricate an action-catalog connectionId.executeColdActionLocally, which wraps priming, connection-ID collection, and execution in a singleenqueue()call.handleExecuteActionpreviously primed the customer module and collected its connection IDs before callingexecuteScriptLocally, so only execution itself was serialized — two concurrent requests for two different cold functions could evaluate their top-level code in genuine parallel, violating the "executions never interleave" guarantee the queue exists to provide.resolveId's importer-suffix branch is now gated byresolveOptions.ssr === true, matching the subgraph-membership branch beside it and this hook's own comment — previously only the second branch checkedssr, so a client-mode resolution using an SSR-only suffixed id as its importer could have inherited the local-execution marker.makeExecuteActionRemotelynow checksconnectionId !== undefinedbefore including it in the outgoing query spec, matchingassertConnectionIdAllowed's own check — the previous truthy check would have silently dropped a legitimate empty-string connectionId instead of forwarding it.handleHttpError, deduping the identicalHttpError-to-status-code/sendErrorcatch-block logic acrosshandleDebugBundle,handleExecuteAction, andhandleExecuteActionViaCloud.local-execution.test.tsreuses its existingtestDollar()helper instead of re-deriving the sameglobalThiscast;dev-server.test.tsnow centralizes its own equivalentglobalThis.$.Actionscast behind a newtestDollarActions()helper instead of repeating it at each call site;createMockRequest/createMockResponse, previously duplicated verbatim across two test files, moved to the sharedmocks.tsalongsidemoduleResolverFor.executeScriptLocally's doc comment now states plainly that it's test-only infrastructure with no production caller (verified:executeColdActionLocallyis the sole production entry point) — kept as its own function rather than merged, since generalizing its signature to accept an in-queue priming step would mean changing whatprimedEntrymeans for the ~90 tests that call it directly.executeColdActionLocally's doc comment now names the priming/connection-IDwithTimeoutcalls' non-cancelling behavior as the same accepted "abandoned, not canceled" trade-off this file already documents for execution itself, rather than leaving it unstated — see the Out of Scope row below.resolveId's SSR check moved to a top-level guard instead of being folded into the suffixed-subgraph condition, so a future branch added below it inherits the gate automatically;normalizeDevServerModuleIdnow composes with the existingnormalizeViteModuleIdfor query-stripping instead of reimplementing it; extractedsendSuccess/guardAuthenticatedhelpers to dedupe the two endpoint branches' identical success-response and auth-gate-plus-catch code.getStaticModuleSources(previously module-private) so the dev server's module-graph collector can resolve each static import specifier the same way Rollup's build-time collector already does, instead of a second AST walk that could drift from it.esbuildas a dependency (used bydev-server-module-graph.tsfor isolated TS/JSX stripping) and promotesrollupfrom dev- to a runtime dependency (needed now that therollupConfig.mjsexternalization fix runs against it) across every published package, plus the corresponding lockfile update.@datadog/action-catalogfixture package (action-execution.js,index.js,package.json) stands in for the real SDK's connection-scoped action call, wired into the fixtures workspace and linked intoapps_backend_projectvia a portal dependency.apps_backend_projectfixture files (helper.ts,nestedImport.backend.ts,viaHelper.backend.ts,mixedImports.backend.ts,actionCatalogCall.backend.ts) exercise nested-import, nested-helper, mixed dynamic/static-import, and action-catalog call scenarios fordev-server.integration.test.ts's real end-to-end coverage.apps_backend_projectfixture files (helperWithBannedImport.ts,viaBannedHelper.backend.ts) givedev-server-module-graph.test.tsa helper module transitively reached through a.backend.tsentry that imports a banned Node builtin.deriveActionTimeoutsnow addspollQueryExecution's worst-case retry-delay budget (shared via a newretry-delay.tsmodule) on top ofmaxRetries * timeoutMs, so the derived ceiling can't undercut a real long-poll cycle that also waits between attempts.collectModuleGraphFromServerprimes each node viaserver.transformRequest(resolve + transform, never executes) as it's visited, instead of relying on a priorssrLoadModulecall — so no module's top-level code can run before this function's static checks get a chance to reject it.shouldTraverseCollectedModule) is now checked before the semantic-query guard, so a non-code import like./template.html?rawis skipped like the build-time collector skips it, instead of being rejected.extractConnectionIdsFromModuleGraphnow checksconnectionId !== undefinedinstead of truthiness, so a declared empty-stringconnectionIdis extracted instead of silently dropped.bundle()'sexternalmatcher now invokesconfig.externalwhen it's a function instead of always calling.includes()on it, fixing aTypeErrorfor any published package whose Rollup config passes a function-shapedexternal.QA Instructions
yarn 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:
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.createServer, 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).tmp-apps-2792-qa2,tmp-apps-2792-staging,tmp-apps-2792-qa) were stale after the master rebase —collectModuleGraphFromServergained a requiredlog4th argument, and the auth object had a leftovermethodfield that no longer exists onAuthOptionsWithDefaults. Fixed and re-ran all three fresh; all match documented expected output.getAllowedConnectionIdsto answer a readiness-pass efficiency concern (it re-walks the module graph with no cache on every request): cold call 9.9ms, repeat call on the same function 0.9ms, two other functions 1.2ms/1.6ms — negligible next to any real$.Actionsnetwork round trip. No caching work needed at this graph size.Staging (real
dd-auth --domain dd.datad0g.comcredentials, realpreview-asyncrequest toapi.datad0g.com, via the same direct-source driver wired with the realgetAuthenticatedRequest()readingDD_API_KEY/DD_APP_KEYfrom the environment instead of a stub). A fake action ID is 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: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.createDevServerMiddleware/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.bump.yamltrigger in this PR)./__dd/executeActionViaCloud) — no live caller exists yet on/__dd/executeActionsince this endpoint isn't released.ssr.noExternalaffects every Vite dev-server session this plugin runs in, not just the local-execution path — low risk in practice (only forces two 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_SUFFIXonly 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.yarn build:allplus the fullrollupConfig.test.tsbundling suite pass clean for every package.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.Out of Scope / Follow-ups
9 follow-up items
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, tracked as a follow-up rather than folded into this passhandleExecuteAction's local-execution path only runsrunBackendStaticChecks(banned Node built-ins, restricted globals) against the.backend.tsentry file, not a nested helper file it imports — unlike the cloud/build path, which registerscreateBackendStaticChecksPluginagainst the whole Rollup module graph. A helper using a banned API runs fine locally but is still caught by that same plugin at real build/deploy time (build-backend-functions.ts'sbuildBackendFunctions, used by bothcloseBundleand the cloud-preview path), so this has no production-security impact — it's a local-npm run devDX gap, not a bypasscollectModuleGraphFromServer's dev-server graph walk to runrunBackendStaticChecksagainst each collected module, not just the entry — moderate effort, and would inherit that collector's own documented limitation (re-reads/re-transforms source rather than consuming Vite's own pipeline output).backend.*) helper module reached during a local execution's traversal is tracked in anAsyncLocalStorage-scoped Set forresolveId's own proxy-vs-real-code decision, but is returned to Vite under its original unsuffixed module ID — so Vite's own module cache stores one entry for that helper regardless of which context resolved it first. A grep of this codebase confirms nothing else callsssrLoadModuleagainst this dev-server instance today, so there is currently no second consumer to actually race against local execution for that cache slot; only relevant if a customer's own Vite config also does ordinary SSR alongside this pluginnode_modules/SDK packages shared — but it likely also removes the currentsubgraphImportersSet and needs new tests for a scenario nothing today can trigger, so tracked as a follow-up rather than folded into this passexecuteColdActionLocally's priming and connection-ID-resolution steps are bounded bywithTimeout, which doesn't cancel the underlying work — if either step legitimately exceedstimeoutMs, the queue advances to the next request while the abandoned step's real top-level customer code keeps running, narrowly reopening the interleaving this function exists to preventexecutionEpoch) — real cancellation isn't available forloadModule/collectModuleGraphFromServer, and blocking the queue until the abandoned call settles would freeze every other function's dev loop behind one slow cold-start, worse for the fast-dev-loop goal than this narrow, low-probability riskresolveId's nested-backend-import detection uses the no-queryBACKEND_FILE_RE, whiletransform's own inclusion filter tolerates a trailing Vite resource query viaBACKEND_FILE_WITH_QUERY_RE. A.backend.tsfile importing another with an explicit resource query (e.g../other.backend?raw) resolves to an idBACKEND_FILE_REdoesn't match, so the suffix is never appended and the nested import falls back to the frontend proxy stub instead of real code.backend.tsfiles are meant to be imported for their exports, not as raw text/worker sources. Fails as a functional error in local execution, not a security or correctness issue in the cloud/build path. A fix needs the suffix-append step to also handle composing with an existing query without producing a double?, which wants its own test coverage rather than a same-pass patchDocumentation