Skip to content
Open
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
60 changes: 3 additions & 57 deletions packages/plugins/apps/src/backend/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2019-Present Datadog, Inc.

import { BACKEND_VIRTUAL_PREFIX, getBackendPlugin } from '@dd/apps-plugin/backend/index';
import { getMockLogger, mockLogFn } from '@dd/tests/_jest/helpers/mocks';
import { getBackendPlugin } from '@dd/apps-plugin/backend/index';
import { getMockLogger } from '@dd/tests/_jest/helpers/mocks';

const log = getMockLogger();

Expand All @@ -24,63 +24,9 @@ describe('Backend Functions - getBackendPlugin', () => {
expect(plugin.enforce).toBe('pre');
});

test('Should have rollup and vite properties', () => {
test('Should have a vite property', () => {
const plugin = getBackendPlugin(functions, new Map(), log);
expect(plugin.rollup).toBeDefined();
expect(plugin.vite).toBeDefined();
});
});

describe('resolveId', () => {
const cases = [
{
description: 'resolve virtual backend module ID',
input: `${BACKEND_VIRTUAL_PREFIX}myHandler`,
expected: `${BACKEND_VIRTUAL_PREFIX}myHandler`,
},
{
description: 'return null for non-backend module',
input: 'some-other-module',
expected: null,
},
{
description: 'return null for empty string',
input: '',
expected: null,
},
];

test.each(cases)('Should $description', ({ input, expected }) => {
const plugin = getBackendPlugin(functions, new Map(), log);
const resolveId = plugin.resolveId as Function;
expect(resolveId(input, undefined, {})).toBe(expected);
});
});

describe('load', () => {
test('Should return virtual entry content for known function', () => {
const plugin = getBackendPlugin(functions, new Map(), log);
const load = plugin.load as Function;
const content = load(`${BACKEND_VIRTUAL_PREFIX}myHandler`);
expect(content).toContain('import { myHandler }');
expect(content).toContain('export async function main($)');
});

test('Should return null and log error for unknown function', () => {
const plugin = getBackendPlugin(functions, new Map(), log);
const load = plugin.load as Function;
const content = load(`${BACKEND_VIRTUAL_PREFIX}unknownFunc`);
expect(content).toBeNull();
expect(mockLogFn).toHaveBeenCalledWith(
expect.stringContaining('Backend function "unknownFunc" not found'),
'error',
);
});

test('Should return null for non-prefixed ID', () => {
const plugin = getBackendPlugin(functions, new Map(), log);
const load = plugin.load as Function;
expect(load('regular-module')).toBeNull();
});
});
});
36 changes: 8 additions & 28 deletions packages/plugins/apps/src/backend/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,46 +5,26 @@
import type { Logger, PluginOptions } from '@dd/core/types';

import type { BackendFunction } from './discovery';
import { getRollupPlugin } from './rollup';
import { generateVirtualEntryContent } from './virtual-entry';
import { getVitePlugin } from './vite';

export const BACKEND_VIRTUAL_PREFIX = '\0dd-backend:';
export interface BackendPluginContext {
buildRoot: string;
bundler: any;
}

/**
* Returns a plugin that injects backend functions as additional entry points
* into the host build. The backendOutputs map is populated during the build
* and read by the upload plugin in asyncTrueEnd.
* Returns a plugin that builds backend functions via a separate vite.build()
* and populates backendOutputs for the upload plugin.
*/
export function getBackendPlugin(
functions: BackendFunction[],
backendOutputs: Map<string, string>,
log: Logger,
pluginContext?: BackendPluginContext,
): PluginOptions {
const functionsByName = new Map(functions.map((f) => [f.name, f]));

return {
name: 'datadog-apps-backend-plugin',
enforce: 'pre',
resolveId(source) {
if (source.startsWith(BACKEND_VIRTUAL_PREFIX)) {
return source;
}
return null;
},
load(id) {
if (!id.startsWith(BACKEND_VIRTUAL_PREFIX)) {
return null;
}
const funcName = id.slice(BACKEND_VIRTUAL_PREFIX.length);
const func = functionsByName.get(funcName);
if (!func) {
log.error(`Backend function "${funcName}" not found.`);
return null;
}
return generateVirtualEntryContent(func.name, func.entryPath);
},
rollup: getRollupPlugin(functions, backendOutputs, log),
vite: getVitePlugin(functions, backendOutputs, log),
vite: getVitePlugin(functions, backendOutputs, log, pluginContext),
};
}
87 changes: 0 additions & 87 deletions packages/plugins/apps/src/backend/rollup.ts

This file was deleted.

10 changes: 6 additions & 4 deletions packages/plugins/apps/src/backend/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@

/**
* Check if @datadog/action-catalog is installed using Node's module resolution.
* Works across all package managers (npm, yarn, yarn PnP, pnpm).
* Resolves from the given directory (defaults to cwd) so the check works
* even when the plugin itself is loaded from a different location (e.g. linked).
*/
export function isActionCatalogInstalled(): boolean {
export function isActionCatalogInstalled(fromDir?: string): boolean {
try {
require.resolve('@datadog/action-catalog/action-execution');
const paths = fromDir ? [fromDir] : undefined;
require.resolve('@datadog/action-catalog/action-execution', { paths });
return true;
} catch {
return false;
Expand All @@ -23,7 +25,7 @@ export const ACTION_CATALOG_IMPORT =
export const SET_EXECUTE_ACTION_SNIPPET = `\
if (typeof setExecuteActionImplementation === 'function') {
setExecuteActionImplementation(async (actionId, request) => {
const actionPath = actionId.replace(/^com\\\\.datadoghq\\\\./, '');
const actionPath = actionId.replace(/^com\\.datadoghq\\./, '');
const pathParts = actionPath.split('.');
let actionFn = $.Actions;
for (const part of pathParts) {
Expand Down
45 changes: 28 additions & 17 deletions packages/plugins/apps/src/backend/virtual-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,44 @@ import {
} from './shared';

/**
* Generate the virtual entry source for a backend function.
* The host bundler resolves imports, so no export-stripping is needed.
* Generate the shared main($) function body lines.
*/
export function generateVirtualEntryContent(functionName: string, entryPath: string): string {
function generateMainBody(functionName: string, argsExpression: string): string[] {
return [
'/** @param {import("./context.types").Context} $ */',
'export async function main($) {',
' globalThis.$ = $;',
'',
' // Register the $.Actions-based implementation for executeAction',
SET_EXECUTE_ACTION_SNIPPET,
'',
` const args = ${argsExpression};`,
` const result = await ${functionName}(...args);`,
' return result;',
'}',
];
}

/**
* Generate the virtual entry source for a backend function (production).
* Uses a template expression resolved at runtime by App Builder.
*/
export function generateVirtualEntryContent(
functionName: string,
entryPath: string,
projectRoot?: string,
): string {
const lines: string[] = [];

lines.push(`import { ${functionName} } from ${JSON.stringify(entryPath)};`);

if (isActionCatalogInstalled()) {
if (isActionCatalogInstalled(projectRoot)) {
lines.push(ACTION_CATALOG_IMPORT);
}

lines.push('');
lines.push('/** @param {import("./context.types").Context} $ */');
lines.push('export async function main($) {');
lines.push(' globalThis.$ = $;');
lines.push('');
lines.push(` // Register the $.Actions-based implementation for executeAction`);
lines.push(SET_EXECUTE_ACTION_SNIPPET);
lines.push('');
lines.push(' // backendFunctionArgs is a template expression resolved at runtime by');
lines.push(" // App Builder's executeBackendFunction client via template_params.");
// eslint-disable-next-line no-template-curly-in-string
lines.push(" const args = JSON.parse('${backendFunctionArgs}' || '[]');");
lines.push(` const result = await ${functionName}(...args);`);
lines.push(' return result;');
lines.push('}');
lines.push(...generateMainBody(functionName, "JSON.parse('${backendFunctionArgs}' || '[]')"));

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