Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/plugins/apps/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ export const getPlugins: GetPlugins = ({ options, context, bundler }) => {
const absoluteBackendDir = path.resolve(context.buildRoot, validatedOptions.backendDir);
const backendFunctions = discoverBackendFunctions(absoluteBackendDir, log);
const backendOutputs = new Map<string, string>();
const hasBackend = backendFunctions.length > 0;

const handleUpload = async () => {
const handleTimer = log.time('handle assets');
Expand Down Expand Up @@ -75,6 +74,9 @@ Either:
}

// Exclude backend output files from frontend assets if backend is active.
// Use backendOutputs.size rather than the initial discovery count so
// that functions discovered via *.backend.ts imports are included.
const hasBackend = backendOutputs.size > 0;
const backendPaths = new Set(backendOutputs.values());
const frontendOnly = hasBackend
? assets.filter((a) => !backendPaths.has(a.absolutePath))
Expand Down
110 changes: 110 additions & 0 deletions packages/plugins/apps/src/vite/backend-proxy-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// 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 { Logger } from '@dd/core/types';
import path from 'path';
import type { Plugin } from 'vite';

import type { BackendFunction } from '../backend/discovery';

import { generateProxyModule } from './proxy-codegen';

const BACKEND_SUFFIX_RE = /\.backend\.(ts|tsx|js|jsx)$/;
const VIRTUAL_PREFIX = '\0dd-backend-proxy:';

/**
* Extract the backend function name from a file path.
* e.g. `/abs/path/getGreeting.backend.ts` → `getGreeting`
*/
function extractFunctionName(filePath: string): string {
const basename = path.basename(filePath);
return basename.replace(BACKEND_SUFFIX_RE, '');
}

export interface BackendProxyPluginOptions {
log: Logger;
}

export interface BackendProxyPlugin {
/** Vite plugin hooks to spread into the plugin object. */
hooks: Pick<Plugin, 'resolveId' | 'load'>;
/** Backend functions discovered through frontend imports (name → absolutePath). */
discoveredFunctions: Map<string, string>;
}

/**
* Creates Vite `resolveId`/`load` hooks that intercept `*.backend.ts` imports
* in frontend code and serve a lightweight proxy module instead of the real
* server-side source.
*
* The proxy calls `executeBackendFunction` from `@datadog/apps-function-query`,
* which routes to the dev-server HTTP endpoint or iframe postMessage at runtime.
*
* As a side-effect, every resolved `.backend.ts` import is tracked in
* `discoveredFunctions` so the production build can bundle them for upload.
*/
export function createBackendProxyPlugin({ log }: BackendProxyPluginOptions): BackendProxyPlugin {
const discoveredFunctions = new Map<string, string>();

const hooks: Pick<Plugin, 'resolveId' | 'load'> = {
async resolveId(source, importer, options) {
if (!BACKEND_SUFFIX_RE.test(source)) {
return null;
}

// Resolve the real file path so we can verify it exists.
const resolved = await this.resolve(source, importer, {
...options,
skipSelf: true,
});

if (!resolved) {
return null;
}

const absolutePath = resolved.id;
const functionName = extractFunctionName(absolutePath);

// Warn on duplicate function names.
const existing = discoveredFunctions.get(functionName);
if (existing && existing !== absolutePath) {
log.warn(
`Duplicate backend function name "${functionName}" ` +
`found at ${absolutePath} (already discovered at ${existing}). ` +
`Skipping the duplicate.`,
);
// Still return the virtual ID so the import resolves — it will
// use the proxy for the first-discovered function.
} else {
discoveredFunctions.set(functionName, absolutePath);
}

log.debug(`Resolved backend proxy for "${functionName}" (${absolutePath})`);
return `${VIRTUAL_PREFIX}${absolutePath}`;
},

load(id) {
if (!id.startsWith(VIRTUAL_PREFIX)) {
return null;
}

const absolutePath = id.slice(VIRTUAL_PREFIX.length);
const functionName = extractFunctionName(absolutePath);
return generateProxyModule(functionName);
},
};

return { hooks, discoveredFunctions };
}

/**
* Convert the proxy plugin's discovered functions map into the
* `BackendFunction[]` format used by the existing build pipeline.
*/
export function toBackendFunctions(discoveredFunctions: Map<string, string>): BackendFunction[] {
return Array.from(discoveredFunctions, ([name, entryPath]) => ({
name,
entryPath,
}));
}
20 changes: 14 additions & 6 deletions packages/plugins/apps/src/vite/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,22 +313,30 @@ async function handleExecuteAction(
/**
* Create a Connect-compatible middleware for the Vite dev server.
* Intercepts backend function requests and handles them via Datadog API.
*
* Accepts either a static array or a getter function so that functions
* discovered lazily (e.g. via `*.backend.ts` import resolution) are
* visible to the middleware without restarting the dev server.
*/
export function createDevServerMiddleware(
viteBuild: typeof build,
backendFunctions: BackendFunction[],
backendFunctions: BackendFunction[] | (() => BackendFunction[]),
auth: AuthOptionsWithDefaults,
projectRoot: string,
log: Logger,
): (req: IncomingMessage, res: ServerResponse, next: () => void) => void {
const functionsByName = new Map(backendFunctions.map((f) => [f.name, f]));
const getFunctions =
typeof backendFunctions === 'function' ? backendFunctions : () => backendFunctions;

const getFunctionsByName = () => new Map(getFunctions().map((f) => [f.name, f]));

const bundle = (func: BackendFunction, args: unknown[]) =>
bundleBackendFunction(viteBuild, func, args, projectRoot, log);

if (backendFunctions.length > 0) {
const initialFunctions = getFunctions();
if (initialFunctions.length > 0) {
log.info(
`Dev server middleware active for ${backendFunctions.length} backend function(s): ${backendFunctions.map((f) => f.name).join(', ')}`,
`Dev server middleware active for ${initialFunctions.length} backend function(s): ${initialFunctions.map((f) => f.name).join(', ')}`,
);
}

Expand All @@ -352,7 +360,7 @@ export function createDevServerMiddleware(
}

if (req.url === '/__dd/debugBundle') {
handleDebugBundle(req, res, functionsByName, bundle).catch(() => {
handleDebugBundle(req, res, getFunctionsByName(), bundle).catch(() => {
sendError(res, 500, 'Unexpected error');
});
} else if (req.url === '/__dd/executeAction') {
Expand All @@ -364,7 +372,7 @@ export function createDevServerMiddleware(
);
return;
}
handleExecuteAction(req, res, functionsByName, bundle, fullAuth, log).catch(() => {
handleExecuteAction(req, res, getFunctionsByName(), bundle, fullAuth, log).catch(() => {
sendError(res, 500, 'Unexpected error');
});
} else {
Expand Down
79 changes: 54 additions & 25 deletions packages/plugins/apps/src/vite/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { build } from 'vite';

import type { BackendFunction } from '../backend/discovery';

import { createBackendProxyPlugin, toBackendFunctions } from './backend-proxy-plugin';
import { buildBackendFunctions } from './build-backend-functions';
import { createDevServerMiddleware } from './dev-server';

Expand All @@ -24,6 +25,9 @@ export interface VitePluginOptions {
/**
* Returns the Vite-specific plugin hooks for the apps plugin.
*
* Resolution (resolveId/load): intercepts `*.backend.ts` imports in frontend
* code and serves a proxy module that delegates to `executeBackendFunction`.
*
* Production (closeBundle): builds backend functions (if any) then uploads
* all assets sequentially.
*
Expand All @@ -38,29 +42,54 @@ export const getVitePlugin = ({
handleUpload,
log,
auth,
}: VitePluginOptions): PluginOptions['vite'] => ({
async closeBundle() {
let backendOutDir: string | undefined;
if (functions.length > 0) {
backendOutDir = await buildBackendFunctions(
viteBuild,
functions,
backendOutputs,
buildRoot,
log,
);
}
try {
await handleUpload();
} finally {
if (backendOutDir) {
await rm(backendOutDir);
}: VitePluginOptions): PluginOptions['vite'] => {
const proxyPlugin = createBackendProxyPlugin({ log });

return {
resolveId: proxyPlugin.hooks.resolveId,
load: proxyPlugin.hooks.load,
async closeBundle() {
// Merge directory-discovered functions with import-discovered ones.
const importDiscovered = toBackendFunctions(proxyPlugin.discoveredFunctions);
const existingNames = new Set(functions.map((f) => f.name));
const allFunctions = [
...functions,
...importDiscovered.filter((f) => !existingNames.has(f.name)),
];

let backendOutDir: string | undefined;
if (allFunctions.length > 0) {
backendOutDir = await buildBackendFunctions(
viteBuild,
allFunctions,
backendOutputs,
buildRoot,
log,
);
}
}
},
configureServer(server) {
server.middlewares.use(
createDevServerMiddleware(viteBuild, functions, auth, buildRoot, log),
);
},
});
try {
await handleUpload();
} finally {
if (backendOutDir) {
await rm(backendOutDir);
}
}
},
configureServer(server) {
// Merge directory-discovered functions with any already discovered
// by the proxy plugin during dev server startup.
const allFunctions = () => {
const importDiscovered = toBackendFunctions(proxyPlugin.discoveredFunctions);
const existingNames = new Set(functions.map((f) => f.name));
return [
...functions,
...importDiscovered.filter((f) => !existingNames.has(f.name)),
];
};

server.middlewares.use(
createDevServerMiddleware(viteBuild, allFunctions, auth, buildRoot, log),
);
},
};
};
23 changes: 23 additions & 0 deletions packages/plugins/apps/src/vite/proxy-codegen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// 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.

/**
* Generate a proxy module for a backend function.
*
* When a frontend file imports a `*.backend.ts` module, Vite serves this
* generated proxy instead of the real server-side code. The proxy delegates
* to `executeBackendFunction` from `@datadog/apps-function-query`, which
* picks the right transport (dev-server HTTP or iframe postMessage) at runtime.
*/
export function generateProxyModule(functionName: string): string {
const lines: string[] = [
`import { executeBackendFunction } from '@datadog/apps-function-query';`,
'',
`export async function ${functionName}(...args) {`,
` return executeBackendFunction('${functionName}', args);`,
`}`,
];

return lines.join('\n');
}
Loading