Skip to content

[APPS-2792] Add: wire local execution into the real dev server - #481

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
masterfrom
tiffany.trinh/apps-2792-wire-into-dev-server
Sep 2, 2026
Merged

[APPS-2792] Add: wire local execution into the real dev server#481
gh-worker-dd-mergequeue-cf854d[bot] merged 3 commits into
masterfrom
tiffany.trinh/apps-2792-wire-into-dev-server

Conversation

@tyffical

@tyffical tyffical commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Motivation

  • Part of APPS-2792 — the wire-into-dev-server milestone of the Kickoff doc.
    • Builds on the in-process execution mechanism and its hardening merged earlier in this stack.
    • Neither was reachable from a real request until now — /__dd/executeAction still bundled and round-tripped to the cloud.
  • Swaps /__dd/executeAction over to the in-process path and drops bundling from it entirely — this is what makes npm run dev fast.
  • Threads connectionId end-to-end through ExecuteActionmakeExecuteActionRemotely → the single-action preview-async query spec, so an $.Actions call naming a specific connection can reach it.
  • Fixes rollupConfig.mjs's external matching bug:
    • It only matched a dependency's exact bare specifier, not a subpath import.
    • rollup/parseAst (now a real dependency of every published package via the module-graph collector) got bundled instead of externalized.
    • This pulled Rollup's native-binary loader into the published output, crashing any consumer's vite.config.ts load.
    • Fixed at the source with subpath-aware externalization.

Architecture

  • createDevServerMiddleware now routes the two execution endpoints down genuinely different paths:
    • One bundle-free and in-process, one bundling and cloud-bound.
    • Both only reconverge at the shared submitQuery/pollQueryExecution helpers once an $.Actions call needs to reach the real Datadog API.
POST /__dd/executeAction                    POST /__dd/executeActionViaCloud
        │                                            │
        ▼                                            ▼
handleExecuteAction                       handleExecuteActionViaCloud
        │                                            │
        ▼                                            ▼
executeScriptLocally               bundleBackendFunction (vite build,
  (local-execution.ts)               in-memory, no bundling on the
        │                            executeAction path anymore)
        │ loadModule =                        │
        │ server.ssrLoadModule                ▼
        │ (direct import of the      executeScriptViaDatadog
        │  customer's *.backend.ts,            │
        │  no bundling)               wraps the whole bundled script as
        │                              a jsFunctionWithActions query
        ▼                                       │
runs in this process                            │
        │                                       │
        │ $.Actions call?                       │
        ▼                                       │
makeExecuteActionRemotely                       │
  (single-action preview-async                  │
   query: {fqn, inputs, connectionId})          │
        │                                       │
        └────────────────┬──────────────────────┘
                          ▼
              submitQuery + pollQueryExecution
           (POST + long-poll api.<site>/api/v2/
              app-builder/queries/preview-async)
  • executeAction never bundles: executeScriptLocally imports the customer's file directly via loadModule (Vite's own ssrLoadModule, 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 $.Actions isn't a loophole around that.

  • A $.Actions call inside the function becomes its own direct single-action preview-async query via makeExecuteActionRemotely, rather than being wrapped in a whole-script query.

  • executeActionViaCloud is the unchanged production round trip: bundle the whole function with Rollup, wrap it as a jsFunctionWithActions query, submit/poll the same way.

  • See the RFC's Proposed Solution for the design-level version of this split.

  • server.ssrLoadModule shares the same transform pipeline as every other module Vite serves, including vite/index.ts's own .backend.ts → RPC-proxy transform (which exists for frontend imports of the same file).

  • Local execution's loadModule call marks its request with a query suffix (LOCAL_EXECUTION_LOAD_SUFFIX, matching Vite's own ?raw/?url convention) 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.ts file — avoids affecting any unrelated future feature that hits the same hook.

Changes

48 changes across 36 files
What changed File
/__dd/executeAction now looks up the requested function and runs it directly via executeScriptLocally — no bundling on this path at all. dev-server.ts
/__dd/debugBundle and the cloud round trip (/__dd/executeActionViaCloud) are unchanged and still bundle. dev-server.ts
makeExecuteActionRemotely now forwards connectionId into the single-action preview-async query spec ({fqn, inputs, connectionId}) instead of silently dropping it. dev-server.ts
createDevServerMiddleware takes a new loadModule: LoadModule parameter, threaded from vite/index.ts's configureServer(server) as server.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. dev-server.ts, vite/index.ts
Added a config() hook returning ssr: { noExternal: [...] } for @datadog/apps-backend/@datadog/action-catalog. vite/index.ts
Both packages ship ESM-only, and Vite's dev server externalizes node_modules by default (a plain require(), for speed) — which throws Cannot use import statement outside a module the first time a customer's function actually uses either SDK locally. vite/index.ts
noExternal forces Vite's SSR transform pipeline to handle them instead, matching how the production bundling path already inlines every dependency. vite/index.ts
Local execution's loadModule call marks its request with the existing LOCAL_EXECUTION_LOAD_SUFFIX/LOCAL_EXECUTION_LOAD_RE constants 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. local-execution.ts, vite/index.ts
collectModuleGraphFromServer runs 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. dev-server-module-graph.ts, index.ts
New regression test calling the transform handler directly with a suffixed vs. unsuffixed id — confirmed red (returned the proxy stub) against the pre-fix code, green after. index.test.ts
This is the first test in the whole stack that exercises the real transform hook for this path. index.test.ts
Extracted the loadModule test double — previously hand-rolled separately across multiple test files in this stack — into a shared moduleResolverFor helper. mocks.ts
Real end-to-end test: spins up an actual Vite dev server (createServer, middleware mode, no port bound) rooted at the same apps_backend_project fixture, and lets its real ssrLoadModule import a real .backend.ts file directly — no mocked bundler, no mocked loadModule. dev-server.integration.test.ts (rewritten)
Confirms a real @datadog/apps-backend typed import resolves $.Source correctly through this exact path. dev-server.integration.test.ts (rewritten)
New/updated unit tests cover: 400/404 for the local path with no auth configured (a function that never calls $.Actions), a clear error when a function does call $.Actions with no auth configured, the single-action preview-async request-body shape now including connectionId, and the new config() hook's ssr.noExternal contract. dev-server.test.ts, index.test.ts
Existing cloud-path tests are unchanged aside from the new loadModule parameter threaded through every createDevServerMiddleware call. dev-server.test.ts, index.test.ts
getAllowedConnectionIds's collectModuleGraphFromServer call looks up the entry node by its fully-resolved (suffixed) id, matching what loadModule actually resolved, while extractConnectionIdsFromModuleGraph still receives the bare id to match its records map's keys. vite/index.ts
A regression test drives the real middleware for a cold entry with no priming import. dev-server.integration.test.ts
bundle()'s external option 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. rollupConfig.mjs
This affects every published package's build, not just vite-plugin. rollupConfig.mjs
handleExecuteAction's module-graph priming load now goes through a new loadCustomerModuleEntry helper (shared with executeScriptLocally's own load) instead of calling loadModule directly. local-execution.ts, dev-server.ts
The priming load is the only place a customer module's top-level code actually runs — Vite caches the module, so the later load inside executeScriptLocally just reuses the resolved object — so it needs the same customerModuleLoadContext scoping, or a customer module reaching for $ during its own top-level evaluation would silently resolve to whatever $ a prior execution left behind instead of the undefined a fresh top-level access should see. local-execution.ts, dev-server.ts
Extracted three inlined function-call arguments into named locals (matching this file's own convention at every other withTimeout call site), replaced a bare as any cast in a test file with a narrower as unknown as ViteDevServer, and added a test asserting the exact startup auth-warning wording. dev-server.ts, local-execution.ts, dev-server-module-graph.test.ts, dev-server.test.ts
handleExecuteAction passes the priming load's resolved module into executeScriptLocally as its own primedEntry parameter instead of wrapping loadModule in a per-request closure — keeps loadModule the same stable reference local-execution.ts's once-ever SDK registration caches key on. local-execution.ts, dev-server.ts
dev-server-module-graph.ts reads module source via the shared @dd/core/helpers/fs readFile, matching every other file in this package, instead of importing node:fs/promises directly. dev-server-module-graph.ts
The suffixed-subgraph tracking resolveId uses to propagate LOCAL_EXECUTION_LOAD_SUFFIX through nested backend imports is now scoped to one local execution via AsyncLocalStorage (established in loadCustomerModuleEntry, alongside the existing customerModuleLoadContext), instead of a single Set shared for the dev server's whole lifetime. vite/index.ts, local-execution.ts
New tests: bundle()'s subpath-aware external matcher (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/executeActionViaCloud mirroring the existing /__dd/executeAction coverage. rollupConfig.test.ts, dev-server-module-graph.test.ts, dev-server.test.ts
pollQueryExecution's outputs check is an explicit attrs.outputs === undefined || attrs.outputs === null, not a bare falsy check — a real action result of 0, false, or '' would otherwise be misclassified as "no outputs" and thrown as an error. dev-server.ts
normalizeDevServerModuleId (renamed from normalizeViteModuleId — same name as backend-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. dev-server-module-graph.ts
Added executeColdActionLocally, which wraps priming, connection-ID collection, and execution in a single enqueue() call. local-execution.ts, dev-server.ts
handleExecuteAction previously primed the customer module and collected its connection IDs before calling executeScriptLocally, 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. local-execution.ts, dev-server.ts
resolveId's importer-suffix branch is now gated by resolveOptions.ssr === true, matching the subgraph-membership branch beside it and this hook's own comment — previously only the second branch checked ssr, so a client-mode resolution using an SSR-only suffixed id as its importer could have inherited the local-execution marker. index.ts
makeExecuteActionRemotely now checks connectionId !== undefined before including it in the outgoing query spec, matching assertConnectionIdAllowed's own check — the previous truthy check would have silently dropped a legitimate empty-string connectionId instead of forwarding it. dev-server.ts
Extracted handleHttpError, deduping the identical HttpError-to-status-code/sendError catch-block logic across handleDebugBundle, handleExecuteAction, and handleExecuteActionViaCloud. dev-server.ts
Test cleanup: local-execution.test.ts reuses its existing testDollar() helper instead of re-deriving the same globalThis cast; dev-server.test.ts now centralizes its own equivalent globalThis.$.Actions cast behind a new testDollarActions() helper instead of repeating it at each call site; createMockRequest/createMockResponse, previously duplicated verbatim across two test files, moved to the shared mocks.ts alongside moduleResolverFor. local-execution.test.ts, dev-server.test.ts, dev-server.integration.test.ts, mocks.ts
executeScriptLocally's doc comment now states plainly that it's test-only infrastructure with no production caller (verified: executeColdActionLocally is 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 what primedEntry means for the ~90 tests that call it directly. local-execution.ts
executeColdActionLocally's doc comment now names the priming/connection-ID withTimeout calls' 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. local-execution.ts
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; normalizeDevServerModuleId now composes with the existing normalizeViteModuleId for query-stripping instead of reimplementing it; extracted sendSuccess/guardAuthenticated helpers to dedupe the two endpoint branches' identical success-response and auth-gate-plus-catch code. index.ts, dev-server-module-graph.ts, dev-server.ts
Exports 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. module-graph.ts
Adds esbuild as a dependency (used by dev-server-module-graph.ts for isolated TS/JSX stripping) and promotes rollup from dev- to a runtime dependency (needed now that the rollupConfig.mjs externalization fix runs against it) across every published package, plus the corresponding lockfile update. plugins/apps/package.json, esbuild-plugin/package.json, rollup-plugin/package.json, rspack-plugin/package.json, vite-plugin/package.json, webpack-plugin/package.json, yarn.lock
New minimal @datadog/action-catalog fixture 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 into apps_backend_project via a portal dependency. action_catalog_project/action-execution.js, action_catalog_project/index.js, action_catalog_project/package.json, fixtures/package.json, fixtures/yarn.lock, apps_backend_project/package.json
New apps_backend_project fixture 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 for dev-server.integration.test.ts's real end-to-end coverage. apps_backend_project/helper.ts, apps_backend_project/nestedImport.backend.ts, apps_backend_project/viaHelper.backend.ts, apps_backend_project/mixedImports.backend.ts, apps_backend_project/actionCatalogCall.backend.ts
New apps_backend_project fixture files (helperWithBannedImport.ts, viaBannedHelper.backend.ts) give dev-server-module-graph.test.ts a helper module transitively reached through a .backend.ts entry that imports a banned Node builtin. apps_backend_project/helperWithBannedImport.ts, apps_backend_project/viaBannedHelper.backend.ts
deriveActionTimeouts now adds pollQueryExecution's worst-case retry-delay budget (shared via a new retry-delay.ts module) on top of maxRetries * timeoutMs, so the derived ceiling can't undercut a real long-poll cycle that also waits between attempts. retry-delay.ts, local-execution.ts
collectModuleGraphFromServer primes each node via server.transformRequest (resolve + transform, never executes) as it's visited, instead of relying on a prior ssrLoadModule call — so no module's top-level code can run before this function's static checks get a chance to reject it. dev-server-module-graph.ts
Module traversability (shouldTraverseCollectedModule) is now checked before the semantic-query guard, so a non-code import like ./template.html?raw is skipped like the build-time collector skips it, instead of being rejected. dev-server-module-graph.ts
extractConnectionIdsFromModuleGraph now checks connectionId !== undefined instead of truthiness, so a declared empty-string connectionId is extracted instead of silently dropped. extract-connection-ids-from-module-graph.ts
bundle()'s external matcher now invokes config.external when it's a function instead of always calling .includes() on it, fixing a TypeError for any published package whose Rollup config passes a function-shaped external. rollupConfig.mjs

QA Instructions

yarn install
yarn build:all && yarn test:unit
# Full, unscoped suite — a scoped run can't catch a process-wide guard leaking into
# an unrelated package's tests via a shared Jest worker (see the Confluence QA guide).
# Expected: Test Suites: 87 passed / Tests: 2100 passed, 1 skipped ✅ VERIFIED
# (rollupConfig.test.ts's webpack "easy project" case can fail locally on a Node version that
# emits an unrelated ExperimentalWarning about localStorage to stderr — pre-existing on master,
# unrelated to this diff, and green on CI's Node version.)
yarn workspace @dd/apps-plugin run typecheck
# Expected: no output, clean exit ✅ VERIFIED
npx 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 ✅ VERIFIED

Manual QA — real scaffolded app, real dev server (local + staging)

# 1. Build and link the plugin from this branch
cd packages/published/vite-plugin
yarn build
npm link

# 2. Scaffold a real app and link this branch's build in
npm create @datadog/apps@latest ~/apps-2792-qa-481 -- --yes
cd ~/apps-2792-qa-481
npm link @datadog/vite-plugin
cat > src/functions.backend.ts <<'EOF'
export async function doubleNumber(input: number) {
    return { doubled: input * 2 };
}
export async function logAndReturn(msg: string) {
    console.log('[qa]', msg);
    return { logged: msg };
}
export async function alwaysThrows() {
    throw new Error('deliberate QA failure');
}
EOF

# 3. Local: confirm in-process execution, no cloud round trip
npm run dev &
sleep 3
curl -s -X POST http://localhost:5173/__dd/executeAction \
  -H 'content-type: application/json' \
  -d '{"functionName":"<hash>.doubleNumber","args":[21]}'
# Expected: {"success":true,"result":{"data":{"doubled":42}}} ✅ VERIFIED
curl -s -X POST http://localhost:5173/__dd/executeAction \
  -H 'content-type: application/json' \
  -d '{"functionName":"<hash>.alwaysThrows","args":[]}'
# Expected: {"success":false,"error":"deliberate QA failure"} — clean error, not a crash ✅ VERIFIED
kill %1

(<hash> is the SHA-256-encoded query name encodeQueryName generates 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 link currently 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 packaged dist/), unrelated to this PR.
  • Re-ran the equivalent checks through the direct-source driver below instead (real createServer, real createDevServerMiddleware, real ssrLoadModule — 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's customerModuleLoadContext scoping resolves $ to undefined through the real ssrLoadModule path, matching dollarGetter's spec-correct semantics (an unresolvable $ reads as undefined, never throws).
  • All three direct-source driver scripts below (tmp-apps-2792-qa2, tmp-apps-2792-staging, tmp-apps-2792-qa) were stale after the master rebase — collectModuleGraphFromServer gained a required log 4th argument, and the auth object had a leftover method field that no longer exists on AuthOptionsWithDefaults. Fixed and re-ran all three fresh; all match documented expected output.
  • Added a timing instrumentation pass to getAllowedConnectionIds to 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 $.Actions network round trip. No caching work needed at this graph size.
mkdir -p tmp-apps-2792-qa2/src
cat > tmp-apps-2792-qa2/src/functions.backend.ts <<'EOF'
export async function doubleNumber(input: number) {
    return { doubled: input * 2 };
}
export async function alwaysThrows() {
    throw new Error('deliberate QA failure');
}
EOF
cat > tmp-apps-2792-qa2/src/coldDollar.backend.ts <<'EOF'
const outcome = typeof $;
export async function reportTopLevelDollar() {
    return { outcome };
}
EOF

cat > tmp-apps-2792-qa2/run.mjs <<'EOF'
import { createServer } from 'vite';
import { PassThrough } from 'stream';
import path from 'path';

const REPO = path.resolve(import.meta.dirname, '..');
const QA_ROOT = import.meta.dirname;

function fakeRequest(body) {
    const req = new PassThrough();
    req.method = 'POST';
    req.url = '/__dd/executeAction';
    req.headers = { 'content-type': 'application/json' };
    req.end(JSON.stringify(body));
    return req;
}
function fakeResponse() {
    const chunks = [];
    return {
        statusCode: 200, headers: {},
        setHeader(k, v) { this.headers[k] = v; },
        end(chunk) { if (chunk) chunks.push(chunk); this._body = chunks.join(''); this._resolved?.(); },
        waitForEnd() { return new Promise((r) => { if (this._body !== undefined) return r(); this._resolved = r; }); },
    };
}

const server = await createServer({
    root: QA_ROOT, configFile: false, server: { middlewareMode: true },
    logLevel: 'error', appType: 'custom', optimizeDeps: { noDiscovery: true },
});
const loadModule = server.ssrLoadModule.bind(server);
const { createDevServerMiddleware } = await loadModule(`${REPO}/packages/plugins/apps/src/vite/dev-server.ts`);
const { collectModuleGraphFromServer } = await loadModule(`${REPO}/packages/plugins/apps/src/vite/dev-server-module-graph.ts`);
const { extractConnectionIdsFromModuleGraph } = await loadModule(`${REPO}/packages/plugins/apps/src/backend/ast-parsing/extract-connection-ids-from-module-graph.ts`);
const { encodeQueryName } = await loadModule(`${REPO}/packages/plugins/apps/src/backend/encodeQueryName.ts`);
const { DEFAULT_LONG_POLLING_CONFIG } = await loadModule(`${REPO}/packages/plugins/apps/src/vite/local-execution.ts`);

const qaLog = { debug: () => {}, info: () => {}, warn: console.warn, error: console.error };

const getAllowedConnectionIds = async (entryId) =>
    extractConnectionIdsFromModuleGraph(
        entryId,
        await collectModuleGraphFromServer(server, entryId, QA_ROOT, qaLog),
        QA_ROOT,
    );

const doubleNumberFn = { name: 'doubleNumber', relativePath: 'functions.backend.ts', absolutePath: `${QA_ROOT}/src/functions.backend.ts`, allowedConnectionIds: [] };
const alwaysThrowsFn = { name: 'alwaysThrows', relativePath: 'functions.backend.ts', absolutePath: `${QA_ROOT}/src/functions.backend.ts`, allowedConnectionIds: [] };
const reportTopLevelDollarFn = { name: 'reportTopLevelDollar', relativePath: 'coldDollar.backend.ts', absolutePath: `${QA_ROOT}/src/coldDollar.backend.ts`, allowedConnectionIds: [] };

const middleware = createDevServerMiddleware(
    async () => { throw new Error('bundler.build should not be called on the local-execution path'); },
    loadModule,
    () => [doubleNumberFn, alwaysThrowsFn, reportTopLevelDollarFn],
    getAllowedConnectionIds,
    { apiKey: 'qa-fake-key', appKey: 'qa-fake-app-key', site: 'datadoghq.com' },
    async () => { throw new Error('doAuthenticatedRequest reached — should not be hit by these three functions'); },
    DEFAULT_LONG_POLLING_CONFIG,
    QA_ROOT,
    qaLog,
);

async function check(name, functionName, args) {
    const req = fakeRequest({ functionName, args });
    const res = fakeResponse();
    await middleware(req, res, (err) => { if (err) throw err; });
    await res.waitForEnd();
    console.log(`${name} -> [${res.statusCode}] ${res._body}`);
}

await check('doubleNumber (real in-process execution)', encodeQueryName(doubleNumberFn), [21]);
await check('alwaysThrows (clean error, not a crash)', encodeQueryName(alwaysThrowsFn), []);
await check('reportTopLevelDollar (cold entry, no prior priming import)', encodeQueryName(reportTopLevelDollarFn), []);

await server.close();
EOF

node tmp-apps-2792-qa2/run.mjs
rm -rf tmp-apps-2792-qa2
# Expected:
#   doubleNumber (real in-process execution) -> [200] {"success":true,"result":{"data":{"doubled":42}}}
#   alwaysThrows (clean error, not a crash) -> [500] {"success":false,"error":"deliberate QA failure"}
#   reportTopLevelDollar (cold entry, no prior priming import) -> [200] {"success":true,"result":{"data":{"outcome":"undefined"}}}
# ✅ VERIFIED

Staging (real dd-auth --domain dd.datad0g.com credentials, real preview-async request to api.datad0g.com, via the same direct-source driver wired with the real getAuthenticatedRequest() reading DD_API_KEY/DD_APP_KEY from 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:

mkdir -p tmp-apps-2792-staging/src
cat > tmp-apps-2792-staging/src/callRealAction.backend.ts <<'EOF'
export async function callRealActionEndpoint() {
    const result = await $.Actions.qa.staging.fakeAction({ inputs: {} });
    return { result };
}
EOF

cat > tmp-apps-2792-staging/run.mjs <<'EOF'
import { createServer } from 'vite';
import { PassThrough } from 'stream';
import path from 'path';

const REPO = path.resolve(import.meta.dirname, '..');
const QA_ROOT = import.meta.dirname;

function fakeRequest(body) {
    const req = new PassThrough();
    req.method = 'POST';
    req.url = '/__dd/executeAction';
    req.headers = { 'content-type': 'application/json' };
    req.end(JSON.stringify(body));
    return req;
}
function fakeResponse() {
    const chunks = [];
    return {
        statusCode: 200, headers: {},
        setHeader(k, v) { this.headers[k] = v; },
        end(chunk) { if (chunk) chunks.push(chunk); this._body = chunks.join(''); this._resolved?.(); },
        waitForEnd() { return new Promise((r) => { if (this._body !== undefined) return r(); this._resolved = r; }); },
    };
}

const server = await createServer({
    root: QA_ROOT, configFile: false, server: { middlewareMode: true },
    logLevel: 'error', appType: 'custom', optimizeDeps: { noDiscovery: true },
});
const loadModule = server.ssrLoadModule.bind(server);
const { createDevServerMiddleware } = await loadModule(`${REPO}/packages/plugins/apps/src/vite/dev-server.ts`);
const { collectModuleGraphFromServer } = await loadModule(`${REPO}/packages/plugins/apps/src/vite/dev-server-module-graph.ts`);
const { extractConnectionIdsFromModuleGraph } = await loadModule(`${REPO}/packages/plugins/apps/src/backend/ast-parsing/extract-connection-ids-from-module-graph.ts`);
const { encodeQueryName } = await loadModule(`${REPO}/packages/plugins/apps/src/backend/encodeQueryName.ts`);
const { DEFAULT_LONG_POLLING_CONFIG } = await loadModule(`${REPO}/packages/plugins/apps/src/vite/local-execution.ts`);
const { getAuthenticatedRequest } = await loadModule(`${REPO}/packages/plugins/apps/src/auth.ts`);

const log = { debug: () => {}, info: console.log, warn: console.warn, error: console.error };

const getAllowedConnectionIds = async (entryId) =>
    extractConnectionIdsFromModuleGraph(
        entryId,
        await collectModuleGraphFromServer(server, entryId, QA_ROOT, log),
        QA_ROOT,
    );

const callRealActionFn = { name: 'callRealActionEndpoint', relativePath: 'callRealAction.backend.ts', absolutePath: `${QA_ROOT}/src/callRealAction.backend.ts`, allowedConnectionIds: [] };
const auth = { apiKey: process.env.DD_API_KEY, appKey: process.env.DD_APP_KEY, site: 'datad0g.com' };
const doAuthenticatedRequest = getAuthenticatedRequest();

const middleware = createDevServerMiddleware(
    async () => { throw new Error('bundler.build should not be called on the local-execution path'); },
    loadModule,
    () => [callRealActionFn],
    getAllowedConnectionIds,
    auth,
    doAuthenticatedRequest,
    DEFAULT_LONG_POLLING_CONFIG,
    QA_ROOT,
    log,
);

async function check(name, functionName, args) {
    const req = fakeRequest({ functionName, args });
    const res = fakeResponse();
    await middleware(req, res, (err) => { if (err) throw err; });
    await res.waitForEnd();
    console.log(`${name} -> [${res.statusCode}] ${res._body}`);
}

await check('callRealActionEndpoint (real dd.datad0g.com preview-async round trip)', encodeQueryName(callRealActionFn), []);

await server.close();
EOF

dd-auth --domain dd.datad0g.com -- sh -c 'node tmp-apps-2792-staging/run.mjs'
rm -rf tmp-apps-2792-staging
# Expected: a real HTTP 400 from api.datad0g.com, e.g.
#   callRealActionEndpoint (real dd.datad0g.com preview-async round trip) -> [500]
#   {"success":false,"error":"HTTP 400 Bad Request\nstatus: 400, ... code: ACTION_NOT_FOUND, ... no action registered for ID ..."}
# ✅ VERIFIED — confirms auth headers, request submission, response parsing, and
# error surfacing all reach the real API and round-trip correctly.

Manual QA — getAllowedConnectionIds module-graph wiring, direct driver

  • npm link above goes through @datadog/vite-plugin's packaged dist/ 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 test getAllowedConnectionIds on a cold entry specifically.
  • This driver imports straight from this branch's TS source instead, sidestepping that packaging layer entirely while still exercising the real createDevServerMiddleware/collectModuleGraphFromServer code:
# 1. Real fixture backend files — placed inside this checkout (not /tmp) so the
# driver script below resolves `vite` from this repo's own node_modules.
mkdir -p tmp-apps-2792-qa/src
cat > tmp-apps-2792-qa/src/normalDouble.backend.ts <<'EOF'
export async function doubleNumber(input: number) {
    return { doubled: input * 2 };
}
EOF
cat > tmp-apps-2792-qa/src/callAction.backend.ts <<'EOF'
export async function callFakeAction() {
    const result = await $.Actions.qa.fakeAction({ inputs: { hello: 'world' } });
    return { result };
}
EOF

# 2. Driver script — real Vite server, real createDevServerMiddleware, no mocks
cat > tmp-apps-2792-qa/run.mjs <<'EOF'
import { createServer } from 'vite';
import { PassThrough } from 'stream';
import path from 'path';

const REPO = path.resolve(import.meta.dirname, '..');
const QA_ROOT = import.meta.dirname;

function fakeRequest(body) {
    const req = new PassThrough();
    req.method = 'POST';
    req.url = '/__dd/executeAction';
    req.headers = { 'content-type': 'application/json' };
    req.end(JSON.stringify(body));
    return req;
}
function fakeResponse() {
    const chunks = [];
    return {
        statusCode: 200, headers: {},
        setHeader(k, v) { this.headers[k] = v; },
        end(chunk) { if (chunk) chunks.push(chunk); this._body = chunks.join(''); this._resolved?.(); },
        waitForEnd() { return new Promise((r) => { if (this._body !== undefined) return r(); this._resolved = r; }); },
    };
}

const server = await createServer({
    root: QA_ROOT, configFile: false, server: { middlewareMode: true },
    logLevel: 'error', appType: 'custom', optimizeDeps: { noDiscovery: true },
});
const loadModule = server.ssrLoadModule.bind(server);
const { createDevServerMiddleware } = await loadModule(`${REPO}/packages/plugins/apps/src/vite/dev-server.ts`);
const { collectModuleGraphFromServer } = await loadModule(`${REPO}/packages/plugins/apps/src/vite/dev-server-module-graph.ts`);
const { extractConnectionIdsFromModuleGraph } = await loadModule(`${REPO}/packages/plugins/apps/src/backend/ast-parsing/extract-connection-ids-from-module-graph.ts`);
const { LOCAL_EXECUTION_LOAD_SUFFIX } = await loadModule(`${REPO}/packages/plugins/apps/src/constants.ts`);
const { encodeQueryName } = await loadModule(`${REPO}/packages/plugins/apps/src/backend/encodeQueryName.ts`);
const { DEFAULT_LONG_POLLING_CONFIG } = await loadModule(`${REPO}/packages/plugins/apps/src/vite/local-execution.ts`);

const qaLog = { debug: console.log, info: console.log, warn: console.log, error: console.log };

// Exactly how vite/index.ts's configureServer wires it — not a mock.
const getAllowedConnectionIds = async (entryId) =>
    extractConnectionIdsFromModuleGraph(
        entryId,
        await collectModuleGraphFromServer(server, entryId, QA_ROOT, qaLog),
        QA_ROOT,
    );

const doubleNumberFn = { name: 'doubleNumber', relativePath: 'normalDouble.backend.ts', absolutePath: `${QA_ROOT}/src/normalDouble.backend.ts`, allowedConnectionIds: [] };
const callFakeActionFn = { name: 'callFakeAction', relativePath: 'callAction.backend.ts', absolutePath: `${QA_ROOT}/src/callAction.backend.ts`, allowedConnectionIds: [] };

const middleware = createDevServerMiddleware(
    async () => { throw new Error('bundler.build should not be called on the local-execution path'); },
    loadModule,
    () => [doubleNumberFn, callFakeActionFn],
    getAllowedConnectionIds,
    { apiKey: 'qa-fake-key', appKey: 'qa-fake-app-key', site: 'datadoghq.com' },
    async () => { throw new Error('doAuthenticatedRequest reached — proves the call was routed all the way to auth, not a stub'); },
    DEFAULT_LONG_POLLING_CONFIG,
    QA_ROOT,
    qaLog,
);

async function check(name, functionName, args) {
    const req = fakeRequest({ functionName, args });
    const res = fakeResponse();
    await middleware(req, res, (err) => { if (err) throw err; });
    await res.waitForEnd();
    console.log(`${name} -> [${res.statusCode}]`, res._body);
}

await check('doubleNumber via real middleware', encodeQueryName(doubleNumberFn), [21]);
await check('callFakeAction via real middleware (auth stub throws by design)', encodeQueryName(callFakeActionFn), []);

// The specific mechanism this PR's fix touches: a COLD entry, no priming import
// beforehand — the exact condition that requires collectModuleGraphFromServer to
// find the module Vite just registered under its suffixed id.
await server.ssrLoadModule(`${doubleNumberFn.absolutePath}${LOCAL_EXECUTION_LOAD_SUFFIX}`);
console.log('Module-graph connectionId extraction:', JSON.stringify(await getAllowedConnectionIds(doubleNumberFn.absolutePath)));

await server.close();
EOF

# 3. Run it from the repo root of this branch
node tmp-apps-2792-qa/run.mjs
# Expected:
#   doubleNumber via real middleware -> [200] {"success":true,"result":{"data":{"doubled":42}}}
#   callFakeAction via real middleware (auth stub throws by design) -> [500] {"success":false,"error":"doAuthenticatedRequest reached — proves the call was routed all the way to auth, not a stub"}
#   Module-graph connectionId extraction: []
# ✅ VERIFIED — against the pre-fix code, the last line instead threw
# "Unsupported local module graph ... missing module record for <entry> could hide an action-catalog connectionId"

# 4. Clean up
rm -rf tmp-apps-2792-qa

A durable writeup of this QA flow (including the local↔staging↔app-builder-code architecture) is in the Confluence QA guide.

Blast Radius

  • First PR in the stack with customer-visible behavior: npm run dev's /__dd/executeAction now 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 (no version bump, no bump.yaml trigger in this PR).
  • The cloud round trip is fully preserved, just moved to a new URL (/__dd/executeActionViaCloud) — no live caller exists yet on /__dd/executeAction since this endpoint isn't released.
  • ssr.noExternal affects 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_SUFFIX only special-cases requests carrying that exact marker — no behavior change for any existing frontend import of a .backend.ts file.
  • The rollupConfig.mjs externalization fix touches the build of all five published packages (esbuild-plugin, rollup-plugin, rspack-plugin, vite-plugin, webpack-plugin), not just vite-plugin.
    • Strictly more correct: a declared dependency's subpath imports are now externalized like its bare specifier already was.
    • yarn build:all plus the full rollupConfig.test.ts bundling suite pass clean for every package.
  • esbuild is now a real (not dev) dependency of all five published packages — dev-server-module-graph.ts uses esbuild.transform to 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.
  • Risk: medium — this is the PR that flips the execution model for any consumer of this endpoint once released, even though today there is none.
    • The bug this PR fixes was a hard blocker for the whole feature working at all, so shipping it fixed is the main risk this PR retires, not one it introduces.
  • A follow-on network/subprocess guard now stacks directly on this branch rather than sitting as a sibling of the prior local-execution work — both independently need the same LOCAL_EXECUTION_LOAD_SUFFIX call-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
Item Status Next step
npm run dev:verify CLI (mode-aware routing to /__dd/executeActionViaCloud, web-ui template changes) In progress Split across a follow-up build-plugins PR and a companion web-ui PR
Real manual QA against a scaffolded app Done See QA Instructions above
npm link @datadog/vite-plugin against a real scaffolded app currently fails (ERR_MODULE_NOT_FOUND on a workspace-linked source import, packages/factory/src/validate, that Node's native ESM loader can't resolve through the packaged dist/) Pre-existing, unrelated to this PR Same class of packaging issue the Confluence QA guide already documents for the getAllowedConnectionIds driver section below; worth a dedicated fix so the scaffolded-app QA path in this PR's own instructions works again
A genuine local @datadog/action-catalog fixture package for a typed-import e2e test Deferred Reasonable, cheap follow-up — not required for this coverage to be meaningful, since both SDKs funnel through the identical $.Actions routing
A customer's own additional Vite plugin (added to their own vite.config.ts — a real, hand-editable file, not something App Builder generates or hides) can register a load/transform hook that rewrites a .backend.ts-reachable file; dev-server-module-graph.ts's connection-ID collector reads that file fresh off disk plus an isolated esbuild.transform, not through Vite's full plugin pipeline, so a call the plugin's rewrite injects is invisible to the allowlist calculation Deferred Customer-reachable today, but fails safe — the call gets rejected with a clear allowlist error, not leaked, and needs the customer's plugin to both target a backend-reachable file and inject action-catalog-relevant code specifically. A real fix means teaching collectActionCatalogImports to also parse Vite's SSR-rewritten __vite_ssr_import__ call syntax (server.transformRequest's actual output), not just plain ImportDeclaration — real parser work, tracked as a follow-up rather than folded into this pass
handleExecuteAction's local-execution path only runs runBackendStaticChecks (banned Node built-ins, restricted globals) against the .backend.ts entry file, not a nested helper file it imports — unlike the cloud/build path, which registers createBackendStaticChecksPlugin against 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's buildBackendFunctions, used by both closeBundle and the cloud-preview path), so this has no production-security impact — it's a local-npm run dev DX gap, not a bypass Deferred A real fix means reusing collectModuleGraphFromServer's dev-server graph walk to run runBackendStaticChecks against 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)
A plain (non-.backend.*) helper module reached during a local execution's traversal is tracked in an AsyncLocalStorage-scoped Set for resolveId'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 calls ssrLoadModule against 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 plugin Deferred This is a real gap. The architecturally sound fix is to give every app-local module in a local-execution traversal a distinct identity, while leaving node_modules/SDK packages shared — but it likely also removes the current subgraphImporters Set and needs new tests for a scenario nothing today can trigger, so tracked as a follow-up rather than folded into this pass
executeColdActionLocally's priming and connection-ID-resolution steps are bounded by withTimeout, which doesn't cancel the underlying work — if either step legitimately exceeds timeoutMs, 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 prevent Accepted, documented trade-off Same "abandoned, not canceled" model this file already accepts for execution itself (see executionEpoch) — real cancellation isn't available for loadModule/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 risk
resolveId's nested-backend-import detection uses the no-query BACKEND_FILE_RE, while transform's own inclusion filter tolerates a trailing Vite resource query via BACKEND_FILE_WITH_QUERY_RE. A .backend.ts file importing another with an explicit resource query (e.g. ./other.backend?raw) resolves to an id BACKEND_FILE_RE doesn't match, so the suffix is never appended and the nested import falls back to the frontend proxy stub instead of real code Deferred Narrow: requires a customer to attach a resource query to a nested backend-to-backend import specifically, an unusual thing to do since .backend.ts files 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 patch

Documentation

@datadog-official

datadog-official Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tests

All CI checks and tests passed.

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: b7cc3d7 | Docs | View more details | Give us feedback!

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch from 6e85225 to ae53df1 Compare August 7, 2026 20:24
tyffical added a commit that referenced this pull request Aug 11, 2026
…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.
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch from 7cbbeec to d976f85 Compare August 20, 2026 22:14
tyffical added a commit that referenced this pull request Aug 20, 2026
…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.
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch from d976f85 to 1900a78 Compare August 20, 2026 23:16
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch from 1900a78 to a0bcc4f Compare August 20, 2026 23:38
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch from a0bcc4f to dc33400 Compare August 21, 2026 03:52
@tyffical
tyffical requested a balanced review from Copilot August 21, 2026 16:24
@DataDog DataDog deleted a comment from chatgpt-codex-connector Bot Aug 21, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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/executeAction locally 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.

Comment thread packages/plugins/apps/src/vite/index.ts Outdated
Comment thread packages/plugins/apps/src/vite/dev-server.ts Outdated
chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

chatgpt-codex-connector[bot]

This comment was marked as resolved.

This comment was marked as resolved.

@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch from 4578c5c to c2676bc Compare August 24, 2026 16:41
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch from 094c394 to 9507db8 Compare August 27, 2026 20:32
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch 2 times, most recently from 6fc021f to d5751a4 Compare August 27, 2026 21:43
@tyffical
tyffical force-pushed the tiffany.trinh/apps-2792-wire-into-dev-server branch 2 times, most recently from 05c85bc to 8a73a78 Compare August 27, 2026 22:43

This comment was marked as resolved.

This comment was marked as resolved.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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(

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge 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 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

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.

Comment thread packages/plugins/apps/src/vite/index.ts Outdated
@tyffical

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-02T21:32:25.288783Z f05b74f Manual request
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 29 out of 31 changed files in this pull request and generated 3 comments.

Comment thread packages/plugins/apps/src/vite/dev-server.ts Outdated
Comment thread packages/plugins/apps/src/vite/dev-server-module-graph.ts Outdated
Comment thread packages/plugins/apps/src/vite/index.ts Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Swish!

Reviewed commit: 399f7a343f

ℹ️ 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".

@tyffical

tyffical commented Sep 1, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread packages/plugins/apps/src/vite/local-execution.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 29 out of 31 changed files in this pull request and generated 2 comments.

Comment thread packages/plugins/apps/src/vite/dev-server-module-graph.ts
Comment thread packages/plugins/apps/src/vite/local-execution.ts Outdated
@tyffical

tyffical commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread packages/plugins/apps/src/vite/local-execution.ts Outdated
Comment thread packages/plugins/apps/src/vite/local-execution.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

Comment thread packages/plugins/apps/src/vite/local-execution.ts Outdated
Comment thread packages/tools/src/rollupConfig.mjs Outdated
Comment thread packages/plugins/apps/src/vite/dev-server-module-graph.ts Outdated
Comment thread packages/plugins/apps/src/vite/dev-server.ts
Comment thread packages/plugins/apps/src/vite/local-execution.ts
oliverli

This comment was marked as outdated.

@tyffical

tyffical commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

@codex review
@copilot review

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 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

Comment thread packages/plugins/apps/src/vite/local-execution.ts Outdated
Comment thread packages/tools/src/rollupConfig.mjs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 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".

Comment thread packages/plugins/apps/src/vite/local-execution.ts
tyffical and others added 3 commits September 2, 2026 17:37
Squashed from 8 commits (see git reflog b84888d for prior history) ahead of
rebasing onto master's upload/publish removal (PR #494).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
@tyffical

tyffical commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351 Bot commented Sep 2, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-09-02 22:08:53 UTC ℹ️ Start processing command /merge


2026-09-02 22:08:59 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 2m (p90).


2026-09-02 22:10:23 UTC ℹ️ MergeQueue: This merge request was merged

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants