diff --git a/packages/plugins/apps/package.json b/packages/plugins/apps/package.json index 2a737b1e2..8228aa9c4 100644 --- a/packages/plugins/apps/package.json +++ b/packages/plugins/apps/package.json @@ -15,8 +15,14 @@ "hideFromRootReadme": true }, "toBuild": { - "apps-runtime": { - "entry": "./src/built/apps-runtime.ts", + "apps-runtime-dev": { + "entry": "./src/built/apps-runtime-dev.ts", + "format": [ + "esm" + ] + }, + "apps-runtime-prod": { + "entry": "./src/built/apps-runtime-prod.ts", "format": [ "esm" ] diff --git a/packages/plugins/apps/src/backend/client/execute-backend-function.ts b/packages/plugins/apps/src/backend/client/execute-backend-function.ts deleted file mode 100644 index 666f1da81..000000000 --- a/packages/plugins/apps/src/backend/client/execute-backend-function.ts +++ /dev/null @@ -1,54 +0,0 @@ -// 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. - -/* eslint-env browser */ - -import { devServerTransport } from './transports/dev-server-transport'; -import { postMessageTransport } from './transports/post-message-transport/post-message-transport'; -import type { BackendFunctionTransport } from './types'; - -function isInIframe(): boolean { - try { - return typeof window !== 'undefined' && window.parent !== window; - } catch { - // Accessing window.parent can throw if cross-origin - return true; - } -} - -function resolveTransport(): BackendFunctionTransport { - if (isInIframe()) { - return postMessageTransport; - } - return devServerTransport; -} - -/** - * Executes a backend function by name with the provided arguments. - * - * When running inside an iframe embedded in App Builder, automatically - * uses postMessage to communicate with the parent window. Otherwise, - * uses HTTP fetch to the backend endpoint. - * - * @param functionName - The name of the backend function to execute - * @param args - Array of arguments to pass to the function - * @returns Promise that resolves to the function's return value - * @throws {BackendFunctionError} If the request fails or the function throws an error - * - * @example - * ```typescript - * const result = await executeBackendFunction<{ sum: number }, [number, number]>( - * 'testWithImport', - * [5, 7] - * ); - * console.log(result.sum); // 12 - * ``` - */ -export async function executeBackendFunction( - functionName: string, - args: TArgs, -): Promise { - const transport = resolveTransport(); - return transport(functionName, args); -} diff --git a/packages/plugins/apps/src/backend/client/execute-backend-function.test.ts b/packages/plugins/apps/src/backend/client/transports/dev-server-transport.test.ts similarity index 73% rename from packages/plugins/apps/src/backend/client/execute-backend-function.test.ts rename to packages/plugins/apps/src/backend/client/transports/dev-server-transport.test.ts index be1504dde..a4f40797c 100644 --- a/packages/plugins/apps/src/backend/client/execute-backend-function.test.ts +++ b/packages/plugins/apps/src/backend/client/transports/dev-server-transport.test.ts @@ -9,10 +9,11 @@ // lives with the original tests in web-ui's @datadog/apps-function-query // until a DOM-enabled harness is introduced. -import { executeBackendFunction } from './execute-backend-function'; -import { BackendFunctionError } from './types'; +import { BackendFunctionError } from '../types'; -describe('executeBackendFunction', () => { +import { devServerTransport } from './dev-server-transport'; + +describe('devServerTransport', () => { let originalFetch: typeof fetch; beforeEach(() => { @@ -30,7 +31,7 @@ describe('executeBackendFunction', () => { json: async () => mockResponse, }); - const result = await executeBackendFunction<{ sum: number }>('testWithImport', [5, 7]); + const result = await devServerTransport<{ sum: number }>('testWithImport', [5, 7]); expect(result).toEqual({ sum: 12 }); expect(global.fetch).toHaveBeenCalledWith('/__dd/executeAction', { @@ -48,11 +49,9 @@ describe('executeBackendFunction', () => { it('should throw BackendFunctionError on network error', async () => { global.fetch = jest.fn().mockRejectedValue(new Error('Network failed')); - await expect(executeBackendFunction('testFunction', [])).rejects.toThrow( - BackendFunctionError, - ); + await expect(devServerTransport('testFunction', [])).rejects.toThrow(BackendFunctionError); - await expect(executeBackendFunction('testFunction', [])).rejects.toThrow( + await expect(devServerTransport('testFunction', [])).rejects.toThrow( 'Network error while executing backend function "testFunction"', ); }); @@ -64,11 +63,9 @@ describe('executeBackendFunction', () => { text: async () => 'Internal Server Error', }); - await expect(executeBackendFunction('testFunction', [])).rejects.toThrow( - BackendFunctionError, - ); + await expect(devServerTransport('testFunction', [])).rejects.toThrow(BackendFunctionError); - await expect(executeBackendFunction('testFunction', [])).rejects.toThrow( + await expect(devServerTransport('testFunction', [])).rejects.toThrow( 'Backend function "testFunction" failed with status 500', ); }); @@ -83,11 +80,9 @@ describe('executeBackendFunction', () => { json: async () => mockResponse, }); - await expect(executeBackendFunction('testFunction', [])).rejects.toThrow( - BackendFunctionError, - ); + await expect(devServerTransport('testFunction', [])).rejects.toThrow(BackendFunctionError); - await expect(executeBackendFunction('testFunction', [])).rejects.toThrow( + await expect(devServerTransport('testFunction', [])).rejects.toThrow( 'Backend function "testFunction" returned an error', ); }); @@ -101,11 +96,9 @@ describe('executeBackendFunction', () => { }, }); - await expect(executeBackendFunction('testFunction', [])).rejects.toThrow( - BackendFunctionError, - ); + await expect(devServerTransport('testFunction', [])).rejects.toThrow(BackendFunctionError); - await expect(executeBackendFunction('testFunction', [])).rejects.toThrow( + await expect(devServerTransport('testFunction', [])).rejects.toThrow( 'Failed to parse response from backend function', ); }); @@ -117,7 +110,7 @@ describe('executeBackendFunction', () => { text: async () => 'Not Found', }); - await expect(executeBackendFunction('testFunction', [])).rejects.toMatchObject({ + await expect(devServerTransport('testFunction', [])).rejects.toMatchObject({ name: 'BackendFunctionError', functionName: 'testFunction', statusCode: 404, diff --git a/packages/plugins/apps/src/built/apps-runtime-dev.ts b/packages/plugins/apps/src/built/apps-runtime-dev.ts new file mode 100644 index 000000000..5fe07491b --- /dev/null +++ b/packages/plugins/apps/src/built/apps-runtime-dev.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. + +/* eslint-env browser */ +/* global globalThis */ +import { devServerTransport } from '../backend/client/transports/dev-server-transport'; + +const globalAny: any = globalThis; +globalAny.DD_APPS_RUNTIME = { executeBackendFunction: devServerTransport }; diff --git a/packages/plugins/apps/src/built/apps-runtime-prod.ts b/packages/plugins/apps/src/built/apps-runtime-prod.ts new file mode 100644 index 000000000..1df4a3516 --- /dev/null +++ b/packages/plugins/apps/src/built/apps-runtime-prod.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. + +/* eslint-env browser */ +/* global globalThis */ +import { postMessageTransport } from '../backend/client/transports/post-message-transport/post-message-transport'; + +const globalAny: any = globalThis; +globalAny.DD_APPS_RUNTIME = { executeBackendFunction: postMessageTransport }; diff --git a/packages/plugins/apps/src/built/apps-runtime.ts b/packages/plugins/apps/src/built/apps-runtime.ts deleted file mode 100644 index 4dd7c2b8d..000000000 --- a/packages/plugins/apps/src/built/apps-runtime.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 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. - -/* eslint-env browser */ -/* global globalThis */ -import { executeBackendFunction } from '../backend/client/execute-backend-function'; - -// Exposed on `globalThis.DD_APPS_RUNTIME` by the apps plugin injection so -// generated proxy modules can call `executeBackendFunction` without importing -// a runtime package. Internal API — consumers (generated .backend.ts proxy -// modules) are produced by the apps plugin itself, not authored by users. -const globalAny: any = globalThis; -globalAny.DD_APPS_RUNTIME = { executeBackendFunction }; diff --git a/packages/plugins/apps/src/index.test.ts b/packages/plugins/apps/src/index.test.ts index 553f29c2a..11c60983a 100644 --- a/packages/plugins/apps/src/index.test.ts +++ b/packages/plugins/apps/src/index.test.ts @@ -59,21 +59,37 @@ describe('Apps Plugin - getPlugins', () => { expect(getPlugins(getArgs())).toHaveLength(1); }); - test('Should inject the apps runtime at the top of the user bundle when enabled', () => { - const injectMock = jest.fn(); - getPlugins( - getGetPluginsArg( - { apps: {} }, - { bundler: { ...getMockBundler({ name: 'vite' }), outDir }, inject: injectMock }, - ), - ); - - expect(injectMock).toHaveBeenCalledWith({ - type: 'file', - position: InjectPosition.MIDDLE, - value: expect.stringContaining('apps-runtime.mjs'), - }); - }); + const runtimeInjectionCases = [ + { command: 'serve' as const, runtime: 'apps-runtime-dev.mjs' }, + { command: 'build' as const, runtime: 'apps-runtime-prod.mjs' }, + ]; + + test.each(runtimeInjectionCases)( + 'Should inject the $runtime runtime when vite command is "$command"', + ({ command, runtime }) => { + const injectMock = jest.fn(); + const plugins = getPlugins( + getGetPluginsArg( + { apps: {} }, + { + bundler: { ...getMockBundler({ name: 'vite' }), outDir }, + inject: injectMock, + }, + ), + ); + const configHook = plugins[0].vite!.config as ( + userConfig: object, + env: { command: 'serve' | 'build' }, + ) => void; + configHook({}, { command }); + + expect(injectMock).toHaveBeenCalledWith({ + type: 'file', + position: InjectPosition.MIDDLE, + value: expect.stringContaining(runtime), + }); + }, + ); test('Should not inject the runtime when disabled', () => { const injectMock = jest.fn(); diff --git a/packages/plugins/apps/src/index.ts b/packages/plugins/apps/src/index.ts index 13af8ff23..be26321ee 100644 --- a/packages/plugins/apps/src/index.ts +++ b/packages/plugins/apps/src/index.ts @@ -4,7 +4,6 @@ import { rm } from '@dd/core/helpers/fs'; import type { GetPlugins } from '@dd/core/types'; -import { InjectPosition } from '@dd/core/types'; import chalk from 'chalk'; import path from 'path'; @@ -89,21 +88,6 @@ export const getPlugins: GetPlugins = ({ options, context, bundler }) => { return []; } - // Inject the runtime that `globalThis.DD_APPS_RUNTIME.executeBackendFunction` - // is read from. The generated proxy modules (emitted by the transform hook - // below) reference that global. NOTE: This file is built alongside the - // bundler plugin via the `toBuild` entry in @dd/apps-plugin's package.json. - // - // Position MIDDLE is used instead of BEFORE so Vite's dev server injects - // the runtime as a