From e869c8d748ffd05c6948ffabf8dfaf51b0982270 Mon Sep 17 00:00:00 2001 From: Scott Kennedy Date: Thu, 7 May 2026 10:25:52 -0400 Subject: [PATCH] Refactor backend discovery AST parsing --- .../extract-backend-functions.ts} | 60 +++++++++++++------ .../src/backend/ast-parsing/type-guards.ts | 10 ++++ .../apps/src/backend/ast-parsing/types.ts | 18 ++++++ .../apps/src/backend/discovery.test.ts | 2 +- .../apps/src/backend/encodeQueryName.ts | 2 +- packages/plugins/apps/src/backend/types.ts | 14 +++++ packages/plugins/apps/src/index.ts | 4 +- .../apps/src/vite/build-backend-functions.ts | 2 +- .../plugins/apps/src/vite/dev-server.test.ts | 2 +- packages/plugins/apps/src/vite/dev-server.ts | 2 +- packages/plugins/apps/src/vite/index.test.ts | 2 +- packages/plugins/apps/src/vite/index.ts | 2 +- 12 files changed, 92 insertions(+), 28 deletions(-) rename packages/plugins/apps/src/backend/{discovery.ts => ast-parsing/extract-backend-functions.ts} (84%) create mode 100644 packages/plugins/apps/src/backend/ast-parsing/type-guards.ts create mode 100644 packages/plugins/apps/src/backend/ast-parsing/types.ts create mode 100644 packages/plugins/apps/src/backend/types.ts diff --git a/packages/plugins/apps/src/backend/discovery.ts b/packages/plugins/apps/src/backend/ast-parsing/extract-backend-functions.ts similarity index 84% rename from packages/plugins/apps/src/backend/discovery.ts rename to packages/plugins/apps/src/backend/ast-parsing/extract-backend-functions.ts index 5a74719cb..ac60c5664 100644 --- a/packages/plugins/apps/src/backend/discovery.ts +++ b/packages/plugins/apps/src/backend/ast-parsing/extract-backend-functions.ts @@ -5,16 +5,8 @@ import type { Declaration, Expression, Program } from 'estree'; import type { AstNode } from 'rollup'; -export interface BackendFunction { - /** Relative path from project root to the .backend.ts file (without extension) */ - relativePath: string; - /** Exported function name */ - name: string; - /** Absolute path to the .backend.ts source file */ - absolutePath: string; - /** Connection IDs this backend function is allowed to use. */ - allowedConnectionIds: string[]; -} +import { isProgramNode } from './type-guards'; +import type { BackendExport } from './types'; /** * Extract exported value (non-type) symbols from an ESTree AST. @@ -27,11 +19,11 @@ export interface BackendFunction { * @param ast - AstNode from `this.parse()` in unplugin's transform hook * @param filePath - Path to the source file (used in error messages) */ -function isProgramNode(node: AstNode): node is AstNode & Program { - return node.type === 'Program'; +export function extractExportedFunctions(ast: AstNode, filePath: string): string[] { + return enumerateBackendExports(ast, filePath).map((backendExport) => backendExport.name); } -export function extractExportedFunctions(ast: AstNode, filePath: string): string[] { +export function enumerateBackendExports(ast: AstNode, filePath: string): BackendExport[] { if (!isProgramNode(ast)) { throw new Error( `Expected a Program node from this.parse() for ${filePath}, got ${ast.type}`, @@ -41,7 +33,7 @@ export function extractExportedFunctions(ast: AstNode, filePath: string): string // Build a map of top-level declarations so we can validate export specifiers. const declarations = buildDeclarationMap(ast); - const names: string[] = []; + const backendExports: BackendExport[] = []; for (const node of ast.body) { // handles: export default ... if (node.type === 'ExportDefaultDeclaration') { @@ -61,9 +53,16 @@ export function extractExportedFunctions(ast: AstNode, filePath: string): string // handles: export function add() {} / export const add = ... if (node.declaration) { - names.push(...namesFromDeclaration(node.declaration, filePath)); + backendExports.push( + ...namesFromDeclaration(node.declaration, filePath).map((name) => ({ + kind: 'local' as const, + name, + localName: name, + })), + ); } + const source = typeof node.source?.value === 'string' ? node.source.value : null; for (const spec of node.specifiers) { if (spec.exported.type !== 'Identifier') { continue; @@ -77,14 +76,37 @@ export function extractExportedFunctions(ast: AstNode, filePath: string): string // Validate specifier binding is callable when we can resolve it. // e.g. `const VERSION = '1.0'; export { VERSION };` — rejected // e.g. `function add() {}; export { add };` — allowed + const localName = + spec.local.type === 'Identifier' + ? spec.local.name + : typeof spec.local.value === 'string' + ? spec.local.value + : null; + if (!localName) { + continue; + } + + if (source) { + backendExports.push({ + kind: 're-export', + name: spec.exported.name, + localName, + source, + }); + continue; + } + if (spec.local.type === 'Identifier') { - validateSpecifierBinding(spec.local.name, declarations, filePath); + validateSpecifierBinding(localName, declarations, filePath); } - // handles: export { add, multiply } - names.push(spec.exported.name); + backendExports.push({ + kind: 'local', + name: spec.exported.name, + localName, + }); } } - return names; + return backendExports; } /** Init types that are definitively non-callable at runtime. */ diff --git a/packages/plugins/apps/src/backend/ast-parsing/type-guards.ts b/packages/plugins/apps/src/backend/ast-parsing/type-guards.ts new file mode 100644 index 000000000..7a5d3205c --- /dev/null +++ b/packages/plugins/apps/src/backend/ast-parsing/type-guards.ts @@ -0,0 +1,10 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +import type { Program } from 'estree'; +import type { AstNode } from 'rollup'; + +export function isProgramNode(node: AstNode): node is AstNode & Program { + return node.type === 'Program'; +} diff --git a/packages/plugins/apps/src/backend/ast-parsing/types.ts b/packages/plugins/apps/src/backend/ast-parsing/types.ts new file mode 100644 index 000000000..bc4ab5a69 --- /dev/null +++ b/packages/plugins/apps/src/backend/ast-parsing/types.ts @@ -0,0 +1,18 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +export type BackendExport = BackendLocalExport | BackendReExport; + +export interface BackendLocalExport { + kind: 'local'; + name: string; + localName: string; +} + +export interface BackendReExport { + kind: 're-export'; + name: string; + localName: string; + source: string; +} diff --git a/packages/plugins/apps/src/backend/discovery.test.ts b/packages/plugins/apps/src/backend/discovery.test.ts index f521b6716..4698c560c 100644 --- a/packages/plugins/apps/src/backend/discovery.test.ts +++ b/packages/plugins/apps/src/backend/discovery.test.ts @@ -2,7 +2,7 @@ // This product includes software developed at Datadog (https://www.datadoghq.com/). // Copyright 2019-Present Datadog, Inc. -import { extractExportedFunctions } from '@dd/apps-plugin/backend/discovery'; +import { extractExportedFunctions } from '@dd/apps-plugin/backend/ast-parsing/extract-backend-functions'; import type { Program } from 'estree'; import type { AstNode } from 'rollup'; diff --git a/packages/plugins/apps/src/backend/encodeQueryName.ts b/packages/plugins/apps/src/backend/encodeQueryName.ts index 1b778a55d..6bc9ede7b 100644 --- a/packages/plugins/apps/src/backend/encodeQueryName.ts +++ b/packages/plugins/apps/src/backend/encodeQueryName.ts @@ -5,7 +5,7 @@ import { createHash } from 'crypto'; import path from 'path'; -import type { BackendFunction } from './discovery'; +import type { BackendFunction } from './types'; /** * Encode a BackendFunction into an opaque query name string. diff --git a/packages/plugins/apps/src/backend/types.ts b/packages/plugins/apps/src/backend/types.ts new file mode 100644 index 000000000..edce3b5f6 --- /dev/null +++ b/packages/plugins/apps/src/backend/types.ts @@ -0,0 +1,14 @@ +// Unless explicitly stated otherwise all files in this repository are licensed under the MIT License. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2019-Present Datadog, Inc. + +export interface BackendFunction { + /** Relative path from project root to the .backend.ts file (without extension) */ + relativePath: string; + /** Exported function name */ + name: string; + /** Absolute path to the .backend.ts source file */ + absolutePath: string; + /** Connection IDs this backend function is allowed to use. */ + allowedConnectionIds: string[]; +} diff --git a/packages/plugins/apps/src/index.ts b/packages/plugins/apps/src/index.ts index 9b54e625c..1299947de 100644 --- a/packages/plugins/apps/src/index.ts +++ b/packages/plugins/apps/src/index.ts @@ -13,10 +13,10 @@ import path from 'path'; import { createArchive } from './archive'; import type { Asset } from './assets'; import { collectAssets } from './assets'; -import type { BackendFunction } from './backend/discovery'; -import { extractExportedFunctions } from './backend/discovery'; +import { extractExportedFunctions } from './backend/ast-parsing/extract-backend-functions'; import { encodeQueryName } from './backend/encodeQueryName'; import { generateProxyModule } from './backend/proxy-codegen'; +import type { BackendFunction } from './backend/types'; import { BACKEND_FILE_RE, CONFIG_KEY, PLUGIN_NAME } from './constants'; import { resolveIdentifier } from './identifier'; import type { AppsManifest, AppsOptions } from './types'; diff --git a/packages/plugins/apps/src/vite/build-backend-functions.ts b/packages/plugins/apps/src/vite/build-backend-functions.ts index e5f80a365..1fc36ab4d 100644 --- a/packages/plugins/apps/src/vite/build-backend-functions.ts +++ b/packages/plugins/apps/src/vite/build-backend-functions.ts @@ -8,8 +8,8 @@ import { tmpdir } from 'os'; import path from 'path'; import type { build } from 'vite'; -import type { BackendFunction } from '../backend/discovery'; import { encodeQueryName } from '../backend/encodeQueryName'; +import type { BackendFunction } from '../backend/types'; import { generateVirtualEntryContent } from '../backend/virtual-entry'; import { getBaseBackendBuildConfig } from './build-config'; diff --git a/packages/plugins/apps/src/vite/dev-server.test.ts b/packages/plugins/apps/src/vite/dev-server.test.ts index 4592e9437..19ed7b539 100644 --- a/packages/plugins/apps/src/vite/dev-server.test.ts +++ b/packages/plugins/apps/src/vite/dev-server.test.ts @@ -8,8 +8,8 @@ import { EventEmitter } from 'events'; import type { IncomingMessage, ServerResponse } from 'http'; import nock from 'nock'; -import type { BackendFunction } from '../backend/discovery'; import { encodeQueryName } from '../backend/encodeQueryName'; +import type { BackendFunction } from '../backend/types'; const mockViteBuild = jest.fn(); diff --git a/packages/plugins/apps/src/vite/dev-server.ts b/packages/plugins/apps/src/vite/dev-server.ts index d3d8a2821..8d033d306 100644 --- a/packages/plugins/apps/src/vite/dev-server.ts +++ b/packages/plugins/apps/src/vite/dev-server.ts @@ -10,9 +10,9 @@ import { randomUUID } from 'crypto'; import type { IncomingMessage, ServerResponse } from 'http'; import type { build } from 'vite'; -import type { BackendFunction } from '../backend/discovery'; import { encodeQueryName } from '../backend/encodeQueryName'; import type { ExecuteActionRequest, ExecuteActionResponse } from '../backend/protocol'; +import type { BackendFunction } from '../backend/types'; import { generateDevVirtualEntryContent } from '../backend/virtual-entry'; import { getBaseBackendBuildConfig } from './build-config'; diff --git a/packages/plugins/apps/src/vite/index.test.ts b/packages/plugins/apps/src/vite/index.test.ts index b39465640..895b1c0d6 100644 --- a/packages/plugins/apps/src/vite/index.test.ts +++ b/packages/plugins/apps/src/vite/index.test.ts @@ -5,8 +5,8 @@ import { getVitePlugin } from '@dd/apps-plugin/vite/index'; import { getMockLogger } from '@dd/tests/_jest/helpers/mocks'; -import type { BackendFunction } from '../backend/discovery'; import { encodeQueryName } from '../backend/encodeQueryName'; +import type { BackendFunction } from '../backend/types'; const log = getMockLogger(); diff --git a/packages/plugins/apps/src/vite/index.ts b/packages/plugins/apps/src/vite/index.ts index b35658349..ad0ec375f 100644 --- a/packages/plugins/apps/src/vite/index.ts +++ b/packages/plugins/apps/src/vite/index.ts @@ -6,7 +6,7 @@ import { rm } from '@dd/core/helpers/fs'; import type { AuthOptionsWithDefaults, Logger, PluginOptions } from '@dd/core/types'; import type { build } from 'vite'; -import type { BackendFunction } from '../backend/discovery'; +import type { BackendFunction } from '../backend/types'; import { buildBackendFunctions } from './build-backend-functions'; import { createDevServerMiddleware } from './dev-server';