From cad9bd63016017479a97839391cb1a7838857501 Mon Sep 17 00:00:00 2001 From: Ivan Porto Wigner Date: Mon, 27 Apr 2026 22:13:13 +0200 Subject: [PATCH 01/10] refactor: missed suggestions --- .../source/commands/constants.ts | 3 + .../commands/generate/actions/config.ts | 1 - .../source/commands/generate/actions/main.ts | 229 +++++++++++++++++- .../admin-ui-sdk/registration.js.template | 6 +- .../source/config/schema/admin-ui-sdk.ts | 99 +++----- .../commands/generate/actions/lib.test.ts | 51 +++- 6 files changed, 305 insertions(+), 84 deletions(-) diff --git a/packages/aio-commerce-lib-app/source/commands/constants.ts b/packages/aio-commerce-lib-app/source/commands/constants.ts index 9789d58c8..485c0c9cd 100644 --- a/packages/aio-commerce-lib-app/source/commands/constants.ts +++ b/packages/aio-commerce-lib-app/source/commands/constants.ts @@ -46,6 +46,9 @@ export const COMMERCE_APP_CONFIG_FILE = "app.commerce.config"; /** The name of the configuration schema file */ export const CONFIG_SCHEMA_FILE_NAME = "configuration-schema.json"; +/** The name of the Admin UI SDK registration file */ +export const REGISTRATION_FILE_NAME = "registration.json"; + /** The name of the project package file */ export const PACKAGE_JSON_FILE = "package.json"; diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts b/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts index b1b444acc..dcbc10585 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts @@ -56,7 +56,6 @@ export const COMMERCE_ACTION_INPUTS = Object.fromEntries( export const CUSTOM_IMPORTS_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_IMPORTS}}"; export const CUSTOM_SCRIPTS_MAP_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_MAP}}"; export const CUSTOM_SCRIPTS_LOADER_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_LOADER}}"; -export const REGISTRATION_JSON_PLACEHOLDER = "// {{REGISTRATION_JSON}}"; /** * Creates a runtime action configuration. diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts b/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts index 101627aa2..2a429fc6d 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts @@ -1,21 +1,29 @@ import { CommerceSdkValidationError } from "@adobe/aio-commerce-lib-core/error"; import { consola } from "consola"; +import { formatTree } from "consola/utils"; +import { stringify } from "safe-stable-stringify"; import { BACKEND_UI_EXTENSION_POINT_ID, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, + GENERATED_ACTIONS_PATH, + getExtensionPointFolderPath, + REGISTRATION_FILE_NAME, } from "#commands/constants"; import { loadAppManifest } from "#commands/utils"; import { hasAdminUiSdk, hasBusinessConfigSchema } from "#config/index"; import { getRuntimeActions } from "./config"; import { - generateActionFiles, - generateRegistrationActionFile, - TEMPLATES_DIR, - updateExtConfig, -} from "./lib"; + buildAdminUiSdkExtConfig, + buildAppManagementExtConfig, + buildBusinessConfigurationExtConfig, + CUSTOM_IMPORTS_PLACEHOLDER, + CUSTOM_SCRIPTS_LOADER_PLACEHOLDER, + CUSTOM_SCRIPTS_MAP_PLACEHOLDER, + getRuntimeActions, +} from "./config"; import type { CommerceAppConfigOutputModel } from "#config/schema/app"; @@ -79,3 +87,214 @@ export async function exec() { process.exit(1); } } + +/** Update the ext.config.yaml file */ +async function updateExtConfig( + appConfig: CommerceAppConfigOutputModel, + extensionPointId: ValidExtensionPointId, +) { + consola.info(`Updating ext.config.yaml for ${extensionPointId}...`); + const extensionPointFolderPath = + getExtensionPointFolderPath(extensionPointId); + + const outputDir = await makeOutputDirFor(extensionPointFolderPath); + const extConfigPath = join(outputDir, "ext.config.yaml"); + const extConfigDoc = await readYamlFile(extConfigPath); + + let extConfig: ExtConfig; + switch (extensionPointId) { + case EXTENSIBILITY_EXTENSION_POINT_ID: { + extConfig = buildAppManagementExtConfig(appConfig); + break; + } + + case CONFIGURATION_EXTENSION_POINT_ID: { + extConfig = buildBusinessConfigurationExtConfig(); + break; + } + + case BACKEND_UI_EXTENSION_POINT_ID: { + extConfig = buildAdminUiSdkExtConfig(); + break; + } + + default: { + throw new Error(`Unsupported extension point ID: ${extensionPointId}`); + } + } + + await createOrUpdateExtConfig(extConfigPath, extConfig, extConfigDoc); + return extConfig; +} + +/** Generate the action files */ +async function generateActionFiles( + appManifest: CommerceAppConfigOutputModel, + actions: TemplateAction[], + extensionPointId: ValidExtensionPointId, +) { + consola.start("Generating runtime actions..."); + const extensionPointFolderPath = + getExtensionPointFolderPath(extensionPointId); + + const outputDir = await makeOutputDirFor( + join(extensionPointFolderPath, GENERATED_ACTIONS_PATH), + ); + + const outputFiles: string[] = []; + const templatesDir = join(__dirname, "generate/actions/templates"); + + for (const action of actions) { + const templatePath = join(templatesDir, action.templateFile); + let template = await readFile(templatePath, "utf-8"); + + // For installation action, inject custom script imports + if (action.name === "installation") { + const customScriptsTemplatePath = join( + templatesDir, + "app-management", + "custom-scripts.js.template", + ); + + const scriptsTemplate = await generateCustomScriptsTemplate( + await readFile(customScriptsTemplatePath, "utf-8"), + appManifest, + ); + + template = applyCustomScripts(template, scriptsTemplate); + } + + const actionPath = join(outputDir, `${action.name}.js`); + + await writeFile(actionPath, template, "utf-8"); + outputFiles.push(` ${relative(process.cwd(), actionPath)}`); + } + + consola.success(`Generated ${actions.length} action(s)`); + consola.log.raw(formatTree(outputFiles)); +} + +/** + * Applies the given custom scripts template code to the given installation template. + * @param installationTemplate - The installation code runtime action template + * @param customScriptsTemplate - The custom scripts dynamically generated template. + */ +export function applyCustomScripts( + installationTemplate: string, + customScriptsTemplate: string | null, +) { + // There are scripts file to include. + if (customScriptsTemplate !== null) { + return installationTemplate + .replace(CUSTOM_SCRIPTS_LOADER_PLACEHOLDER, customScriptsTemplate) + .replace( + "const args = { appConfig };", + "const args = { appConfig, customScriptsLoader };", + ); + } + // No custom scripts, remove the loader references + consola.debug( + "No custom installation steps found, skipping custom-scripts.js generation...", + ); + + return installationTemplate.replace( + CUSTOM_SCRIPTS_LOADER_PLACEHOLDER, + "// No custom installation scripts configured", + ); +} + +/** + * Generate the installation template with dynamic custom script imports + */ +export async function generateCustomScriptsTemplate( + template: string, + appManifest: CommerceAppConfigOutputModel, +) { + if (!hasCustomInstallationSteps(appManifest)) { + return null; + } + + // The generated installation action with will be at: + // src/commerce-extensibility-1/.generated/actions/.generated/app-management + // We need to resolve paths from project root to relative imports from this location + const projectRoot = await getProjectRootDirectory(); + const installationActionDir = join( + projectRoot, + getExtensionPointFolderPath(EXTENSIBILITY_EXTENSION_POINT_ID), + GENERATED_ACTIONS_PATH, + ); + + // Generate import statements + const customSteps = appManifest.installation.customInstallationSteps; + const importStatements = customSteps + .map((step: CustomInstallationStep, index: number) => { + // step.script is relative to project root (e.g., "./scripts/setup.js") + const absoluteScriptPath = join(projectRoot, step.script); + let relativeImportPath = relative( + installationActionDir, + absoluteScriptPath, + ); + if (!relativeImportPath.startsWith(".")) { + relativeImportPath = `./${relativeImportPath}`; + } + relativeImportPath = relativeImportPath.replace(/\\/g, "/"); + + const importName = `customScript${index}`; + return `import * as ${importName} from "${relativeImportPath}";`; + }) + .join("\n"); + + // Generate the loadCustomInstallationScripts function + const scriptMap = customSteps + .map((step: CustomInstallationStep, index: number) => { + const scriptPath = step.script; + const importName = `customScript${index}`; + const entry = `"${scriptPath}": ${importName},`; + + return entry.padStart(entry.length + 6); // add indentation + }) + .join("\n"); + + // Inject imports and function into template + const result = template.replace(CUSTOM_IMPORTS_PLACEHOLDER, importStatements); + return result.replace(CUSTOM_SCRIPTS_MAP_PLACEHOLDER, scriptMap); +} + +/** + * Generates `registration/index.js` with the Admin UI SDK registration config inlined as a JS object literal. + * @param appManifest - The validated app config; must satisfy `hasAdminUiSdk`. + * @param extensionPointId - The extension point ID that owns the registration action. + */ +export async function generateRegistrationActionFile( + appManifest: CommerceAppConfigOutputModel, + extensionPointId: ValidExtensionPointId, +) { + consola.start("Generating Admin UI SDK registration action..."); + const extensionPointFolderPath = + getExtensionPointFolderPath(extensionPointId); + const generatedDir = await makeOutputDirFor( + join(extensionPointFolderPath, ".generated"), + ); + + const outputDir = await makeOutputDirFor( + join(extensionPointFolderPath, ADMIN_UI_SDK_ACTIONS_PATH), + ); + + const templatePath = join( + __dirname, + "generate/actions/templates/admin-ui-sdk/registration.js.template", + ); + const template = await readFile(templatePath, "utf-8"); + + const registration = appManifest.adminUiSdk?.registration ?? {}; + const actionPath = join(outputDir, "index.js"); + const registrationPath = join(generatedDir, REGISTRATION_FILE_NAME); + const registrationContents = stringify(registration, null, 2); + const formattedContent = await prettierFormat(template, actionPath); + + await writeFile(actionPath, formattedContent, "utf-8"); + await writeFile(registrationPath, registrationContents, "utf-8"); + consola.success( + `Generated registration action at ${relative(process.cwd(), actionPath)}`, + ); +} diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template index 7223be11a..23700af01 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template @@ -15,6 +15,8 @@ import { registrationRuntimeAction } from "@adobe/aio-commerce-lib-app/actions/registration"; -// {{REGISTRATION_JSON}} +// The registration config is always at this relative constant path from the action. +import registration from "../../registration.json" with { type: "json" }; -export const main = registrationRuntimeAction({ registration }); +const args = { registration }; +export const main = registrationRuntimeAction(args); 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..c95ffce81 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,21 +24,24 @@ 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(", ")}`, ), ); +const ColumnAlignSchema = v.picklist(["left", "right", "center"]); +const ViewButtonLevelSchema = v.picklist([-1, 0, 1]); const ColumnTypeSchema = v.picklist([ "boolean", "date", @@ -46,8 +49,6 @@ const ColumnTypeSchema = v.picklist([ "integer", "string", ]); -const ColumnAlignSchema = v.picklist(["left", "right", "center"]); -const ViewButtonLevelSchema = v.picklist([-1, 0, 1]); const MassActionConfirmSchema = v.object({ title: v.optional(nonEmptyStringValueSchema("confirm title")), @@ -58,12 +59,24 @@ const ViewButtonConfirmSchema = v.object({ message: v.optional(nonEmptyStringValueSchema("confirm message")), }); -const iframeActionEntries = { - displayIframe: v.optional(booleanValueSchema("displayIframe")), +const IframeBaseEntries = { timeout: v.optional(positiveNumberValueSchema("timeout")), +}; + +const IframeEnabledEntries = { + ...IframeBaseEntries, + displayIframe: v.literal(true), sandbox: v.optional(SandboxSchema), }; +const IframeDisabledEntries = { + ...IframeBaseEntries, + displayIframe: v.optional(v.literal(false)), + sandbox: v.optional( + v.never("sandbox is only relevant when displayIframe is set to true"), + ), +}; + const GridColumnPropertySchema = v.object({ label: nonEmptyStringValueSchema("column label"), columnId: nonEmptyStringValueSchema("column ID"), @@ -87,62 +100,21 @@ const massActionBaseEntries = { title: v.optional(nonEmptyStringValueSchema("mass action page title")), confirm: v.optional(MassActionConfirmSchema), path: nonEmptyStringValueSchema("mass action path"), - ...iframeActionEntries, -}; - -const SANDBOX_DISPLAY_IFRAME_MESSAGE = - "sandbox is only relevant when displayIframe is set to true"; - -type SandboxDisplayIframeInput = { - sandbox?: string | undefined; - displayIframe?: boolean | undefined; }; -// Defined once with a concrete input type; cast inside withSandboxDisplayIframeCheck -// to the actual schema output type (safe because every schema using this has both fields). -const sandboxDisplayIframeCheck = v.forward( - v.partialCheck< - SandboxDisplayIframeInput, - readonly [readonly ["sandbox"], readonly ["displayIframe"]], - SandboxDisplayIframeInput, - typeof SANDBOX_DISPLAY_IFRAME_MESSAGE - >( - [["sandbox"], ["displayIframe"]], - (input) => input.sandbox === undefined || input.displayIframe === true, - SANDBOX_DISPLAY_IFRAME_MESSAGE, - ), - ["sandbox"], -); - -function withSandboxDisplayIframeCheck< - TSchema extends v.BaseSchema< - unknown, - SandboxDisplayIframeInput, - v.BaseIssue - >, ->(schema: TSchema) { - return v.pipe( - schema, - // Cast is required: valibot's "~types" property is invariant, so a shared action - // constant cannot be assigned to PipeItem without this cast. - // Runtime behavior is identical to inlining the check in each schema. - sandboxDisplayIframeCheck as unknown as v.BaseValidation< - v.InferOutput, - v.InferOutput, - v.BaseIssue - >, - ); +function createIframeActionSchema( + schema: v.StrictObjectSchema, +) { + return v.variant("displayIframe", [ + v.strictObject({ ...schema.entries, ...IframeEnabledEntries }), + v.strictObject({ ...schema.entries, ...IframeDisabledEntries }), + ]); } -type SchemaEntries = Record< - string, - v.BaseSchema> ->; - -function createMassActionSchema( +function createMassActionSchema( variantEntries: TEntries, ) { - return withSandboxDisplayIframeCheck( + return createIframeActionSchema( v.strictObject({ ...massActionBaseEntries, ...variantEntries, @@ -166,15 +138,14 @@ const CustomerMassActionSchema = createMassActionSchema({ ), }); -const OrderViewButtonSchema = withSandboxDisplayIframeCheck( - v.object({ +const OrderViewButtonSchema = createIframeActionSchema( + v.strictObject({ buttonId: nonEmptyStringValueSchema("view button ID"), label: nonEmptyStringValueSchema("view button label"), confirm: v.optional(ViewButtonConfirmSchema), path: nonEmptyStringValueSchema("view button path"), level: v.optional(ViewButtonLevelSchema), sortOrder: v.optional(positiveNumberValueSchema("sortOrder")), - ...iframeActionEntries, }), ); diff --git a/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts b/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts index f6375e7eb..5a7e35e21 100644 --- a/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts +++ b/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts @@ -12,13 +12,26 @@ import { readFile, writeFile } from "node:fs/promises"; -const QUOTED_MENU_ITEMS_RE = /"menuItems":/u; +const { REGISTRATION_TEMPLATE } = vi.hoisted(() => ({ + REGISTRATION_TEMPLATE: [ + "// This file has been auto-generated by `@adobe/aio-commerce-lib-app`", + "// Do not modify this file directly", + "", + 'import { registrationRuntimeAction } from "@adobe/aio-commerce-lib-app/actions/registration";', + "", + "// The registration config is always at this relative constant path from the action.", + 'import registration from "../../registration.json" with { type: "json" };', + "", + "const args = { registration };", + "export const main = registrationRuntimeAction(args);", + ].join("\n"), +})); import { beforeEach, describe, expect, test, vi } from "vitest"; import { BACKEND_UI_EXTENSION_POINT_ID, - EXTENSIBILITY_EXTENSION_POINT_ID, + REGISTRATION_FILE_NAME, } from "#commands/constants"; import { CUSTOM_IMPORTS_PLACEHOLDER, @@ -224,7 +237,7 @@ describe("generateRegistrationActionFile", () => { vi.mocked(readFile).mockResolvedValue(templates.registration); }); - test("writes registration.js with inlined registration JSON", async () => { + test("writes registration.js that imports the generated registration JSON", async () => { const mockReadFile = vi.mocked(readFile); const mockWriteFile = vi.mocked(writeFile); @@ -234,7 +247,7 @@ describe("generateRegistrationActionFile", () => { ); expect(mockReadFile).toHaveBeenCalledOnce(); - expect(mockWriteFile).toHaveBeenCalledOnce(); + expect(mockWriteFile).toHaveBeenCalledTimes(2); const [_path, content] = mockWriteFile.mock.calls[0]; const contentStr = content as string; @@ -243,16 +256,30 @@ describe("generateRegistrationActionFile", () => { expect(contentStr).toContain( 'import { registrationRuntimeAction } from "@adobe/aio-commerce-lib-app/actions/registration"', ); - expect(contentStr).toContain("const registration ="); expect(contentStr).toContain( - "export const main = registrationRuntimeAction({ registration })", + 'import registration from "../../registration.json" with { type: "json" }', + ); + expect(contentStr).toContain( + "export const main = registrationRuntimeAction(args)", + ); + }); + + test("writes registration.json with the serialized registration", async () => { + const mockWriteFile = vi.mocked(writeFile); + + await generateRegistrationActionFile( + configWithFullAdminUiSdk, + BACKEND_UI_EXTENSION_POINT_ID, + ); + + const [filePath, content] = mockWriteFile.mock.calls[1]; + const contentStr = content as string; + const registration = JSON.parse(contentStr); + + expect(String(filePath)).toContain(REGISTRATION_FILE_NAME); + expect(registration).toStrictEqual( + configWithFullAdminUiSdk.adminUiSdk.registration, ); - expect(contentStr).toContain('"my-app::first"'); - expect(contentStr).toContain("selectionLimit: 1"); - expect(contentStr).toContain("productSelectLimit: 1"); - expect(contentStr).toContain("customerSelectLimit: 1"); - expect(contentStr).toContain("menuItems: ["); - expect(contentStr).not.toMatch(QUOTED_MENU_ITEMS_RE); }); test("writes to registration/index.js", async () => { From cbaaa7d10f1b3535aeb5e3b32d8e633633412e2b Mon Sep 17 00:00:00 2001 From: Ivan Porto Wigner Date: Mon, 27 Apr 2026 22:29:34 +0200 Subject: [PATCH 02/10] test: add positive iframe test cases --- .../unit/config/schema/admin-ui-sdk.test.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) 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: { From abfa9e9a98d96e9235e45949416cc4c7b368810e Mon Sep 17 00:00:00 2001 From: Ivan Porto Wigner Date: Tue, 28 Apr 2026 12:46:12 +0200 Subject: [PATCH 03/10] refactor: revert schema changes --- .../source/config/schema/admin-ui-sdk.ts | 84 +++++++++++++------ .../unit/config/schema/admin-ui-sdk.test.ts | 24 ++++++ 2 files changed, 81 insertions(+), 27 deletions(-) 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 c95ffce81..940777b77 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 @@ -40,8 +40,6 @@ const SandboxSchema = v.pipe( ), ); -const ColumnAlignSchema = v.picklist(["left", "right", "center"]); -const ViewButtonLevelSchema = v.picklist([-1, 0, 1]); const ColumnTypeSchema = v.picklist([ "boolean", "date", @@ -49,6 +47,8 @@ const ColumnTypeSchema = v.picklist([ "integer", "string", ]); +const ColumnAlignSchema = v.picklist(["left", "right", "center"]); +const ViewButtonLevelSchema = v.picklist([-1, 0, 1]); const MassActionConfirmSchema = v.object({ title: v.optional(nonEmptyStringValueSchema("confirm title")), @@ -59,24 +59,12 @@ const ViewButtonConfirmSchema = v.object({ message: v.optional(nonEmptyStringValueSchema("confirm message")), }); -const IframeBaseEntries = { +const iframeActionEntries = { + displayIframe: v.optional(booleanValueSchema("displayIframe")), timeout: v.optional(positiveNumberValueSchema("timeout")), -}; - -const IframeEnabledEntries = { - ...IframeBaseEntries, - displayIframe: v.literal(true), sandbox: v.optional(SandboxSchema), }; -const IframeDisabledEntries = { - ...IframeBaseEntries, - displayIframe: v.optional(v.literal(false)), - sandbox: v.optional( - v.never("sandbox is only relevant when displayIframe is set to true"), - ), -}; - const GridColumnPropertySchema = v.object({ label: nonEmptyStringValueSchema("column label"), columnId: nonEmptyStringValueSchema("column ID"), @@ -100,21 +88,62 @@ const massActionBaseEntries = { title: v.optional(nonEmptyStringValueSchema("mass action page title")), confirm: v.optional(MassActionConfirmSchema), path: nonEmptyStringValueSchema("mass action path"), + ...iframeActionEntries, }; -function createIframeActionSchema( - schema: v.StrictObjectSchema, -) { - return v.variant("displayIframe", [ - v.strictObject({ ...schema.entries, ...IframeEnabledEntries }), - v.strictObject({ ...schema.entries, ...IframeDisabledEntries }), - ]); +const SANDBOX_DISPLAY_IFRAME_MESSAGE = + "sandbox is only relevant when displayIframe is set to true"; + +type SandboxDisplayIframeInput = { + sandbox?: string | undefined; + displayIframe?: boolean | undefined; +}; + +// Defined once with a concrete input type; cast inside withSandboxDisplayIframeCheck +// to the actual schema output type (safe because every schema using this has both fields). +const sandboxDisplayIframeCheck = v.forward( + v.partialCheck< + SandboxDisplayIframeInput, + readonly [readonly ["sandbox"], readonly ["displayIframe"]], + SandboxDisplayIframeInput, + typeof SANDBOX_DISPLAY_IFRAME_MESSAGE + >( + [["sandbox"], ["displayIframe"]], + (input) => input.sandbox === undefined || input.displayIframe === true, + SANDBOX_DISPLAY_IFRAME_MESSAGE, + ), + ["sandbox"], +); + +function withSandboxDisplayIframeCheck< + TSchema extends v.BaseSchema< + unknown, + SandboxDisplayIframeInput, + v.BaseIssue + >, +>(schema: TSchema) { + return v.pipe( + schema, + // Cast is required: valibot's "~types" property is invariant, so a shared action + // constant cannot be assigned to PipeItem without this cast. + // Runtime behavior is identical to inlining the check in each schema. + sandboxDisplayIframeCheck as unknown as v.BaseValidation< + v.InferOutput, + v.InferOutput, + v.BaseIssue + >, + ); } -function createMassActionSchema( +type SchemaEntries = Record< + string, + v.BaseSchema> +>; + +function createMassActionSchema( variantEntries: TEntries, ) { - return createIframeActionSchema( + return withSandboxDisplayIframeCheck( v.strictObject({ ...massActionBaseEntries, ...variantEntries, @@ -138,14 +167,15 @@ const CustomerMassActionSchema = createMassActionSchema({ ), }); -const OrderViewButtonSchema = createIframeActionSchema( - v.strictObject({ +const OrderViewButtonSchema = withSandboxDisplayIframeCheck( + v.object({ buttonId: nonEmptyStringValueSchema("view button ID"), label: nonEmptyStringValueSchema("view button label"), confirm: v.optional(ViewButtonConfirmSchema), path: nonEmptyStringValueSchema("view button path"), level: v.optional(ViewButtonLevelSchema), sortOrder: v.optional(positiveNumberValueSchema("sortOrder")), + ...iframeActionEntries, }), ); 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 6a4dba9b4..34f0139a4 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 @@ -448,5 +448,29 @@ describe("AdminUiSdkSchema", () => { expect(result.success).toBe(false); } }); + + test("iframe action with non-boolean displayIframe returns a boolean error", () => { + const result = parseRegistration({ + order: { + massActions: [ + { + actionId: "app::action", + label: "Action", + path: "#/action", + displayIframe: 1, + }, + ], + }, + }); + + expect(result.success).toBe(false); + expect(result.issues).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + message: "Expected a boolean value for 'displayIframe'", + }), + ]), + ); + }); }); }); From f168d355c5c880c3f41c31cf46a41c67bde0a2f5 Mon Sep 17 00:00:00 2001 From: Ivan Porto Wigner Date: Tue, 28 Apr 2026 12:54:07 +0200 Subject: [PATCH 04/10] chore: add todo --- .../aio-commerce-lib-app/source/config/schema/admin-ui-sdk.ts | 1 + 1 file changed, 1 insertion(+) 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 940777b77..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 @@ -115,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, From 1025ac4dc60099e00a0054e1f6383c87a18a4b4a Mon Sep 17 00:00:00 2001 From: Ivan Porto Wigner Date: Tue, 28 Apr 2026 12:55:56 +0200 Subject: [PATCH 05/10] refactor: remove useless intermediate constant --- .../actions/templates/admin-ui-sdk/registration.js.template | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template index 23700af01..0b7b9225c 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template @@ -18,5 +18,4 @@ import { registrationRuntimeAction } from "@adobe/aio-commerce-lib-app/actions/r // The registration config is always at this relative constant path from the action. import registration from "../../registration.json" with { type: "json" }; -const args = { registration }; -export const main = registrationRuntimeAction(args); +export const main = registrationRuntimeAction({ registration }); From 36d849ac55272b888b1151b5e992db2f6706666b Mon Sep 17 00:00:00 2001 From: Ivan Porto Wigner Date: Tue, 28 Apr 2026 22:55:56 +0200 Subject: [PATCH 06/10] chore: checkout files from main --- .../commands/generate/actions/config.ts | 1 + .../source/commands/generate/actions/main.ts | 229 +----------------- .../admin-ui-sdk/registration.js.template | 3 +- 3 files changed, 7 insertions(+), 226 deletions(-) diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts b/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts index dcbc10585..b1b444acc 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts @@ -56,6 +56,7 @@ export const COMMERCE_ACTION_INPUTS = Object.fromEntries( export const CUSTOM_IMPORTS_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_IMPORTS}}"; export const CUSTOM_SCRIPTS_MAP_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_MAP}}"; export const CUSTOM_SCRIPTS_LOADER_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_LOADER}}"; +export const REGISTRATION_JSON_PLACEHOLDER = "// {{REGISTRATION_JSON}}"; /** * Creates a runtime action configuration. diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts b/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts index 2a429fc6d..101627aa2 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts @@ -1,29 +1,21 @@ import { CommerceSdkValidationError } from "@adobe/aio-commerce-lib-core/error"; import { consola } from "consola"; -import { formatTree } from "consola/utils"; -import { stringify } from "safe-stable-stringify"; import { BACKEND_UI_EXTENSION_POINT_ID, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, - GENERATED_ACTIONS_PATH, - getExtensionPointFolderPath, - REGISTRATION_FILE_NAME, } from "#commands/constants"; import { loadAppManifest } from "#commands/utils"; import { hasAdminUiSdk, hasBusinessConfigSchema } from "#config/index"; import { getRuntimeActions } from "./config"; import { - buildAdminUiSdkExtConfig, - buildAppManagementExtConfig, - buildBusinessConfigurationExtConfig, - CUSTOM_IMPORTS_PLACEHOLDER, - CUSTOM_SCRIPTS_LOADER_PLACEHOLDER, - CUSTOM_SCRIPTS_MAP_PLACEHOLDER, - getRuntimeActions, -} from "./config"; + generateActionFiles, + generateRegistrationActionFile, + TEMPLATES_DIR, + updateExtConfig, +} from "./lib"; import type { CommerceAppConfigOutputModel } from "#config/schema/app"; @@ -87,214 +79,3 @@ export async function exec() { process.exit(1); } } - -/** Update the ext.config.yaml file */ -async function updateExtConfig( - appConfig: CommerceAppConfigOutputModel, - extensionPointId: ValidExtensionPointId, -) { - consola.info(`Updating ext.config.yaml for ${extensionPointId}...`); - const extensionPointFolderPath = - getExtensionPointFolderPath(extensionPointId); - - const outputDir = await makeOutputDirFor(extensionPointFolderPath); - const extConfigPath = join(outputDir, "ext.config.yaml"); - const extConfigDoc = await readYamlFile(extConfigPath); - - let extConfig: ExtConfig; - switch (extensionPointId) { - case EXTENSIBILITY_EXTENSION_POINT_ID: { - extConfig = buildAppManagementExtConfig(appConfig); - break; - } - - case CONFIGURATION_EXTENSION_POINT_ID: { - extConfig = buildBusinessConfigurationExtConfig(); - break; - } - - case BACKEND_UI_EXTENSION_POINT_ID: { - extConfig = buildAdminUiSdkExtConfig(); - break; - } - - default: { - throw new Error(`Unsupported extension point ID: ${extensionPointId}`); - } - } - - await createOrUpdateExtConfig(extConfigPath, extConfig, extConfigDoc); - return extConfig; -} - -/** Generate the action files */ -async function generateActionFiles( - appManifest: CommerceAppConfigOutputModel, - actions: TemplateAction[], - extensionPointId: ValidExtensionPointId, -) { - consola.start("Generating runtime actions..."); - const extensionPointFolderPath = - getExtensionPointFolderPath(extensionPointId); - - const outputDir = await makeOutputDirFor( - join(extensionPointFolderPath, GENERATED_ACTIONS_PATH), - ); - - const outputFiles: string[] = []; - const templatesDir = join(__dirname, "generate/actions/templates"); - - for (const action of actions) { - const templatePath = join(templatesDir, action.templateFile); - let template = await readFile(templatePath, "utf-8"); - - // For installation action, inject custom script imports - if (action.name === "installation") { - const customScriptsTemplatePath = join( - templatesDir, - "app-management", - "custom-scripts.js.template", - ); - - const scriptsTemplate = await generateCustomScriptsTemplate( - await readFile(customScriptsTemplatePath, "utf-8"), - appManifest, - ); - - template = applyCustomScripts(template, scriptsTemplate); - } - - const actionPath = join(outputDir, `${action.name}.js`); - - await writeFile(actionPath, template, "utf-8"); - outputFiles.push(` ${relative(process.cwd(), actionPath)}`); - } - - consola.success(`Generated ${actions.length} action(s)`); - consola.log.raw(formatTree(outputFiles)); -} - -/** - * Applies the given custom scripts template code to the given installation template. - * @param installationTemplate - The installation code runtime action template - * @param customScriptsTemplate - The custom scripts dynamically generated template. - */ -export function applyCustomScripts( - installationTemplate: string, - customScriptsTemplate: string | null, -) { - // There are scripts file to include. - if (customScriptsTemplate !== null) { - return installationTemplate - .replace(CUSTOM_SCRIPTS_LOADER_PLACEHOLDER, customScriptsTemplate) - .replace( - "const args = { appConfig };", - "const args = { appConfig, customScriptsLoader };", - ); - } - // No custom scripts, remove the loader references - consola.debug( - "No custom installation steps found, skipping custom-scripts.js generation...", - ); - - return installationTemplate.replace( - CUSTOM_SCRIPTS_LOADER_PLACEHOLDER, - "// No custom installation scripts configured", - ); -} - -/** - * Generate the installation template with dynamic custom script imports - */ -export async function generateCustomScriptsTemplate( - template: string, - appManifest: CommerceAppConfigOutputModel, -) { - if (!hasCustomInstallationSteps(appManifest)) { - return null; - } - - // The generated installation action with will be at: - // src/commerce-extensibility-1/.generated/actions/.generated/app-management - // We need to resolve paths from project root to relative imports from this location - const projectRoot = await getProjectRootDirectory(); - const installationActionDir = join( - projectRoot, - getExtensionPointFolderPath(EXTENSIBILITY_EXTENSION_POINT_ID), - GENERATED_ACTIONS_PATH, - ); - - // Generate import statements - const customSteps = appManifest.installation.customInstallationSteps; - const importStatements = customSteps - .map((step: CustomInstallationStep, index: number) => { - // step.script is relative to project root (e.g., "./scripts/setup.js") - const absoluteScriptPath = join(projectRoot, step.script); - let relativeImportPath = relative( - installationActionDir, - absoluteScriptPath, - ); - if (!relativeImportPath.startsWith(".")) { - relativeImportPath = `./${relativeImportPath}`; - } - relativeImportPath = relativeImportPath.replace(/\\/g, "/"); - - const importName = `customScript${index}`; - return `import * as ${importName} from "${relativeImportPath}";`; - }) - .join("\n"); - - // Generate the loadCustomInstallationScripts function - const scriptMap = customSteps - .map((step: CustomInstallationStep, index: number) => { - const scriptPath = step.script; - const importName = `customScript${index}`; - const entry = `"${scriptPath}": ${importName},`; - - return entry.padStart(entry.length + 6); // add indentation - }) - .join("\n"); - - // Inject imports and function into template - const result = template.replace(CUSTOM_IMPORTS_PLACEHOLDER, importStatements); - return result.replace(CUSTOM_SCRIPTS_MAP_PLACEHOLDER, scriptMap); -} - -/** - * Generates `registration/index.js` with the Admin UI SDK registration config inlined as a JS object literal. - * @param appManifest - The validated app config; must satisfy `hasAdminUiSdk`. - * @param extensionPointId - The extension point ID that owns the registration action. - */ -export async function generateRegistrationActionFile( - appManifest: CommerceAppConfigOutputModel, - extensionPointId: ValidExtensionPointId, -) { - consola.start("Generating Admin UI SDK registration action..."); - const extensionPointFolderPath = - getExtensionPointFolderPath(extensionPointId); - const generatedDir = await makeOutputDirFor( - join(extensionPointFolderPath, ".generated"), - ); - - const outputDir = await makeOutputDirFor( - join(extensionPointFolderPath, ADMIN_UI_SDK_ACTIONS_PATH), - ); - - const templatePath = join( - __dirname, - "generate/actions/templates/admin-ui-sdk/registration.js.template", - ); - const template = await readFile(templatePath, "utf-8"); - - const registration = appManifest.adminUiSdk?.registration ?? {}; - const actionPath = join(outputDir, "index.js"); - const registrationPath = join(generatedDir, REGISTRATION_FILE_NAME); - const registrationContents = stringify(registration, null, 2); - const formattedContent = await prettierFormat(template, actionPath); - - await writeFile(actionPath, formattedContent, "utf-8"); - await writeFile(registrationPath, registrationContents, "utf-8"); - consola.success( - `Generated registration action at ${relative(process.cwd(), actionPath)}`, - ); -} diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template index 0b7b9225c..7223be11a 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template @@ -15,7 +15,6 @@ import { registrationRuntimeAction } from "@adobe/aio-commerce-lib-app/actions/registration"; -// The registration config is always at this relative constant path from the action. -import registration from "../../registration.json" with { type: "json" }; +// {{REGISTRATION_JSON}} export const main = registrationRuntimeAction({ registration }); From c4e48280f719dc119da8acab57f6bbb8b2a8d831 Mon Sep 17 00:00:00 2001 From: Ivan Porto Wigner Date: Wed, 29 Apr 2026 10:20:42 +0200 Subject: [PATCH 07/10] refactor: consistency fixes --- .../source/commands/constants.ts | 2 +- .../commands/generate/actions/config.ts | 47 ++++--- .../source/commands/generate/actions/lib.ts | 83 +++++------- .../source/commands/generate/actions/main.ts | 17 ++- .../admin-ui-sdk/registration.js.template | 5 +- .../source/commands/hooks/pre-app-build.ts | 17 ++- .../source/commands/utils.ts | 19 +-- .../test/fixtures/project.ts | 3 +- .../commands/generate/actions.test.ts | 24 +++- .../commands/hooks/pre-app-build.test.ts | 23 +++- .../commands/generate/actions/lib.test.ts | 126 ++++-------------- 11 files changed, 170 insertions(+), 196 deletions(-) diff --git a/packages/aio-commerce-lib-app/source/commands/constants.ts b/packages/aio-commerce-lib-app/source/commands/constants.ts index 485c0c9cd..14c9130c4 100644 --- a/packages/aio-commerce-lib-app/source/commands/constants.ts +++ b/packages/aio-commerce-lib-app/source/commands/constants.ts @@ -35,7 +35,7 @@ export const BACKEND_UI_EXTENSION_POINT_ID = "commerce/backend-ui/1"; export const ADMIN_UI_SDK_PACKAGE_NAME = "admin-ui-sdk"; /** The path to the directory containing the generated Admin UI SDK actions. */ -export const ADMIN_UI_SDK_ACTIONS_PATH = `${GENERATED_PATH}/actions/registration`; +export const ADMIN_UI_SDK_ACTIONS_PATH = `${GENERATED_PATH}/actions`; /** The name of the configuration schema file */ export const APP_MANIFEST_FILE = "app.commerce.manifest.json"; diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts b/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts index b1b444acc..3194f44cc 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts @@ -31,6 +31,7 @@ import type { CommerceAppConfigDomain } from "#config/schema/domains"; type ActionConfig = { requiresSchema?: boolean; requiresEncryptionKey?: boolean; + generatedBasePath?: string; }; export type TemplateAction = ActionConfig & { @@ -71,7 +72,7 @@ function createActionDefinition( const def: ActionDefinition = { ...options, - function: `${GENERATED_ACTIONS_PATH}/${actionName}.js`, + function: `${config.generatedBasePath ?? GENERATED_ACTIONS_PATH}/${actionName}.js`, web: options.web ?? "yes", runtime: "nodejs:22", annotations: { @@ -93,15 +94,25 @@ function createActionDefinition( /** * Gets the runtime actions to be generated from the ext.config.yaml configuration. * @param extConfig - The ext.config.yaml configuration. + * @param dir - The directory where the action templates are located. + * @param packageName - The name of the package containing the actions (default: "app-management"). */ -export function getRuntimeActions(extConfig: ExtConfig, dir: string) { +export function getRuntimeActions( + extConfig: ExtConfig, + dir: string, + packageName = PACKAGE_NAME, +) { return Object.entries( - extConfig.runtimeManifest?.packages?.[PACKAGE_NAME]?.actions ?? {}, + extConfig.runtimeManifest?.packages?.[packageName]?.actions ?? {}, ).map( ([name, _]) => ({ name, templateFile: join(dir, `${name}.js.template`), + generatedBasePath: + packageName === ADMIN_UI_SDK_PACKAGE_NAME + ? ADMIN_UI_SDK_ACTIONS_PATH + : undefined, }) satisfies TemplateAction, ); } @@ -220,10 +231,17 @@ export function buildBusinessConfigurationExtConfig() { } satisfies ExtConfig; } -/** - * Builds the ext.config.yaml configuration for the Admin UI SDK backend-ui extension. - */ +/** Builds the ext.config.yaml configuration for the Admin UI SDK backend-ui extension. */ export function buildAdminUiSdkExtConfig() { + const actions = [ + { + name: "registration", + templateFile: "registration.js.template", + requiresEncryptionKey: true, + generatedBasePath: ADMIN_UI_SDK_ACTIONS_PATH, + }, + ] satisfies TemplateAction[]; + return { hooks: { "pre-app-build": @@ -239,17 +257,12 @@ export function buildAdminUiSdkExtConfig() { packages: { [ADMIN_UI_SDK_PACKAGE_NAME]: { license: "Apache-2.0", - actions: { - registration: { - function: `${ADMIN_UI_SDK_ACTIONS_PATH}/index.js`, - web: "yes", - runtime: "nodejs:22", - annotations: { - "require-adobe-auth": true, - final: true, - }, - }, - } satisfies Record, + actions: Object.fromEntries( + actions.map((action) => [ + action.name, + createActionDefinition(action.name, action), + ]), + ), }, }, }, 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..2330382fe 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 @@ -28,14 +28,12 @@ import { CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, getExtensionPointFolderPath, + REGISTRATION_FILE_NAME, } from "#commands/constants"; import { - getActionPath, getActionsDir, - getAdminUiSdkActionsDir, - getAdminUiSdkRegistrationActionPath, getExtConfigPath, - prettierFormat, + getGeneratedDir, } from "#commands/utils"; import { hasCustomInstallationSteps } from "#config/index"; @@ -46,7 +44,6 @@ import { CUSTOM_IMPORTS_PLACEHOLDER, CUSTOM_SCRIPTS_LOADER_PLACEHOLDER, CUSTOM_SCRIPTS_MAP_PLACEHOLDER, - REGISTRATION_JSON_PLACEHOLDER, } from "./config"; import type { ExtConfig } from "@aio-commerce-sdk/scripting-utils/yaml"; @@ -141,7 +138,6 @@ export async function generateActionFiles( ) { consola.start("Generating runtime actions..."); - await makeOutputDirFor(getActionsDir(extensionPointId)); const projectRoot = await getProjectRootDirectory(); const outputFiles: string[] = []; @@ -149,6 +145,15 @@ export async function generateActionFiles( const templatePath = join(templatesDir, action.templateFile); let template = await readFile(templatePath, "utf-8"); + const outputDir = action.generatedBasePath + ? join( + getExtensionPointFolderPath(extensionPointId), + action.generatedBasePath, + ) + : getActionsDir(extensionPointId); + + await makeOutputDirFor(outputDir); + // For installation action, inject custom script imports if (action.name === "installation") { const customScriptsTemplatePath = join( @@ -165,12 +170,9 @@ export async function generateActionFiles( template = applyCustomScripts(template, scriptsTemplate); } - const actionPath = join( - projectRoot, - getActionPath(extensionPointId, action.name), - ); - + const actionPath = join(projectRoot, outputDir, `${action.name}.js`); await writeFile(actionPath, template, "utf-8"); + outputFiles.push(` ${relative(process.cwd(), actionPath)}`); } @@ -178,6 +180,24 @@ export async function generateActionFiles( consola.log.raw(formatTree(outputFiles)); } +/** Generate the backend-ui registration JSON consumed by the generated action. */ +export async function generateRegistrationJson(appManifest: AdminUiSdkConfig) { + await makeOutputDirFor(getGeneratedDir(BACKEND_UI_EXTENSION_POINT_ID)); + const registrationJsonPath = join( + await getProjectRootDirectory(), + getGeneratedDir(BACKEND_UI_EXTENSION_POINT_ID), + REGISTRATION_FILE_NAME, + ); + + await writeFile( + registrationJsonPath, + JSON.stringify(appManifest.adminUiSdk.registration, null, 2), + "utf-8", + ); + + return registrationJsonPath; +} + /** * Applies the given custom scripts template code to the given installation template. * @param installationTemplate - The installation code runtime action template @@ -263,44 +283,3 @@ export async function generateCustomScriptsTemplate( const result = template.replace(CUSTOM_IMPORTS_PLACEHOLDER, importStatements); return result.replace(CUSTOM_SCRIPTS_MAP_PLACEHOLDER, scriptMap); } - -/** - * Generates `registration/index.js` with the Admin UI SDK registration config - * inlined as a JS object literal. - * @param appManifest - The validated app config; must satisfy `hasAdminUiSdk`. - * @param extensionPointId - The extension point ID that owns the registration action. - * @param templatesDir - The root directory containing the action templates. - */ -export async function generateRegistrationActionFile( - appManifest: AdminUiSdkConfig, - extensionPointId: typeof BACKEND_UI_EXTENSION_POINT_ID, - templatesDir = TEMPLATES_DIR, -) { - consola.start("Generating Admin UI SDK registration action..."); - - await makeOutputDirFor(getAdminUiSdkActionsDir(extensionPointId)); - const projectRoot = await getProjectRootDirectory(); - const templatePath = join( - templatesDir, - "admin-ui-sdk", - "registration.js.template", - ); - const template = await readFile(templatePath, "utf-8"); - - const { registration } = appManifest.adminUiSdk; - const actionPath = join( - projectRoot, - getAdminUiSdkRegistrationActionPath(extensionPointId), - ); - const content = template.replace( - REGISTRATION_JSON_PLACEHOLDER, - `const registration = ${JSON.stringify(registration)};`, - ); - - const formattedContent = await prettierFormat(content, actionPath); - - await writeFile(actionPath, formattedContent, "utf-8"); - consola.success( - `Generated registration action at ${relative(process.cwd(), actionPath)}`, - ); -} diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts b/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts index 101627aa2..41bb51c92 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts @@ -2,6 +2,7 @@ import { CommerceSdkValidationError } from "@adobe/aio-commerce-lib-core/error"; import { consola } from "consola"; import { + ADMIN_UI_SDK_PACKAGE_NAME, BACKEND_UI_EXTENSION_POINT_ID, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, @@ -12,7 +13,7 @@ import { hasAdminUiSdk, hasBusinessConfigSchema } from "#config/index"; import { getRuntimeActions } from "./config"; import { generateActionFiles, - generateRegistrationActionFile, + generateRegistrationJson, TEMPLATES_DIR, updateExtConfig, } from "./lib"; @@ -56,10 +57,20 @@ export async function run( } if (hasAdminUiSdk(appManifest)) { - await updateExtConfig(appManifest, BACKEND_UI_EXTENSION_POINT_ID); - await generateRegistrationActionFile( + const adminUiExtConfig = await updateExtConfig( appManifest, BACKEND_UI_EXTENSION_POINT_ID, + ); + + await generateRegistrationJson(appManifest); + await generateActionFiles( + appManifest, + getRuntimeActions( + adminUiExtConfig, + "admin-ui-sdk", + ADMIN_UI_SDK_PACKAGE_NAME, + ), + BACKEND_UI_EXTENSION_POINT_ID, templatesDir, ); } diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template index 7223be11a..d0fc3f356 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template @@ -15,6 +15,7 @@ import { registrationRuntimeAction } from "@adobe/aio-commerce-lib-app/actions/registration"; -// {{REGISTRATION_JSON}} +// The registration config is always at this relative constant path from the action. +import registration from "../registration.json" with { type: "json" }; -export const main = registrationRuntimeAction({ registration }); +export const main = registrationRuntimeAction({ registration }); \ No newline at end of file diff --git a/packages/aio-commerce-lib-app/source/commands/hooks/pre-app-build.ts b/packages/aio-commerce-lib-app/source/commands/hooks/pre-app-build.ts index dce6d23c7..3c2d2f800 100644 --- a/packages/aio-commerce-lib-app/source/commands/hooks/pre-app-build.ts +++ b/packages/aio-commerce-lib-app/source/commands/hooks/pre-app-build.ts @@ -15,6 +15,7 @@ import { syncImsCredentials } from "@aio-commerce-sdk/scripting-utils/env"; import consola from "consola"; import { + ADMIN_UI_SDK_PACKAGE_NAME, BACKEND_UI_EXTENSION_POINT_ID, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, @@ -22,7 +23,7 @@ import { import { getRuntimeActions } from "#commands/generate/actions/config"; import { generateActionFiles, - generateRegistrationActionFile, + generateRegistrationJson, readExtConfig, TEMPLATES_DIR, } from "#commands/generate/actions/lib"; @@ -86,11 +87,23 @@ export async function run(extension: Extension, templatesDir = TEMPLATES_DIR) { if (extension === "backend-ui/1") { if (hasAdminUiSdk(appManifest)) { - await generateRegistrationActionFile( + const { doc: adminUiSdkExtConfig } = await readExtConfig( + BACKEND_UI_EXTENSION_POINT_ID, + ); + + await generateRegistrationJson(appManifest); + await generateActionFiles( appManifest, + getRuntimeActions( + adminUiSdkExtConfig.toJS() as ExtConfig, + "admin-ui-sdk", + ADMIN_UI_SDK_PACKAGE_NAME, + ), BACKEND_UI_EXTENSION_POINT_ID, + templatesDir, ); } + return; } diff --git a/packages/aio-commerce-lib-app/source/commands/utils.ts b/packages/aio-commerce-lib-app/source/commands/utils.ts index 5aaeb1010..1d873aa2b 100644 --- a/packages/aio-commerce-lib-app/source/commands/utils.ts +++ b/packages/aio-commerce-lib-app/source/commands/utils.ts @@ -20,6 +20,7 @@ import { parseCommerceAppConfig } from "#config/index"; import { ADMIN_UI_SDK_ACTIONS_PATH, APP_MANIFEST_FILE, + BACKEND_UI_EXTENSION_POINT_ID, CONFIG_SCHEMA_FILE_NAME, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, @@ -89,23 +90,17 @@ export function getExtConfigPath(extensionPointId: string) { return join(getExtensionPointFolderPath(extensionPointId), "ext.config.yaml"); } -/** - * Path to an Admin UI SDK generated actions directory, relative to the project root. - * @param extensionPointId - The extension point ID, e.g. "commerce/backend-ui/1" - */ -export function getAdminUiSdkActionsDir(extensionPointId: string) { +/** Path to an Admin UI SDK generated actions directory, relative to the project root. */ +export function getAdminUiSdkActionsDir() { return join( - getExtensionPointFolderPath(extensionPointId), + getExtensionPointFolderPath(BACKEND_UI_EXTENSION_POINT_ID), ADMIN_UI_SDK_ACTIONS_PATH, ); } -/** - * Path to the generated Admin UI SDK registration action file, relative to the project root. - * @param extensionPointId - The extension point ID, e.g. "commerce/backend-ui/1" - */ -export function getAdminUiSdkRegistrationActionPath(extensionPointId: string) { - return join(getAdminUiSdkActionsDir(extensionPointId), "index.js"); +/** Path to the generated Admin UI SDK registration action file, relative to the project root. */ +export function getAdminUiSdkRegistrationActionPath() { + return join(getAdminUiSdkActionsDir(), "registration.js"); } /** Path to the generated app manifest file, relative to the project root. */ diff --git a/packages/aio-commerce-lib-app/test/fixtures/project.ts b/packages/aio-commerce-lib-app/test/fixtures/project.ts index 793c92123..1888de8b7 100644 --- a/packages/aio-commerce-lib-app/test/fixtures/project.ts +++ b/packages/aio-commerce-lib-app/test/fixtures/project.ts @@ -106,11 +106,12 @@ export function envObject(env: Record): string { export function makeExtConfigFile( extensionPointId: string, actionNames: string[], + packageName = PACKAGE_NAME, ) { const extConfig = { runtimeManifest: { packages: { - [PACKAGE_NAME]: { + [packageName]: { actions: Object.fromEntries( actionNames.map((actionName) => [actionName, {}]), ), diff --git a/packages/aio-commerce-lib-app/test/integration/commands/generate/actions.test.ts b/packages/aio-commerce-lib-app/test/integration/commands/generate/actions.test.ts index 3fe7ea4f8..587b64ca4 100644 --- a/packages/aio-commerce-lib-app/test/integration/commands/generate/actions.test.ts +++ b/packages/aio-commerce-lib-app/test/integration/commands/generate/actions.test.ts @@ -23,7 +23,10 @@ import { getExtensionPointFolderPath, } from "#commands/constants"; import { exec, run } from "#commands/generate/actions/main"; -import { getAdminUiSdkRegistrationActionPath } from "#commands/utils"; +import { + getAdminUiSdkRegistrationActionPath, + getGeneratedDir, +} from "#commands/utils"; import { makeTemplateFiles } from "#test/fixtures/commands"; import { configWithBusinessConfig, @@ -124,7 +127,7 @@ describe("commands/generate/actions", () => { ); }); - test("generates backend-ui registration action when adminUiSdk is configured", async () => { + test("generates backend-ui registration action and registration json when adminUiSdk is configured", async () => { await withTempProject( { ...EMPTY_PROJECT, ...makeTemplateFiles() }, async (tempDir) => { @@ -132,20 +135,31 @@ describe("commands/generate/actions", () => { const registrationPath = join( tempDir, - getAdminUiSdkRegistrationActionPath(BACKEND_UI_EXTENSION_POINT_ID), + getAdminUiSdkRegistrationActionPath(), ); + const extConfigPath = join( tempDir, getExtensionPointFolderPath(BACKEND_UI_EXTENSION_POINT_ID), "ext.config.yaml", ); + const registrationJsonPath = join( + tempDir, + getGeneratedDir(BACKEND_UI_EXTENSION_POINT_ID), + "registration.json", + ); + expect(existsSync(registrationPath)).toBe(true); expect(existsSync(extConfigPath)).toBe(true); + expect(existsSync(registrationJsonPath)).toBe(true); const content = await readFile(registrationPath, "utf-8"); - expect(content).toContain("registrationRuntimeAction"); - expect(content).toContain("my-app::first"); + expect(content).toContain('with { type: "json" }'); + + expect( + JSON.parse(await readFile(registrationJsonPath, "utf-8")), + ).toStrictEqual(configWithFullAdminUiSdk.adminUiSdk.registration); }, ); }); diff --git a/packages/aio-commerce-lib-app/test/integration/commands/hooks/pre-app-build.test.ts b/packages/aio-commerce-lib-app/test/integration/commands/hooks/pre-app-build.test.ts index f55896116..6525c4b0a 100644 --- a/packages/aio-commerce-lib-app/test/integration/commands/hooks/pre-app-build.test.ts +++ b/packages/aio-commerce-lib-app/test/integration/commands/hooks/pre-app-build.test.ts @@ -17,6 +17,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, test, vi } from "vitest"; import { + ADMIN_UI_SDK_PACKAGE_NAME, BACKEND_UI_EXTENSION_POINT_ID, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, @@ -24,6 +25,7 @@ import { import { exec, run } from "#commands/hooks/pre-app-build"; import { getAdminUiSdkRegistrationActionPath, + getGeneratedDir, getManifestPath, getSchemaPath, } from "#commands/utils"; @@ -111,24 +113,39 @@ describe("commands/hooks/pre-app-build", () => { }); test("generates backend-ui registration action for backend-ui/1", async () => { + const actions = ["registration"]; await withTempProject( { ...makeProjectFiles(configWithFullAdminUiSdk), ...makeTemplateFiles(), + ...makeExtConfigFile( + BACKEND_UI_EXTENSION_POINT_ID, + actions, + ADMIN_UI_SDK_PACKAGE_NAME, + ), }, async (tempDir) => { await run("backend-ui/1"); const registrationPath = join( tempDir, - getAdminUiSdkRegistrationActionPath(BACKEND_UI_EXTENSION_POINT_ID), + getAdminUiSdkRegistrationActionPath(), + ); + const registrationJsonPath = join( + tempDir, + getGeneratedDir(BACKEND_UI_EXTENSION_POINT_ID), + "registration.json", ); expect(existsSync(registrationPath)).toBe(true); + expect(existsSync(registrationJsonPath)).toBe(true); const content = await readFile(registrationPath, "utf-8"); expect(content).toContain("registrationRuntimeAction"); - expect(content).toContain("my-app::first"); + expect(content).toContain('with { type: "json" }'); + expect( + JSON.parse(await readFile(registrationJsonPath, "utf-8")), + ).toStrictEqual(configWithFullAdminUiSdk.adminUiSdk.registration); }, ); }); @@ -141,7 +158,7 @@ describe("commands/hooks/pre-app-build", () => { const registrationPath = join( tempDir, - getAdminUiSdkRegistrationActionPath(BACKEND_UI_EXTENSION_POINT_ID), + getAdminUiSdkRegistrationActionPath(), ); expect(existsSync(registrationPath)).toBe(false); diff --git a/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts b/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts index 5a7e35e21..3244a9cec 100644 --- a/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts +++ b/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts @@ -1,38 +1,8 @@ -/* - * Copyright 2026 Adobe. All rights reserved. - * This file is licensed to you under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. You may obtain a copy - * of the License at http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software distributed under - * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS - * OF ANY KIND, either express or implied. See the License for the specific language - * governing permissions and limitations under the License. - */ - -import { readFile, writeFile } from "node:fs/promises"; - -const { REGISTRATION_TEMPLATE } = vi.hoisted(() => ({ - REGISTRATION_TEMPLATE: [ - "// This file has been auto-generated by `@adobe/aio-commerce-lib-app`", - "// Do not modify this file directly", - "", - 'import { registrationRuntimeAction } from "@adobe/aio-commerce-lib-app/actions/registration";', - "", - "// The registration config is always at this relative constant path from the action.", - 'import registration from "../../registration.json" with { type: "json" };', - "", - "const args = { registration };", - "export const main = registrationRuntimeAction(args);", - ].join("\n"), -})); +import { writeFile } from "node:fs/promises"; -import { beforeEach, describe, expect, test, vi } from "vitest"; +import { describe, expect, test, vi } from "vitest"; -import { - BACKEND_UI_EXTENSION_POINT_ID, - REGISTRATION_FILE_NAME, -} from "#commands/constants"; +import { EXTENSIBILITY_EXTENSION_POINT_ID } from "#commands/constants"; import { CUSTOM_IMPORTS_PLACEHOLDER, CUSTOM_SCRIPTS_LOADER_PLACEHOLDER, @@ -41,7 +11,7 @@ import { import { applyCustomScripts, generateCustomScriptsTemplate, - generateRegistrationActionFile, + generateRegistrationJson, readExtConfig, } from "#commands/generate/actions/lib"; import { templates } from "#test/fixtures/commands"; @@ -87,6 +57,30 @@ describe("readExtConfig", () => { }); }); +describe("generateRegistrationJson", () => { + test("writes the generated backend-ui registration json", async () => { + const mockWriteFile = vi.mocked(writeFile); + + const registrationJsonPath = await generateRegistrationJson( + configWithFullAdminUiSdk, + ); + + expect(mockWriteFile).toHaveBeenCalledOnce(); + expect(registrationJsonPath).toContain( + "/src/commerce-backend-ui-1/.generated/registration.json", + ); + + const [filePath, content] = mockWriteFile.mock.calls[0]; + expect(String(filePath)).toContain( + "/src/commerce-backend-ui-1/.generated/registration.json", + ); + + expect(JSON.parse(String(content))).toStrictEqual( + configWithFullAdminUiSdk.adminUiSdk.registration, + ); + }); +}); + describe("applyCustomScripts", () => { describe("when no custom installation steps are configured", () => { test("should not replace args or import customScriptsLoader", async () => { @@ -230,67 +224,3 @@ describe("generateCustomScriptsTemplate", () => { }); }); }); - -describe("generateRegistrationActionFile", () => { - beforeEach(() => { - vi.clearAllMocks(); - vi.mocked(readFile).mockResolvedValue(templates.registration); - }); - - test("writes registration.js that imports the generated registration JSON", async () => { - const mockReadFile = vi.mocked(readFile); - const mockWriteFile = vi.mocked(writeFile); - - await generateRegistrationActionFile( - configWithFullAdminUiSdk, - BACKEND_UI_EXTENSION_POINT_ID, - ); - - expect(mockReadFile).toHaveBeenCalledOnce(); - expect(mockWriteFile).toHaveBeenCalledTimes(2); - - const [_path, content] = mockWriteFile.mock.calls[0]; - const contentStr = content as string; - - expect(contentStr).toContain("// This file has been auto-generated"); - expect(contentStr).toContain( - 'import { registrationRuntimeAction } from "@adobe/aio-commerce-lib-app/actions/registration"', - ); - expect(contentStr).toContain( - 'import registration from "../../registration.json" with { type: "json" }', - ); - expect(contentStr).toContain( - "export const main = registrationRuntimeAction(args)", - ); - }); - - test("writes registration.json with the serialized registration", async () => { - const mockWriteFile = vi.mocked(writeFile); - - await generateRegistrationActionFile( - configWithFullAdminUiSdk, - BACKEND_UI_EXTENSION_POINT_ID, - ); - - const [filePath, content] = mockWriteFile.mock.calls[1]; - const contentStr = content as string; - const registration = JSON.parse(contentStr); - - expect(String(filePath)).toContain(REGISTRATION_FILE_NAME); - expect(registration).toStrictEqual( - configWithFullAdminUiSdk.adminUiSdk.registration, - ); - }); - - test("writes to registration/index.js", async () => { - const mockWriteFile = vi.mocked(writeFile); - - await generateRegistrationActionFile( - configWithFullAdminUiSdk, - BACKEND_UI_EXTENSION_POINT_ID, - ); - - const [filePath] = mockWriteFile.mock.calls[0]; - expect(String(filePath)).toContain("index.js"); - }); -}); From 99b7b3674cfe69c76b1491784287ece716f8350a Mon Sep 17 00:00:00 2001 From: Ivan Porto Wigner Date: Wed, 29 Apr 2026 17:24:50 +0200 Subject: [PATCH 08/10] fix: don't generate empty yaml keys --- .../scripting-utils/source/yaml/codegen.ts | 77 +++++++++++-------- .../scripting-utils/test/yaml/codegen.test.ts | 9 ++- 2 files changed, 53 insertions(+), 33 deletions(-) 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: []"); }); }); From 1c6bd3687f9292013874ff5bbc7a10de0e9cc5e3 Mon Sep 17 00:00:00 2001 From: Ivan Porto Wigner Date: Thu, 30 Apr 2026 11:37:07 +0200 Subject: [PATCH 09/10] refactor: revert --- .../source/commands/constants.ts | 5 +- .../commands/generate/actions/config.ts | 47 ++++----- .../source/commands/generate/actions/lib.ts | 84 ++++++++++------ .../source/commands/generate/actions/main.ts | 17 +--- .../admin-ui-sdk/registration.js.template | 5 +- .../source/commands/hooks/pre-app-build.ts | 17 +--- .../source/commands/utils.ts | 19 ++-- .../test/fixtures/project.ts | 3 +- .../commands/generate/actions.test.ts | 24 +---- .../commands/hooks/pre-app-build.test.ts | 23 +---- .../commands/generate/actions/lib.test.ts | 99 +++++++++++++------ 11 files changed, 170 insertions(+), 173 deletions(-) diff --git a/packages/aio-commerce-lib-app/source/commands/constants.ts b/packages/aio-commerce-lib-app/source/commands/constants.ts index 14c9130c4..9789d58c8 100644 --- a/packages/aio-commerce-lib-app/source/commands/constants.ts +++ b/packages/aio-commerce-lib-app/source/commands/constants.ts @@ -35,7 +35,7 @@ export const BACKEND_UI_EXTENSION_POINT_ID = "commerce/backend-ui/1"; export const ADMIN_UI_SDK_PACKAGE_NAME = "admin-ui-sdk"; /** The path to the directory containing the generated Admin UI SDK actions. */ -export const ADMIN_UI_SDK_ACTIONS_PATH = `${GENERATED_PATH}/actions`; +export const ADMIN_UI_SDK_ACTIONS_PATH = `${GENERATED_PATH}/actions/registration`; /** The name of the configuration schema file */ export const APP_MANIFEST_FILE = "app.commerce.manifest.json"; @@ -46,9 +46,6 @@ export const COMMERCE_APP_CONFIG_FILE = "app.commerce.config"; /** The name of the configuration schema file */ export const CONFIG_SCHEMA_FILE_NAME = "configuration-schema.json"; -/** The name of the Admin UI SDK registration file */ -export const REGISTRATION_FILE_NAME = "registration.json"; - /** The name of the project package file */ export const PACKAGE_JSON_FILE = "package.json"; diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts b/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts index 3194f44cc..b1b444acc 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/config.ts @@ -31,7 +31,6 @@ import type { CommerceAppConfigDomain } from "#config/schema/domains"; type ActionConfig = { requiresSchema?: boolean; requiresEncryptionKey?: boolean; - generatedBasePath?: string; }; export type TemplateAction = ActionConfig & { @@ -72,7 +71,7 @@ function createActionDefinition( const def: ActionDefinition = { ...options, - function: `${config.generatedBasePath ?? GENERATED_ACTIONS_PATH}/${actionName}.js`, + function: `${GENERATED_ACTIONS_PATH}/${actionName}.js`, web: options.web ?? "yes", runtime: "nodejs:22", annotations: { @@ -94,25 +93,15 @@ function createActionDefinition( /** * Gets the runtime actions to be generated from the ext.config.yaml configuration. * @param extConfig - The ext.config.yaml configuration. - * @param dir - The directory where the action templates are located. - * @param packageName - The name of the package containing the actions (default: "app-management"). */ -export function getRuntimeActions( - extConfig: ExtConfig, - dir: string, - packageName = PACKAGE_NAME, -) { +export function getRuntimeActions(extConfig: ExtConfig, dir: string) { return Object.entries( - extConfig.runtimeManifest?.packages?.[packageName]?.actions ?? {}, + extConfig.runtimeManifest?.packages?.[PACKAGE_NAME]?.actions ?? {}, ).map( ([name, _]) => ({ name, templateFile: join(dir, `${name}.js.template`), - generatedBasePath: - packageName === ADMIN_UI_SDK_PACKAGE_NAME - ? ADMIN_UI_SDK_ACTIONS_PATH - : undefined, }) satisfies TemplateAction, ); } @@ -231,17 +220,10 @@ export function buildBusinessConfigurationExtConfig() { } satisfies ExtConfig; } -/** Builds the ext.config.yaml configuration for the Admin UI SDK backend-ui extension. */ +/** + * Builds the ext.config.yaml configuration for the Admin UI SDK backend-ui extension. + */ export function buildAdminUiSdkExtConfig() { - const actions = [ - { - name: "registration", - templateFile: "registration.js.template", - requiresEncryptionKey: true, - generatedBasePath: ADMIN_UI_SDK_ACTIONS_PATH, - }, - ] satisfies TemplateAction[]; - return { hooks: { "pre-app-build": @@ -257,12 +239,17 @@ export function buildAdminUiSdkExtConfig() { packages: { [ADMIN_UI_SDK_PACKAGE_NAME]: { license: "Apache-2.0", - actions: Object.fromEntries( - actions.map((action) => [ - action.name, - createActionDefinition(action.name, action), - ]), - ), + actions: { + registration: { + function: `${ADMIN_UI_SDK_ACTIONS_PATH}/index.js`, + web: "yes", + runtime: "nodejs:22", + annotations: { + "require-adobe-auth": true, + final: true, + }, + }, + } satisfies Record, }, }, }, 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 2330382fe..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,18 +22,21 @@ 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, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, getExtensionPointFolderPath, - REGISTRATION_FILE_NAME, } from "#commands/constants"; import { + getActionPath, getActionsDir, + getAdminUiSdkActionsDir, + getAdminUiSdkRegistrationActionPath, getExtConfigPath, - getGeneratedDir, + prettierFormat, } from "#commands/utils"; import { hasCustomInstallationSteps } from "#config/index"; @@ -44,6 +47,7 @@ import { CUSTOM_IMPORTS_PLACEHOLDER, CUSTOM_SCRIPTS_LOADER_PLACEHOLDER, CUSTOM_SCRIPTS_MAP_PLACEHOLDER, + REGISTRATION_JSON_PLACEHOLDER, } from "./config"; import type { ExtConfig } from "@aio-commerce-sdk/scripting-utils/yaml"; @@ -138,6 +142,7 @@ export async function generateActionFiles( ) { consola.start("Generating runtime actions..."); + await makeOutputDirFor(getActionsDir(extensionPointId)); const projectRoot = await getProjectRootDirectory(); const outputFiles: string[] = []; @@ -145,15 +150,6 @@ export async function generateActionFiles( const templatePath = join(templatesDir, action.templateFile); let template = await readFile(templatePath, "utf-8"); - const outputDir = action.generatedBasePath - ? join( - getExtensionPointFolderPath(extensionPointId), - action.generatedBasePath, - ) - : getActionsDir(extensionPointId); - - await makeOutputDirFor(outputDir); - // For installation action, inject custom script imports if (action.name === "installation") { const customScriptsTemplatePath = join( @@ -170,9 +166,12 @@ export async function generateActionFiles( template = applyCustomScripts(template, scriptsTemplate); } - const actionPath = join(projectRoot, outputDir, `${action.name}.js`); - await writeFile(actionPath, template, "utf-8"); + const actionPath = join( + projectRoot, + getActionPath(extensionPointId, action.name), + ); + await writeFile(actionPath, template, "utf-8"); outputFiles.push(` ${relative(process.cwd(), actionPath)}`); } @@ -180,24 +179,6 @@ export async function generateActionFiles( consola.log.raw(formatTree(outputFiles)); } -/** Generate the backend-ui registration JSON consumed by the generated action. */ -export async function generateRegistrationJson(appManifest: AdminUiSdkConfig) { - await makeOutputDirFor(getGeneratedDir(BACKEND_UI_EXTENSION_POINT_ID)); - const registrationJsonPath = join( - await getProjectRootDirectory(), - getGeneratedDir(BACKEND_UI_EXTENSION_POINT_ID), - REGISTRATION_FILE_NAME, - ); - - await writeFile( - registrationJsonPath, - JSON.stringify(appManifest.adminUiSdk.registration, null, 2), - "utf-8", - ); - - return registrationJsonPath; -} - /** * Applies the given custom scripts template code to the given installation template. * @param installationTemplate - The installation code runtime action template @@ -283,3 +264,44 @@ export async function generateCustomScriptsTemplate( const result = template.replace(CUSTOM_IMPORTS_PLACEHOLDER, importStatements); return result.replace(CUSTOM_SCRIPTS_MAP_PLACEHOLDER, scriptMap); } + +/** + * Generates `registration/index.js` with the Admin UI SDK registration config + * inlined as a JS object literal. + * @param appManifest - The validated app config; must satisfy `hasAdminUiSdk`. + * @param extensionPointId - The extension point ID that owns the registration action. + * @param templatesDir - The root directory containing the action templates. + */ +export async function generateRegistrationActionFile( + appManifest: AdminUiSdkConfig, + extensionPointId: typeof BACKEND_UI_EXTENSION_POINT_ID, + templatesDir = TEMPLATES_DIR, +) { + consola.start("Generating Admin UI SDK registration action..."); + + await makeOutputDirFor(getAdminUiSdkActionsDir(extensionPointId)); + const projectRoot = await getProjectRootDirectory(); + const templatePath = join( + templatesDir, + "admin-ui-sdk", + "registration.js.template", + ); + const template = await readFile(templatePath, "utf-8"); + + const { registration } = appManifest.adminUiSdk; + const actionPath = join( + projectRoot, + getAdminUiSdkRegistrationActionPath(extensionPointId), + ); + const content = template.replace( + REGISTRATION_JSON_PLACEHOLDER, + `const registration = ${stringify(registration)};`, + ); + + const formattedContent = await prettierFormat(content, actionPath); + + await writeFile(actionPath, formattedContent, "utf-8"); + consola.success( + `Generated registration action at ${relative(process.cwd(), actionPath)}`, + ); +} diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts b/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts index 41bb51c92..101627aa2 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/main.ts @@ -2,7 +2,6 @@ import { CommerceSdkValidationError } from "@adobe/aio-commerce-lib-core/error"; import { consola } from "consola"; import { - ADMIN_UI_SDK_PACKAGE_NAME, BACKEND_UI_EXTENSION_POINT_ID, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, @@ -13,7 +12,7 @@ import { hasAdminUiSdk, hasBusinessConfigSchema } from "#config/index"; import { getRuntimeActions } from "./config"; import { generateActionFiles, - generateRegistrationJson, + generateRegistrationActionFile, TEMPLATES_DIR, updateExtConfig, } from "./lib"; @@ -57,20 +56,10 @@ export async function run( } if (hasAdminUiSdk(appManifest)) { - const adminUiExtConfig = await updateExtConfig( + await updateExtConfig(appManifest, BACKEND_UI_EXTENSION_POINT_ID); + await generateRegistrationActionFile( appManifest, BACKEND_UI_EXTENSION_POINT_ID, - ); - - await generateRegistrationJson(appManifest); - await generateActionFiles( - appManifest, - getRuntimeActions( - adminUiExtConfig, - "admin-ui-sdk", - ADMIN_UI_SDK_PACKAGE_NAME, - ), - BACKEND_UI_EXTENSION_POINT_ID, templatesDir, ); } diff --git a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template index d0fc3f356..7223be11a 100644 --- a/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template +++ b/packages/aio-commerce-lib-app/source/commands/generate/actions/templates/admin-ui-sdk/registration.js.template @@ -15,7 +15,6 @@ import { registrationRuntimeAction } from "@adobe/aio-commerce-lib-app/actions/registration"; -// The registration config is always at this relative constant path from the action. -import registration from "../registration.json" with { type: "json" }; +// {{REGISTRATION_JSON}} -export const main = registrationRuntimeAction({ registration }); \ No newline at end of file +export const main = registrationRuntimeAction({ registration }); diff --git a/packages/aio-commerce-lib-app/source/commands/hooks/pre-app-build.ts b/packages/aio-commerce-lib-app/source/commands/hooks/pre-app-build.ts index 3c2d2f800..dce6d23c7 100644 --- a/packages/aio-commerce-lib-app/source/commands/hooks/pre-app-build.ts +++ b/packages/aio-commerce-lib-app/source/commands/hooks/pre-app-build.ts @@ -15,7 +15,6 @@ import { syncImsCredentials } from "@aio-commerce-sdk/scripting-utils/env"; import consola from "consola"; import { - ADMIN_UI_SDK_PACKAGE_NAME, BACKEND_UI_EXTENSION_POINT_ID, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, @@ -23,7 +22,7 @@ import { import { getRuntimeActions } from "#commands/generate/actions/config"; import { generateActionFiles, - generateRegistrationJson, + generateRegistrationActionFile, readExtConfig, TEMPLATES_DIR, } from "#commands/generate/actions/lib"; @@ -87,23 +86,11 @@ export async function run(extension: Extension, templatesDir = TEMPLATES_DIR) { if (extension === "backend-ui/1") { if (hasAdminUiSdk(appManifest)) { - const { doc: adminUiSdkExtConfig } = await readExtConfig( - BACKEND_UI_EXTENSION_POINT_ID, - ); - - await generateRegistrationJson(appManifest); - await generateActionFiles( + await generateRegistrationActionFile( appManifest, - getRuntimeActions( - adminUiSdkExtConfig.toJS() as ExtConfig, - "admin-ui-sdk", - ADMIN_UI_SDK_PACKAGE_NAME, - ), BACKEND_UI_EXTENSION_POINT_ID, - templatesDir, ); } - return; } diff --git a/packages/aio-commerce-lib-app/source/commands/utils.ts b/packages/aio-commerce-lib-app/source/commands/utils.ts index 1d873aa2b..5aaeb1010 100644 --- a/packages/aio-commerce-lib-app/source/commands/utils.ts +++ b/packages/aio-commerce-lib-app/source/commands/utils.ts @@ -20,7 +20,6 @@ import { parseCommerceAppConfig } from "#config/index"; import { ADMIN_UI_SDK_ACTIONS_PATH, APP_MANIFEST_FILE, - BACKEND_UI_EXTENSION_POINT_ID, CONFIG_SCHEMA_FILE_NAME, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, @@ -90,17 +89,23 @@ export function getExtConfigPath(extensionPointId: string) { return join(getExtensionPointFolderPath(extensionPointId), "ext.config.yaml"); } -/** Path to an Admin UI SDK generated actions directory, relative to the project root. */ -export function getAdminUiSdkActionsDir() { +/** + * Path to an Admin UI SDK generated actions directory, relative to the project root. + * @param extensionPointId - The extension point ID, e.g. "commerce/backend-ui/1" + */ +export function getAdminUiSdkActionsDir(extensionPointId: string) { return join( - getExtensionPointFolderPath(BACKEND_UI_EXTENSION_POINT_ID), + getExtensionPointFolderPath(extensionPointId), ADMIN_UI_SDK_ACTIONS_PATH, ); } -/** Path to the generated Admin UI SDK registration action file, relative to the project root. */ -export function getAdminUiSdkRegistrationActionPath() { - return join(getAdminUiSdkActionsDir(), "registration.js"); +/** + * Path to the generated Admin UI SDK registration action file, relative to the project root. + * @param extensionPointId - The extension point ID, e.g. "commerce/backend-ui/1" + */ +export function getAdminUiSdkRegistrationActionPath(extensionPointId: string) { + return join(getAdminUiSdkActionsDir(extensionPointId), "index.js"); } /** Path to the generated app manifest file, relative to the project root. */ diff --git a/packages/aio-commerce-lib-app/test/fixtures/project.ts b/packages/aio-commerce-lib-app/test/fixtures/project.ts index 1888de8b7..793c92123 100644 --- a/packages/aio-commerce-lib-app/test/fixtures/project.ts +++ b/packages/aio-commerce-lib-app/test/fixtures/project.ts @@ -106,12 +106,11 @@ export function envObject(env: Record): string { export function makeExtConfigFile( extensionPointId: string, actionNames: string[], - packageName = PACKAGE_NAME, ) { const extConfig = { runtimeManifest: { packages: { - [packageName]: { + [PACKAGE_NAME]: { actions: Object.fromEntries( actionNames.map((actionName) => [actionName, {}]), ), diff --git a/packages/aio-commerce-lib-app/test/integration/commands/generate/actions.test.ts b/packages/aio-commerce-lib-app/test/integration/commands/generate/actions.test.ts index 587b64ca4..3fe7ea4f8 100644 --- a/packages/aio-commerce-lib-app/test/integration/commands/generate/actions.test.ts +++ b/packages/aio-commerce-lib-app/test/integration/commands/generate/actions.test.ts @@ -23,10 +23,7 @@ import { getExtensionPointFolderPath, } from "#commands/constants"; import { exec, run } from "#commands/generate/actions/main"; -import { - getAdminUiSdkRegistrationActionPath, - getGeneratedDir, -} from "#commands/utils"; +import { getAdminUiSdkRegistrationActionPath } from "#commands/utils"; import { makeTemplateFiles } from "#test/fixtures/commands"; import { configWithBusinessConfig, @@ -127,7 +124,7 @@ describe("commands/generate/actions", () => { ); }); - test("generates backend-ui registration action and registration json when adminUiSdk is configured", async () => { + test("generates backend-ui registration action when adminUiSdk is configured", async () => { await withTempProject( { ...EMPTY_PROJECT, ...makeTemplateFiles() }, async (tempDir) => { @@ -135,31 +132,20 @@ describe("commands/generate/actions", () => { const registrationPath = join( tempDir, - getAdminUiSdkRegistrationActionPath(), + getAdminUiSdkRegistrationActionPath(BACKEND_UI_EXTENSION_POINT_ID), ); - const extConfigPath = join( tempDir, getExtensionPointFolderPath(BACKEND_UI_EXTENSION_POINT_ID), "ext.config.yaml", ); - const registrationJsonPath = join( - tempDir, - getGeneratedDir(BACKEND_UI_EXTENSION_POINT_ID), - "registration.json", - ); - expect(existsSync(registrationPath)).toBe(true); expect(existsSync(extConfigPath)).toBe(true); - expect(existsSync(registrationJsonPath)).toBe(true); const content = await readFile(registrationPath, "utf-8"); - expect(content).toContain('with { type: "json" }'); - - expect( - JSON.parse(await readFile(registrationJsonPath, "utf-8")), - ).toStrictEqual(configWithFullAdminUiSdk.adminUiSdk.registration); + expect(content).toContain("registrationRuntimeAction"); + expect(content).toContain("my-app::first"); }, ); }); diff --git a/packages/aio-commerce-lib-app/test/integration/commands/hooks/pre-app-build.test.ts b/packages/aio-commerce-lib-app/test/integration/commands/hooks/pre-app-build.test.ts index 6525c4b0a..f55896116 100644 --- a/packages/aio-commerce-lib-app/test/integration/commands/hooks/pre-app-build.test.ts +++ b/packages/aio-commerce-lib-app/test/integration/commands/hooks/pre-app-build.test.ts @@ -17,7 +17,6 @@ import { join } from "node:path"; import { afterEach, describe, expect, test, vi } from "vitest"; import { - ADMIN_UI_SDK_PACKAGE_NAME, BACKEND_UI_EXTENSION_POINT_ID, CONFIGURATION_EXTENSION_POINT_ID, EXTENSIBILITY_EXTENSION_POINT_ID, @@ -25,7 +24,6 @@ import { import { exec, run } from "#commands/hooks/pre-app-build"; import { getAdminUiSdkRegistrationActionPath, - getGeneratedDir, getManifestPath, getSchemaPath, } from "#commands/utils"; @@ -113,39 +111,24 @@ describe("commands/hooks/pre-app-build", () => { }); test("generates backend-ui registration action for backend-ui/1", async () => { - const actions = ["registration"]; await withTempProject( { ...makeProjectFiles(configWithFullAdminUiSdk), ...makeTemplateFiles(), - ...makeExtConfigFile( - BACKEND_UI_EXTENSION_POINT_ID, - actions, - ADMIN_UI_SDK_PACKAGE_NAME, - ), }, async (tempDir) => { await run("backend-ui/1"); const registrationPath = join( tempDir, - getAdminUiSdkRegistrationActionPath(), - ); - const registrationJsonPath = join( - tempDir, - getGeneratedDir(BACKEND_UI_EXTENSION_POINT_ID), - "registration.json", + getAdminUiSdkRegistrationActionPath(BACKEND_UI_EXTENSION_POINT_ID), ); expect(existsSync(registrationPath)).toBe(true); - expect(existsSync(registrationJsonPath)).toBe(true); const content = await readFile(registrationPath, "utf-8"); expect(content).toContain("registrationRuntimeAction"); - expect(content).toContain('with { type: "json" }'); - expect( - JSON.parse(await readFile(registrationJsonPath, "utf-8")), - ).toStrictEqual(configWithFullAdminUiSdk.adminUiSdk.registration); + expect(content).toContain("my-app::first"); }, ); }); @@ -158,7 +141,7 @@ describe("commands/hooks/pre-app-build", () => { const registrationPath = join( tempDir, - getAdminUiSdkRegistrationActionPath(), + getAdminUiSdkRegistrationActionPath(BACKEND_UI_EXTENSION_POINT_ID), ); expect(existsSync(registrationPath)).toBe(false); diff --git a/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts b/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts index 3244a9cec..f6375e7eb 100644 --- a/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts +++ b/packages/aio-commerce-lib-app/test/unit/commands/generate/actions/lib.test.ts @@ -1,8 +1,25 @@ -import { writeFile } from "node:fs/promises"; +/* + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ -import { describe, expect, test, vi } from "vitest"; +import { readFile, writeFile } from "node:fs/promises"; -import { EXTENSIBILITY_EXTENSION_POINT_ID } from "#commands/constants"; +const QUOTED_MENU_ITEMS_RE = /"menuItems":/u; + +import { beforeEach, describe, expect, test, vi } from "vitest"; + +import { + BACKEND_UI_EXTENSION_POINT_ID, + EXTENSIBILITY_EXTENSION_POINT_ID, +} from "#commands/constants"; import { CUSTOM_IMPORTS_PLACEHOLDER, CUSTOM_SCRIPTS_LOADER_PLACEHOLDER, @@ -11,7 +28,7 @@ import { import { applyCustomScripts, generateCustomScriptsTemplate, - generateRegistrationJson, + generateRegistrationActionFile, readExtConfig, } from "#commands/generate/actions/lib"; import { templates } from "#test/fixtures/commands"; @@ -57,30 +74,6 @@ describe("readExtConfig", () => { }); }); -describe("generateRegistrationJson", () => { - test("writes the generated backend-ui registration json", async () => { - const mockWriteFile = vi.mocked(writeFile); - - const registrationJsonPath = await generateRegistrationJson( - configWithFullAdminUiSdk, - ); - - expect(mockWriteFile).toHaveBeenCalledOnce(); - expect(registrationJsonPath).toContain( - "/src/commerce-backend-ui-1/.generated/registration.json", - ); - - const [filePath, content] = mockWriteFile.mock.calls[0]; - expect(String(filePath)).toContain( - "/src/commerce-backend-ui-1/.generated/registration.json", - ); - - expect(JSON.parse(String(content))).toStrictEqual( - configWithFullAdminUiSdk.adminUiSdk.registration, - ); - }); -}); - describe("applyCustomScripts", () => { describe("when no custom installation steps are configured", () => { test("should not replace args or import customScriptsLoader", async () => { @@ -224,3 +217,53 @@ describe("generateCustomScriptsTemplate", () => { }); }); }); + +describe("generateRegistrationActionFile", () => { + beforeEach(() => { + vi.clearAllMocks(); + vi.mocked(readFile).mockResolvedValue(templates.registration); + }); + + test("writes registration.js with inlined registration JSON", async () => { + const mockReadFile = vi.mocked(readFile); + const mockWriteFile = vi.mocked(writeFile); + + await generateRegistrationActionFile( + configWithFullAdminUiSdk, + BACKEND_UI_EXTENSION_POINT_ID, + ); + + expect(mockReadFile).toHaveBeenCalledOnce(); + expect(mockWriteFile).toHaveBeenCalledOnce(); + + const [_path, content] = mockWriteFile.mock.calls[0]; + const contentStr = content as string; + + expect(contentStr).toContain("// This file has been auto-generated"); + expect(contentStr).toContain( + 'import { registrationRuntimeAction } from "@adobe/aio-commerce-lib-app/actions/registration"', + ); + expect(contentStr).toContain("const registration ="); + expect(contentStr).toContain( + "export const main = registrationRuntimeAction({ registration })", + ); + expect(contentStr).toContain('"my-app::first"'); + expect(contentStr).toContain("selectionLimit: 1"); + expect(contentStr).toContain("productSelectLimit: 1"); + expect(contentStr).toContain("customerSelectLimit: 1"); + expect(contentStr).toContain("menuItems: ["); + expect(contentStr).not.toMatch(QUOTED_MENU_ITEMS_RE); + }); + + test("writes to registration/index.js", async () => { + const mockWriteFile = vi.mocked(writeFile); + + await generateRegistrationActionFile( + configWithFullAdminUiSdk, + BACKEND_UI_EXTENSION_POINT_ID, + ); + + const [filePath] = mockWriteFile.mock.calls[0]; + expect(String(filePath)).toContain("index.js"); + }); +}); From b65ab43a77727e9f779bea3ec4af085a8e9936da Mon Sep 17 00:00:00 2001 From: Ivan Porto Wigner Date: Thu, 30 Apr 2026 11:43:54 +0200 Subject: [PATCH 10/10] test: remove test of reverted logic --- .../unit/config/schema/admin-ui-sdk.test.ts | 24 ------------------- 1 file changed, 24 deletions(-) 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 34f0139a4..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 @@ -448,29 +448,5 @@ describe("AdminUiSdkSchema", () => { expect(result.success).toBe(false); } }); - - test("iframe action with non-boolean displayIframe returns a boolean error", () => { - const result = parseRegistration({ - order: { - massActions: [ - { - actionId: "app::action", - label: "Action", - path: "#/action", - displayIframe: 1, - }, - ], - }, - }); - - expect(result.success).toBe(false); - expect(result.issues).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - message: "Expected a boolean value for 'displayIframe'", - }), - ]), - ); - }); }); });