diff --git a/packages/plugins/apps/src/backend/shared.ts b/packages/plugins/apps/src/backend/shared.ts index 249e3ed60..1dc537072 100644 --- a/packages/plugins/apps/src/backend/shared.ts +++ b/packages/plugins/apps/src/backend/shared.ts @@ -16,10 +16,30 @@ export function isActionCatalogInstalled(fromDir: string): boolean { } } +/** Check if the Datadog Apps backend runtime integration is installed. */ +export function isDatadogAppsBackendRuntimeInstalled(fromDir: string): boolean { + try { + require.resolve('@datadog/apps/backend/_runtime', { paths: [fromDir] }); + return true; + } catch { + return false; + } +} + /** The import line to pull action-catalog's setExecuteActionImplementation into bundles. */ export const ACTION_CATALOG_IMPORT = "import { setExecuteActionImplementation } from '@datadog/action-catalog/action-execution';"; +/** The import line that lets @datadog/apps install its backend runtime provider. */ +export const DATADOG_APPS_BACKEND_RUNTIME_IMPORT = + "import { setBackendRuntimeFallback } from '@datadog/apps/backend/_runtime';"; + +/** Script snippet that installs the SDK-provided runtime when the host has not installed one. */ +export const SET_DATADOG_APPS_BACKEND_RUNTIME_SNIPPET = `\ + if (typeof setBackendRuntimeFallback === 'function') { + setBackendRuntimeFallback($); + }`; + /** Script snippet that registers the $.Actions-based executeAction implementation at runtime. */ export const SET_EXECUTE_ACTION_SNIPPET = `\ if (typeof setExecuteActionImplementation === 'function') { diff --git a/packages/plugins/apps/src/backend/virtual-entry.test.ts b/packages/plugins/apps/src/backend/virtual-entry.test.ts index 376b15a3c..f5df31b06 100644 --- a/packages/plugins/apps/src/backend/virtual-entry.test.ts +++ b/packages/plugins/apps/src/backend/virtual-entry.test.ts @@ -18,6 +18,7 @@ describe('Backend Functions - generateVirtualEntryContent', () => { describe('without action-catalog', () => { beforeEach(() => { jest.spyOn(shared, 'isActionCatalogInstalled').mockReturnValue(false); + jest.spyOn(shared, 'isDatadogAppsBackendRuntimeInstalled').mockReturnValue(false); }); test('Should import the function by name from the entry path', () => { @@ -89,11 +90,21 @@ describe('Backend Functions - generateVirtualEntryContent', () => { ); expect(result).not.toContain('@datadog/action-catalog'); }); + + test('Should not include the Datadog Apps backend runtime import', () => { + const result = generateVirtualEntryContent( + 'myHandler', + '/src/handler.ts', + PROJECT_ROOT, + ); + expect(result).not.toContain('@datadog/apps/backend/_runtime'); + }); }); describe('with action-catalog', () => { beforeEach(() => { jest.spyOn(shared, 'isActionCatalogInstalled').mockReturnValue(true); + jest.spyOn(shared, 'isDatadogAppsBackendRuntimeInstalled').mockReturnValue(false); }); test('Should include action-catalog import', () => { @@ -117,8 +128,34 @@ describe('Backend Functions - generateVirtualEntryContent', () => { }); }); + describe('with the Datadog Apps backend runtime', () => { + beforeEach(() => { + jest.spyOn(shared, 'isActionCatalogInstalled').mockReturnValue(false); + jest.spyOn(shared, 'isDatadogAppsBackendRuntimeInstalled').mockReturnValue(true); + }); + + test('Should import and install the SDK-provided runtime before calling the handler', () => { + const result = generateVirtualEntryContent( + 'myHandler', + '/src/handler.ts', + PROJECT_ROOT, + ); + + expect(result).toContain( + "import { setBackendRuntimeFallback } from '@datadog/apps/backend/_runtime'", + ); + expect(result.indexOf('setBackendRuntimeFallback($)')).toBeGreaterThan( + result.indexOf('globalThis.$ = $'), + ); + expect(result.indexOf('await myHandler(...args)')).toBeGreaterThan( + result.indexOf('setBackendRuntimeFallback($)'), + ); + }); + }); + test('Should escape entry paths with special characters', () => { jest.spyOn(shared, 'isActionCatalogInstalled').mockReturnValue(false); + jest.spyOn(shared, 'isDatadogAppsBackendRuntimeInstalled').mockReturnValue(false); const result = generateVirtualEntryContent( 'handler', '/path/with "quotes"/handler.ts', @@ -132,6 +169,7 @@ describe('Backend Functions - generateDevVirtualEntryContent', () => { beforeEach(() => { jest.restoreAllMocks(); jest.spyOn(shared, 'isActionCatalogInstalled').mockReturnValue(false); + jest.spyOn(shared, 'isDatadogAppsBackendRuntimeInstalled').mockReturnValue(false); }); test('Should produce identical output to generateVirtualEntryContent', () => { @@ -162,6 +200,7 @@ describe('Backend Functions - args round-trip via $.backendFunctionArgs', () => beforeEach(() => { jest.restoreAllMocks(); jest.spyOn(shared, 'isActionCatalogInstalled').mockReturnValue(false); + jest.spyOn(shared, 'isDatadogAppsBackendRuntimeInstalled').mockReturnValue(false); }); // Extract the body of the generated `main($)` function so we can eval it diff --git a/packages/plugins/apps/src/backend/virtual-entry.ts b/packages/plugins/apps/src/backend/virtual-entry.ts index 235d9b1a5..22d3947bb 100644 --- a/packages/plugins/apps/src/backend/virtual-entry.ts +++ b/packages/plugins/apps/src/backend/virtual-entry.ts @@ -4,8 +4,11 @@ import { ACTION_CATALOG_IMPORT, + DATADOG_APPS_BACKEND_RUNTIME_IMPORT, + SET_DATADOG_APPS_BACKEND_RUNTIME_SNIPPET, SET_EXECUTE_ACTION_SNIPPET, isActionCatalogInstalled, + isDatadogAppsBackendRuntimeInstalled, } from './shared'; /** @@ -21,6 +24,10 @@ export function generateVirtualEntryContent( lines.push(`import { ${functionName} } from ${JSON.stringify(entryPath)};`); + if (isDatadogAppsBackendRuntimeInstalled(projectRoot)) { + lines.push(DATADOG_APPS_BACKEND_RUNTIME_IMPORT); + } + if (isActionCatalogInstalled(projectRoot)) { lines.push(ACTION_CATALOG_IMPORT); } @@ -30,6 +37,9 @@ export function generateVirtualEntryContent( lines.push('export async function main($) {'); lines.push(' globalThis.$ = $;'); lines.push(''); + lines.push(' // Install the @datadog/apps backend runtime provider'); + lines.push(SET_DATADOG_APPS_BACKEND_RUNTIME_SNIPPET); + lines.push(''); lines.push(` // Register the $.Actions-based implementation for executeAction`); lines.push(SET_EXECUTE_ACTION_SNIPPET); lines.push('');