diff --git a/packages-private/scripting-utils/source/yaml/codegen.ts b/packages-private/scripting-utils/source/yaml/codegen.ts index 5d1eb50f5..6f9f73ee5 100644 --- a/packages-private/scripting-utils/source/yaml/codegen.ts +++ b/packages-private/scripting-utils/source/yaml/codegen.ts @@ -37,17 +37,19 @@ export async function createOrUpdateExtConfig( ) { const extConfigDoc = doc ?? new Document({}); + // Keep this up so that it appears before other sections, and users see it right away. + if (config.web !== undefined) { + buildWeb(extConfigDoc, config.web); + } + config.hooks ??= {}; - config.operations ??= { workerProcess: [] }; config.runtimeManifest ??= { packages: {} }; await buildHooks(extConfigDoc, config.hooks); - buildOperations(extConfigDoc, config.operations); - buildRuntimeManifest(extConfigDoc, config.runtimeManifest); - - if (config.web !== undefined) { - buildWeb(extConfigDoc, config.web); + if (config.operations !== undefined) { + buildOperations(extConfigDoc, config.operations); } + buildRuntimeManifest(extConfigDoc, config.runtimeManifest); await writeExtConfig(path, extConfigDoc); return extConfigDoc; @@ -113,38 +115,46 @@ function buildActionDefinition(action: ActionDefinition) { * @param operations - The operations to build */ function buildOperations(extConfig: Document, operations: Operations) { - getOrCreateMap(extConfig, ["operations"], { - onBeforeCreate: (pair) => { - pair.key.spaceBefore = true; - }, - }); - - const workerProcess = getOrCreateSeq( - extConfig, - ["operations", "workerProcess"], - { + const ourOps = operations.workerProcess ?? []; + if (ourOps.length > 0) { + getOrCreateMap(extConfig, ["operations"], { onBeforeCreate: (pair) => { - pair.key.commentBefore = - " These worker processes definitions are auto-generated. Do not remove or manually edit."; + pair.key.spaceBefore = true; }, - }, - ); + }); - // Clear existing items to rebuild from scratch - workerProcess.items = []; + const workerProcess = getOrCreateSeq( + extConfig, + ["operations", "workerProcess"], + { + onBeforeCreate: (pair) => { + pair.key.commentBefore = + " These worker processes definitions are auto-generated. Do not remove or manually edit."; + }, + }, + ); - const ourOps = operations.workerProcess ?? []; - workerProcess.items.push( - ...ourOps.map((op) => { - const map = new YAMLMap(); - map.set("type", op.type); - map.set("impl", op.impl); + workerProcess.items = []; + workerProcess.items.push( + ...ourOps.map((op) => { + const map = new YAMLMap(); + map.set("type", op.type); + map.set("impl", op.impl); - return map; - }), - ); + return map; + }), + ); + } else if (extConfig.hasIn(["operations", "workerProcess"])) { + extConfig.deleteIn(["operations", "workerProcess"]); + } if (operations.view !== undefined) { + getOrCreateMap(extConfig, ["operations"], { + onBeforeCreate: (pair) => { + pair.key.spaceBefore = true; + }, + }); + const view = getOrCreateSeq(extConfig, ["operations", "view"]); // Seed defaults only when the user has not already populated the view list. @@ -160,6 +170,11 @@ function buildOperations(extConfig: Document, operations: Operations) { ); } } + + const operationsMap = extConfig.getIn(["operations"]); + if (operationsMap instanceof YAMLMap && operationsMap.items.length === 0) { + extConfig.deleteIn(["operations"]); + } } /** diff --git a/packages-private/scripting-utils/test/yaml/codegen.test.ts b/packages-private/scripting-utils/test/yaml/codegen.test.ts index cab19992c..0ffbc1933 100644 --- a/packages-private/scripting-utils/test/yaml/codegen.test.ts +++ b/packages-private/scripting-utils/test/yaml/codegen.test.ts @@ -109,6 +109,7 @@ runtimeManifest: const fileContent = await readFile(configPath, "utf-8"); expect(fileContent).toContain("existing-hook"); expect(fileContent).toContain("new-hook"); + expect(fileContent).not.toContain("workerProcess: []"); }, ); }); @@ -127,7 +128,7 @@ runtimeManifest: // Should have default empty structures expect(doc.has("hooks")).toBe(true); - expect(doc.has("operations")).toBe(true); + expect(doc.has("operations")).toBe(false); expect(doc.has("runtimeManifest")).toBe(true); }); }); @@ -165,7 +166,10 @@ runtimeManifest: new Document({}), ); - expect(doc.has("operations")).toBe(true); + expect(doc.has("operations")).toBe(false); + + const fileContent = await readFile(configPath, "utf-8"); + expect(fileContent).not.toContain("workerProcess: []"); }); }); @@ -533,6 +537,7 @@ hooks: expect(fileContent).toContain("view:"); expect(fileContent).toContain("type: web"); expect(fileContent).toContain("impl: index.html"); + expect(fileContent).not.toContain("workerProcess: []"); }); }); diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/lib.ts b/packages/aio-commerce-lib-app/source/commands/generate/actions/lib.ts index aba0fd88d..fdd044c38 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/lib.ts +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/lib.ts @@ -22,6 +22,7 @@ import { createOrUpdateExtConfig } from "@aio-commerce-sdk/scripting-utils/yaml" import { readYamlFile } from "@aio-commerce-sdk/scripting-utils/yaml/index"; import { consola } from "consola"; import { formatTree } from "consola/utils"; +import stringify from "safe-stable-stringify"; import { BACKEND_UI_EXTENSION_POINT_ID, @@ -294,7 +295,7 @@ export async function generateRegistrationActionFile( ); const content = template.replace( REGISTRATION_JSON_PLACEHOLDER, - `const registration = ${JSON.stringify(registration)};`, + `const registration = ${stringify(registration)};`, ); const formattedContent = await prettierFormat(content, actionPath); diff --git a/packages/aio-commerce-lib-app/source/config/schema/admin-ui-sdk.ts b/packages/aio-commerce-lib-app/source/config/schema/admin-ui-sdk.ts index 56615932e..197109500 100644 --- a/packages/aio-commerce-lib-app/source/config/schema/admin-ui-sdk.ts +++ b/packages/aio-commerce-lib-app/source/config/schema/admin-ui-sdk.ts @@ -24,17 +24,18 @@ const SANDBOX_VALUES = [ "allow-downloads", "allow-modals", "allow-popups", -] as const; +] as const satisfies string[]; + +type SandboxValue = (typeof SANDBOX_VALUES)[number]; + +function isSandboxValue(value: string): value is SandboxValue { + return SANDBOX_VALUES.includes(value as SandboxValue); +} const SandboxSchema = v.pipe( v.string('Expected a string value for "sandbox"'), v.check( - (val) => - val - .split(" ") - .every((value) => - (SANDBOX_VALUES as readonly string[]).includes(value), - ), + (val) => val.split(" ").every(isSandboxValue), `sandbox must contain only single-space-separated values from: ${SANDBOX_VALUES.map((t) => `"${t}"`).join(", ")}`, ), ); @@ -114,6 +115,7 @@ const sandboxDisplayIframeCheck = v.forward( ["sandbox"], ); +// TODO: Cleanup after https://github.com/open-circle/valibot/issues/1459 function withSandboxDisplayIframeCheck< TSchema extends v.BaseSchema< unknown, diff --git a/packages/aio-commerce-lib-app/test/unit/config/schema/admin-ui-sdk.test.ts b/packages/aio-commerce-lib-app/test/unit/config/schema/admin-ui-sdk.test.ts index bc028183b..6a4dba9b4 100644 --- a/packages/aio-commerce-lib-app/test/unit/config/schema/admin-ui-sdk.test.ts +++ b/packages/aio-commerce-lib-app/test/unit/config/schema/admin-ui-sdk.test.ts @@ -119,6 +119,25 @@ describe("AdminUiSdkSchema", () => { expect(result.success).toBe(true); }); + test("iframe-enabled mass action allows sandbox", () => { + const result = v.safeParse(AdminUiSdkSchema, { + registration: { + order: { + massActions: [ + { + actionId: "app::action", + label: "Action", + path: "#/action", + displayIframe: true, + sandbox: "allow-modals", + }, + ], + }, + }, + }); + expect(result.success).toBe(true); + }); + test("registration with grid columns — all 5 type values", () => { for (const type of [ "boolean", @@ -163,6 +182,25 @@ describe("AdminUiSdkSchema", () => { } }); + test("iframe-enabled view button allows sandbox", () => { + const result = v.safeParse(AdminUiSdkSchema, { + registration: { + order: { + viewButtons: [ + { + buttonId: "app::btn", + label: "Btn", + path: "#/btn", + displayIframe: true, + sandbox: "allow-modals", + }, + ], + }, + }, + }); + expect(result.success).toBe(true); + }); + test("registration with custom fees including applyFeeOnLastCreditMemo", () => { const result = v.safeParse(AdminUiSdkSchema, { registration: {