[APPS-2792] Add: reject Node built-in imports in backend files - #476
[APPS-2792] Add: reject Node built-in imports in backend files#476tyffical wants to merge 6 commits into
Conversation
84b9e52 to
ec0f520
Compare
ec0f520 to
e17c9c8
Compare
There was a problem hiding this comment.
Pull request overview
Friend, this PR adds build-time restrictions for unsupported Node built-ins and network globals in backend functions.
Changes:
- Adds AST validation for Node built-in imports and restricted globals.
- Integrates validation into the Vite backend transform.
- Adds unit and transform-level tests.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
packages/plugins/apps/src/vite/index.ts |
Runs backend restrictions during transformation. |
packages/plugins/apps/src/vite/index.test.ts |
Tests transform-level built-in rejection. |
packages/plugins/apps/src/backend/ast-parsing/reject-restricted-globals.ts |
Detects unresolved restricted globals. |
packages/plugins/apps/src/backend/ast-parsing/reject-restricted-globals.test.ts |
Tests global detection and shadowing. |
packages/plugins/apps/src/backend/ast-parsing/reject-node-builtin-imports.ts |
Detects Node built-in imports. |
packages/plugins/apps/src/backend/ast-parsing/reject-node-builtin-imports.test.ts |
Tests import restrictions and exceptions. |
Suppressed comments (2)
packages/plugins/apps/src/backend/ast-parsing/reject-node-builtin-imports.ts:40
- The suggested remedy is inaccurate for non-privileged built-ins such as
path,util, orevents: an Action Platform action is not a replacement for those APIs. Mention standard JavaScript or a runtime-neutral package for portable functionality, reserving the Action Platform guidance for privileged operations, so the error remains actionable for every module this guard rejects.
`Backend functions run in a restricted environment and must use an Action ` +
`Platform action ($.Actions or an @datadog/action-catalog typed wrapper) instead: ${filePath}`,
packages/plugins/apps/src/backend/ast-parsing/reject-node-builtin-imports.test.ts:84
- Repository guidance disallows passing a function call directly into another call. Store the import declaration first so this test follows that rule.
const ast = program([importDecl(source)]);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
f9ef9c7 to
5de2a92
Compare
…nd files Static AST checks run against every *.backend.ts file (and its nested backend-module imports) at build/dev-server time: importing a Node built-in, or referencing a network global (fetch, XMLHttpRequest, etc.) directly or via globalThis, fails the build with a clear error instead of surfacing as a runtime failure inside Datadog's execution sandbox.
Closes a series of edge cases found through iterative review of the Node-builtin-import and restricted-globals checks: global-alias and dynamic-import bypasses, template-literal and default-parameter destructuring bypasses, a quoted-key bypass, and a globalThis.globalThis self-reference bypass. Also adds warnAboutDivergentGlobals for crypto/Intl, extracts a shared ambient-global-access traversal reused by the reject/warn checks, wires a nested backend-static-checks-plugin that re-runs all checks against the whole module graph (not just the entry file), fixes a temp-dir cleanup failure masking a real build error, and clarifies error wording for built-in-import rejections.
…s plugins backend-static-checks-plugin.ts copied VIRTUAL_MODULE_ID_RE, normalizeViteModuleId, and isViteVirtualModuleId verbatim from its sibling backend-module-graph-collector.ts instead of importing them, so a future change to virtual-id detection would need to be applied in both places. Also computes scopeAnalysis once in the fallback (no cached record) branch instead of leaving it uncomputed, so rejectRestrictedGlobals and warnAboutDivergentGlobals stop each independently re-walking the same AST.
rejectRestrictedGlobals and warnAboutDivergentGlobals each computed their own eslint-scope analysis of the same AST on every backend-file transform (including every dev-server HMR re-transform), despite both already accepting a precomputed one to avoid exactly this.
…call argument Replaces the eslint-scope-vs-estree as-cast in ambient-global-access.ts with a genuine type guard (isVariableDeclaratorNode), and rewrites type-guards.ts's own as-casts (isStringLiteral, isNoSubstitutionTemplateLiteral) to narrow via the `in` operator instead. Also un-inlines a parseAst(...) call passed directly as a createParsedModuleRecord argument in backend-static-checks-plugin.test.ts. These were previously left as-is after a bot flagged them, on the mistaken belief that no repo convention banned the pattern; the repo-root CLAUDE.md does.
90a0438 to
4480458
Compare
|
@codex review |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 19 out of 19 changed files in this pull request and generated 3 comments.
Suppressed comments (3)
packages/plugins/apps/src/vite/build-backend-functions.test.ts:52
- This double assertion bypasses type checking for the mocked build function. Define it against
typeof builddirectly so the test remains checked against Vite's build contract.
failingViteBuild as unknown as typeof build,
packages/plugins/apps/src/vite/build-backend-functions.test.ts:75
- This double assertion bypasses type checking for the mocked build function. Define it against
typeof builddirectly so the test remains checked against Vite's build contract.
failingViteBuild as unknown as typeof build,
packages/plugins/apps/src/vite/build-backend-functions.test.ts:99
- This double assertion bypasses type checking for the mocked build function. Define it against
typeof builddirectly so the test remains checked against Vite's build contract.
failingViteBuild as unknown as typeof build,
| handler(code, id) { | ||
| const ast = this.parse(code); | ||
| // Shared so rejectRestrictedGlobals/warnAboutDivergentGlobals don't each independently re-walk the same AST to build the same scope graph. | ||
| const scopeAnalysis = analyzeModuleScope(ensureProgram(ast, id)); |
There was a problem hiding this comment.
Fixed in bca0b05 — assigned ensureProgram(...) to a named local before passing it to analyzeModuleScope.
| throw new Error( | ||
| `Importing Node built-in module "${value}" is not supported in backend function code. ` + |
There was a problem hiding this comment.
Fixed the PR description — updated all 3 grep patterns/expected outputs to match the actual emitted message ("not supported in backend function code"), and refreshed the stale test-count claim (28 suites / 399 tests) while I was in there.
|
Codex Review: Didn't find any major issues. Chef's kiss. 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". |
Two call sites passed ensureProgram(...) directly as analyzeModuleScope's argument, violating the repo's no-inlined-function-call-arguments convention.
Motivation
fetch) — everything must go through an Action Platform action ($.Actionsor an@datadog/action-catalogtyped wrapper).fs,child_process,net, etc.) in.backend.tsfiles are now rejected at build time, so an author gets immediate feedback instead of code that silently breaks once local Node execution lands.fetch,XMLHttpRequest,WebSocket,EventSource) need a separate check, since they're bare globals rather than imports.fetchworks fine during local dev today but fails once published, since production's sandbox blocks it. Node's ownglobalalias reaches the same globals and is treated identically.crypto/Intlbehave identically enough to work in both runtimes, but aren't guaranteed identical (RNG implementation, bundled ICU data) — part of the RFC's prod-parity divergence list.npm run dev:verify's real cloud round trip as the actual parity gate.fetchin the first place. That reduces how often it's written, but only this build-time check guarantees it never ships — both layers exist for a reason.$that omit Node-specific types) is deferred — see Out of Scope.Architecture
Three checks run in the Vite transform hook right after
this.parse(code); two of them (the reject-style checks for restricted globals) share one AST traversal so a bypass fix lands once, not twice. A nested plugin re-runs all three against the whole backend module graph, not just the entry file.Changes
rejectNodeBuiltinImports, which walks a.backend.tsfile's staticImportDeclarations and throws if any source is a Node built-in (vianode:prefix or Node's ownbuiltinModuleslist).import('node:fs'), which Rollup represents as anImportExpressionrather than a top-levelImportDeclaration.fetchitself is blocked too.rejectRestrictedGlobals, an eslint-scope-based check that throws on any unshadowed reference tofetch/XMLHttpRequest/WebSocket/EventSource— i.e. one that doesn't resolve to a local declaration or import of the same name, so it falls through to the real ambient global.globalalias the same asglobalThis, in both qualified-access forms (global.fetch, destructuring offglobal).warnAboutDivergentGlobals, which warns (never rejects) oncrypto/Intlreferences — bare,globalThis-qualified, or destructured — deduped once per distinct global per file.rejectRestrictedGlobalsalready had; its per-file dedup cache is now bounded so a long dev-server session can't grow it forever.forEachAmbientGlobalAccess, a shared traversal for every syntactic form that reachesglobalThis/global(bare reference, qualified member access, destructuring), used by both reject/warn checks so a bypass fix lands once instead of drifting between two hand-mirrored copies.globalThis[\`fetch\`]).constalias ofglobalThis/global, including a chain of aliases (e.g.const x = globalThis; const y = x;), so the ambient-global identity is still recognized after being reassigned to a new name.this.parse(code)and before export extraction.createBackendStaticChecksPlugin, a nested Vite plugin that re-runs all three static checks against every app-local module the backend build resolves — not just the.backend.tsentry — so an imported helper can't ship an undetected violation.node:fs, barefs,child_process,net,fs/promises), and edge cases (type-only imports, non-import statements).fetch()calls, referencingfetchwithout calling it,new XMLHttpRequest()/WebSocket()/EventSource()) and allowed cases (an imported action-catalog function, a locally-declared function/parameter namedfetch— shadowing-safe)..backend.tsfile with anode:fsimport through the actual transform handler (using rollup's realparseAst, not a hand-built AST) to confirm the rejection fires through the genuine pipeline.QA Instructions
Build the plugin and link it into a scratch Vite project, then confirm a backend file importing a Node built-in — or referencing
fetch— is rejected while an ordinary backend file still transforms correctly.Blast Radius
.backend.tsfiles and the local helper modules they import. No feature flag — production's real sandbox already blocks both restricted patterns, so these are new build-time errors for code that wasn't usable in production anyway;crypto/Intlonly ever warn, never fail a build.importspecifiers and a dynamicimport()with a string/template-literal specifier — notrequire()or a runtime-computed specifier.globalThis/global-qualified access (including aconstalias chain), and destructuring — not a reference reached through a reassignable (let) binding or a fully dynamic computed key.crypto/Intlget an additional warn-level log line only..backend.tsfile that also imports a banned module now hard-fails instead of being silently warned-and-stripped — catching the banned pattern as soon as it's written.Out of Scope / Follow-ups
backend-function-globals.d.ts(ambient TypeScript type for$that omitsDeno/process/Node-builtin globals).d.tsinto the publisheddist/tarball requires new build-tooling wiring inpackages/tools/src/rollupConfig.mjs(shared by all 5 published bundler plugins), which is disproportionate scope for this PR. Revisit once a scaffold tool exists to actually wire the type into a consumer'stsconfig.json.fetch; not blocking today's v1 rolloutDocumentation