From ceb63574a2a26b53de8ddb28c25da679dbeefcb0 Mon Sep 17 00:00:00 2001 From: "Lucas A. C. Lessa" Date: Thu, 11 Jun 2026 23:59:25 +0000 Subject: [PATCH 1/3] Fix extension bundling by externalizing esbuild and improving error messages Add esbuild to external dependencies in build config and enhance error logging in bundler with detailed output file information. Co-Authored-By: Claude Sonnet 4.5 --- build.mjs | 2 ++ src/services/extension/bundler.ts | 38 ++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/build.mjs b/build.mjs index 13b34f8..61157d2 100644 --- a/build.mjs +++ b/build.mjs @@ -87,6 +87,8 @@ esbuild "typescript", // pino-pretty uses worker threads and must be external "pino-pretty", + // esbuild must be external to access its binary executable + "esbuild", ], }) .catch((e) => { diff --git a/src/services/extension/bundler.ts b/src/services/extension/bundler.ts index 802dbcc..69fb6b3 100644 --- a/src/services/extension/bundler.ts +++ b/src/services/extension/bundler.ts @@ -214,20 +214,35 @@ export function bundleExtensionEffect( // Run esbuild const buildResult = yield* Effect.tryPromise({ try: () => esbuild.build(config), - catch: (error) => - new ConfigurationError({ - message: `ESBUILD_ERROR: ${error instanceof Error ? error.message : String(error)}`, - userMessage: "Failed to bundle extension", - }), + catch: (error) => { + const errorMessage = error instanceof Error ? error.message : String(error); + logger.error({ + type: "esbuild_error", + extension: pkg.name, + entryPath, + config, + error: errorMessage, + }); + return new ConfigurationError({ + message: `ESBUILD_ERROR: ${errorMessage}`, + userMessage: `Failed to bundle extension: ${errorMessage}`, + }); + }, }); // Extract output files const outputFiles = buildResult.outputFiles; if (!outputFiles || outputFiles.length === 0) { + logger.error({ + type: "bundle_error", + extension: pkg.name, + reason: "no_output_files", + buildResult, + }); return yield* Effect.fail( new ConfigurationError({ message: "No output files generated by esbuild", - userMessage: "Failed to bundle extension", + userMessage: "Failed to bundle extension: No output files generated by esbuild", }), ); } @@ -236,10 +251,17 @@ export function bundleExtensionEffect( const mapFile = outputFiles.find((f) => f.path.endsWith(".mjs.map")); if (!mjsFile) { + const fileList = outputFiles.map(f => f.path).join(", "); + logger.error({ + type: "bundle_error", + extension: pkg.name, + reason: "no_mjs_file", + outputFiles: fileList, + }); return yield* Effect.fail( new ConfigurationError({ - message: "No .mjs file generated by esbuild", - userMessage: "Failed to bundle extension", + message: `No .mjs file generated by esbuild. Output files: ${fileList}`, + userMessage: `Failed to bundle extension: No .mjs file generated. Got: ${fileList}`, }), ); } From b523925a17f830cb836d5f2b56c7295d28e19ef0 Mon Sep 17 00:00:00 2001 From: "Lucas A. C. Lessa" Date: Fri, 12 Jun 2026 06:01:28 +0000 Subject: [PATCH 2/3] Add UI extensions support to application release flow Include UI extensions from config when creating releases. Validates that extensions have at most one target (API limitation) and includes extension data in the release mutation. Co-Authored-By: Claude Sonnet 4.5 --- src/core/applications.ts | 41 +++++++++++++++++++++++++++++++++++- src/services/applications.ts | 18 ++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/core/applications.ts b/src/core/applications.ts index 90c4571..feb666c 100644 --- a/src/core/applications.ts +++ b/src/core/applications.ts @@ -123,6 +123,17 @@ export interface ReleaseInfo { createdAt: string; } +/** + * UI extension format for release API (single target instead of array) + */ +export interface ReleaseUiExtension { + name: string; + handle: string; + source: string; + type: string; + target?: string; +} + export interface ExtensionSecurityReport { extensionName: string; extensionDir: string; @@ -848,9 +859,10 @@ export function applicationReleaseEffect( ); } - // Load configuration to get actions and subscriptions + // Load configuration to get actions, subscriptions, and extensions let actions: ActionConfig[] = []; let subscriptions: SubscriptionConfig[] = []; + let uiExtensions: ReleaseUiExtension[] = []; const configResult = yield* getConfigFileEffect({ configPath: input.configPath, @@ -860,6 +872,32 @@ export function applicationReleaseEffect( if (configResult && !isConfigValidationErrorResult(configResult)) { actions = configResult.actions || []; subscriptions = configResult.subscriptions?.webhook || []; + + // Extract UI extensions from config + const extensions = yield* getExtensionsFromConfigEffect({ + configPath: input.configPath, + env: input.env as Environment, + }).pipe(Effect.orElseSucceed(() => [])); + + // Validate that only one target is specified per extension (API limitation) + for (const ext of extensions) { + if (ext.targets && ext.targets.length > 1) { + yield* Effect.fail( + new ValidationError({ + message: `UI extension "${ext.name}" has ${ext.targets.length} targets, but only one target is supported per extension during release`, + }), + ); + } + } + + // Map extensions to release format + uiExtensions = extensions.map((ext) => ({ + name: ext.name, + handle: ext.handle, + source: ext.source, + type: ext.type, + target: ext.targets?.[0]?.target, + })); } const releaseData = { @@ -868,6 +906,7 @@ export function applicationReleaseEffect( description: input.description, actions, subscriptions, + uiExtensions, }; const result = yield* callCreateRelease(releaseData, { accessToken }); diff --git a/src/services/applications.ts b/src/services/applications.ts index 8d966fa..871a13b 100644 --- a/src/services/applications.ts +++ b/src/services/applications.ts @@ -108,6 +108,14 @@ export const CreateReleaseMutation = graphql(` version description createdAt + uiExtensions { + id + name + handle + type + source + target + } } } `); @@ -167,12 +175,22 @@ export const subscriptionInput = type({ url: "string", }); +export const uiExtensionInput = type({ + name: "string", + handle: "string", + source: "string", + // Must match ExtensionType from src/core/extension/bundler-config.ts + type: '"embed" | "checkout" | "blocks"', + target: "string?", +}); + export const releaseInput = type({ applicationId: "string", version: "string", description: "string?", actions: actionInput.array().optional(), subscriptions: subscriptionInput.array().optional(), + uiExtensions: uiExtensionInput.array().optional(), }); export function createApplicationEffect( From 70b3f1ca022297e7832f67dad74fba7347280f05 Mon Sep 17 00:00:00 2001 From: "Lucas A. C. Lessa" Date: Fri, 12 Jun 2026 06:24:19 +0000 Subject: [PATCH 3/3] Add test coverage for UI extensions in release flow - Add integration test for creating releases with UI extensions - Update test fixture to support uiExtensions in release response - Verify extensions are properly included in release data Co-Authored-By: Claude Sonnet 4.5 --- tests/integration/application-service.test.ts | 38 +++++++++++++++++++ tests/setup/fixtures/application-fixtures.ts | 13 ++++++- 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/tests/integration/application-service.test.ts b/tests/integration/application-service.test.ts index 3b1ea78..85d6f05 100644 --- a/tests/integration/application-service.test.ts +++ b/tests/integration/application-service.test.ts @@ -132,6 +132,44 @@ describe("Application Service", () => { expect(release?.createRelease?.createdAt).toBeTruthy(); }); + test("creates application release with UI extensions", async () => { + const input = { + applicationId: "app-1", + version: "2.1.0", + description: "Release with UI extensions", + actions: [], + subscriptions: [], + uiExtensions: [ + { + name: "product-embed", + handle: "product-embed-handle", + source: "./extensions/embed/index.tsx", + type: "embed", + target: "products.view", + }, + { + name: "checkout-extension", + handle: "checkout-ext-handle", + source: "./extensions/checkout/index.tsx", + type: "checkout", + target: "checkout.summary", + }, + ], + }; + + const release = await runEffect( + createReleaseEffect(input, { + accessToken: "test-token-123", + }), + ); + + expect(release?.createRelease?.version).toBe(input.version); + expect(release?.createRelease?.description).toBe(input.description); + expect(release?.createRelease?.id).toBeTruthy(); + expect(release?.createRelease?.createdAt).toBeTruthy(); + expect(release?.createRelease?.uiExtensions).toBeDefined(); + }); + test("enables application", async () => { const result = await runEffect( enableApplicationEffect( diff --git a/tests/setup/fixtures/application-fixtures.ts b/tests/setup/fixtures/application-fixtures.ts index 9182293..00bdfb0 100644 --- a/tests/setup/fixtures/application-fixtures.ts +++ b/tests/setup/fixtures/application-fixtures.ts @@ -71,10 +71,21 @@ export const applicationFixtures = { publicKey: "public-key-data", }), - createReleaseResponse: (input: { version: string; description: string }) => ({ + createReleaseResponse: (input: { + version: string; + description: string; + uiExtensions?: Array<{ + name: string; + handle: string; + source: string; + type: string; + target?: string; + }>; + }) => ({ id: `release-${Date.now()}`, version: input.version, description: input.description, createdAt: new Date().toISOString(), + uiExtensions: input.uiExtensions || [], }), };