@adobe/aio-commerce-lib-app 1.3.1 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cjs/actions/app-config.cjs +3 -3
  3. package/dist/cjs/actions/app-config.d.cts +1 -1
  4. package/dist/cjs/actions/config.cjs +31 -9
  5. package/dist/cjs/actions/installation.cjs +16 -13
  6. package/dist/cjs/actions/installation.d.cts +2 -2
  7. package/dist/cjs/actions/registration.cjs +41 -0
  8. package/dist/cjs/actions/registration.d.cts +33 -0
  9. package/dist/cjs/actions/scope-tree.cjs +5 -4
  10. package/dist/cjs/admin-ui-sdk-BiSopnKI.d.cts +665 -0
  11. package/dist/cjs/{app-DcQMhW2N.d.cts → app-C6kImBl7.d.cts} +242 -16
  12. package/dist/cjs/chunk-DS2yp5vX.cjs +48 -0
  13. package/dist/cjs/commands/index.cjs +582 -321
  14. package/dist/cjs/commands/templates/admin-ui-sdk/registration.js.template +20 -0
  15. package/dist/cjs/config/index.cjs +4 -3
  16. package/dist/cjs/config/index.d.cts +558 -62
  17. package/dist/cjs/{config-BppBKCFj.cjs → config-ct-qMOGj.cjs} +56 -25
  18. package/dist/cjs/{error-Dn7ool6k.cjs → error-C5LNOZ4f.cjs} +1 -1
  19. package/dist/cjs/{index-BVwQk9bx.d.cts → index-B5WKJ3hJ.d.cts} +20 -174
  20. package/dist/cjs/{logging-4s36JTiN.cjs → logging-BTevALnL.cjs} +2 -2
  21. package/dist/cjs/management/index.cjs +1 -1
  22. package/dist/cjs/management/index.d.cts +3 -2
  23. package/dist/cjs/{management-C6xG5bfl.cjs → management-D1QXg4YU.cjs} +128 -75
  24. package/dist/cjs/{router-KeQRduO3.cjs → router-CFErodHU.cjs} +2 -2
  25. package/dist/cjs/{schemas-nkIxa8sL.cjs → schemas-CukVpc19.cjs} +2 -34
  26. package/dist/cjs/step-CWYnKbga.d.cts +187 -0
  27. package/dist/cjs/{validate-CeUCT_7k.cjs → validate-b_bI4mDb.cjs} +4 -3
  28. package/dist/cjs/{webhooks-CLtDxwMa.cjs → webhooks-DS1BtgED.cjs} +157 -17
  29. package/dist/es/actions/app-config.d.mts +1 -1
  30. package/dist/es/actions/app-config.mjs +1 -1
  31. package/dist/es/actions/config.mjs +28 -7
  32. package/dist/es/actions/installation.d.mts +2 -2
  33. package/dist/es/actions/installation.mjs +12 -10
  34. package/dist/es/actions/registration.d.mts +33 -0
  35. package/dist/es/actions/registration.mjs +39 -0
  36. package/dist/es/actions/scope-tree.mjs +1 -1
  37. package/dist/es/admin-ui-sdk-CCo2SWiL.d.mts +667 -0
  38. package/dist/es/{app-DJr-mN9d.d.mts → app-DcY6dJiQ.d.mts} +242 -16
  39. package/dist/es/commands/index.mjs +575 -316
  40. package/dist/es/commands/templates/admin-ui-sdk/registration.js.template +20 -0
  41. package/dist/es/config/index.d.mts +558 -62
  42. package/dist/es/config/index.mjs +4 -4
  43. package/dist/es/{config-BohwKkQS.mjs → config-DGPFTk6w.mjs} +51 -20
  44. package/dist/es/{index-BENO5T7n.d.mts → index-CCov0odd.d.mts} +20 -174
  45. package/dist/es/management/index.d.mts +3 -2
  46. package/dist/es/management/index.mjs +1 -1
  47. package/dist/es/{management-ByHvVJ12.mjs → management-CCsOfniv.mjs} +124 -71
  48. package/dist/es/step-0eI2x4C9.d.mts +187 -0
  49. package/dist/es/{validate-CqJdGzyZ.mjs → validate-Dn3lLTGj.mjs} +2 -2
  50. package/dist/es/{webhooks-CYo-pqbR.mjs → webhooks-Dhrkkip7.mjs} +155 -16
  51. package/package.json +12 -9
  52. /package/dist/cjs/commands/{generate/actions/templates → templates}/app-management/app-config.js.template +0 -0
  53. /package/dist/cjs/commands/{generate/actions/templates → templates}/app-management/custom-scripts.js.template +0 -0
  54. /package/dist/cjs/commands/{generate/actions/templates → templates}/app-management/installation.js.template +0 -0
  55. /package/dist/cjs/commands/{generate/actions/templates → templates}/business-configuration/config.js.template +0 -0
  56. /package/dist/cjs/commands/{generate/actions/templates → templates}/business-configuration/scope-tree.js.template +0 -0
  57. /package/dist/es/commands/{generate/actions/templates → templates}/app-management/app-config.js.template +0 -0
  58. /package/dist/es/commands/{generate/actions/templates → templates}/app-management/custom-scripts.js.template +0 -0
  59. /package/dist/es/commands/{generate/actions/templates → templates}/app-management/installation.js.template +0 -0
  60. /package/dist/es/commands/{generate/actions/templates → templates}/business-configuration/config.js.template +0 -0
  61. /package/dist/es/commands/{generate/actions/templates → templates}/business-configuration/scope-tree.js.template +0 -0
  62. /package/dist/es/{schemas-CVXHgUhv.mjs → schemas-DOcnD16x.mjs} +0 -0
@@ -13,9 +13,9 @@
13
13
  * governing permissions and limitations under the License.
14
14
  */
15
15
 
16
- import { a as hasCustomInstallationSteps } from "../webhooks-CYo-pqbR.mjs";
17
- import { c as hasBusinessConfigSchema, i as getConfigDomains, t as validateCommerceAppConfig } from "../validate-CqJdGzyZ.mjs";
18
- import { a as findNearestPackageJson, c as isESM, i as detectPackageManager, l as makeOutputDirFor, n as readCommerceAppConfig, o as getExecCommand, s as getProjectRootDirectory, t as parseCommerceAppConfig, u as readPackageJson } from "../config-BohwKkQS.mjs";
16
+ import { a as hasCustomInstallationSteps, d as hasAdminUiSdk } from "../webhooks-Dhrkkip7.mjs";
17
+ import { c as hasBusinessConfigSchema, i as getConfigDomains, t as validateCommerceAppConfig } from "../validate-Dn3lLTGj.mjs";
18
+ import { a as getExecCommand, c as isESM, i as detectPackageManager, l as makeOutputDirFor, n as readCommerceAppConfig, o as getInstallCommand, s as getProjectRootDirectory, t as parseCommerceAppConfig, u as readPackageJson } from "../config-DGPFTk6w.mjs";
19
19
  import { t as stringifyError } from "../error-DHlYzkbb.mjs";
20
20
  import { t as inspect } from "../logging-XIUXDK5T.mjs";
21
21
  import { CommerceSdkValidationError } from "@adobe/aio-commerce-lib-core/error";
@@ -23,16 +23,257 @@ import path, { basename, dirname, join, relative, resolve } from "path";
23
23
  import { existsSync, readFileSync, writeFileSync } from "fs";
24
24
  import { readFile, writeFile } from "fs/promises";
25
25
  import consola, { consola as consola$1 } from "consola";
26
+ import * as prettier from "prettier";
26
27
  import { fileURLToPath } from "url";
27
28
  import { Document, YAMLMap, YAMLSeq, isMap, isSeq, parseDocument } from "yaml";
28
29
  import { colorize, formatTree } from "consola/utils";
29
- import { stringify } from "safe-stable-stringify";
30
- import { execSync } from "child_process";
30
+ import stringify, { stringify as stringify$1 } from "safe-stable-stringify";
31
+ import { execSync, spawnSync } from "child_process";
31
32
  import config from "@adobe/aio-lib-core-config";
32
33
  import aioIms from "@adobe/aio-lib-ims";
33
34
  import dotenv from "dotenv";
34
- import * as prettier from "prettier";
35
+ import NpmPackageJson from "@npmcli/package-json";
36
+
37
+ //#region source/commands/constants.ts
38
+ /** The name of the package containing all the generated runtime actions */
39
+ const PACKAGE_NAME = "app-management";
40
+ /** The path to the directory containing the generated files */
41
+ const GENERATED_PATH = ".generated";
42
+ /** The folder name containing the runtime actions */
43
+ const ACTIONS_FOLDER = `actions/${PACKAGE_NAME}`;
44
+ /** The path to the directory containing the generated actions */
45
+ const GENERATED_ACTIONS_PATH = `${GENERATED_PATH}/${ACTIONS_FOLDER}`;
46
+ /** The name of the extension point for extensibility. */
47
+ const EXTENSIBILITY_EXTENSION_POINT_ID = "commerce/extensibility/1";
48
+ /** The name of the extension point for configuration. */
49
+ const CONFIGURATION_EXTENSION_POINT_ID = "commerce/configuration/1";
50
+ /** The name of the extension point for Admin UI SDK. */
51
+ const BACKEND_UI_EXTENSION_POINT_ID = "commerce/backend-ui/1";
52
+ /** The name of the package containing the admin UI SDK generated actions */
53
+ const ADMIN_UI_SDK_PACKAGE_NAME = "admin-ui-sdk";
54
+ /** The path to the directory containing the generated Admin UI SDK actions. */
55
+ const ADMIN_UI_SDK_ACTIONS_PATH = `${GENERATED_PATH}/actions/registration`;
56
+ /** The name of the configuration schema file */
57
+ const APP_MANIFEST_FILE = "app.commerce.manifest.json";
58
+ /** The name of the commerce app configuration file */
59
+ const COMMERCE_APP_CONFIG_FILE = "app.commerce.config";
60
+ /** The name of the configuration schema file */
61
+ const CONFIG_SCHEMA_FILE_NAME = "configuration-schema.json";
62
+ /** The name of the project package file */
63
+ const PACKAGE_JSON_FILE = "package.json";
64
+ /** The name of the app configuration file */
65
+ const APP_CONFIG_FILE = "app.config.yaml";
66
+ /** The name of the install configuration file */
67
+ const INSTALL_YAML_FILE = "install.yaml";
68
+ /** Get the path to the extension point folder */
69
+ function getExtensionPointFolderPath(extensionPointId) {
70
+ return `src/${extensionPointId.replaceAll("/", "-")}`;
71
+ }
72
+
73
+ //#endregion
74
+ //#region source/commands/utils.ts
75
+ /** Format file content using prettier, inferring the parser from the file path. */
76
+ function prettierFormat(content, filepath) {
77
+ return prettier.format(content, {
78
+ semi: true,
79
+ quoteStyle: "double",
80
+ arrowParens: "always",
81
+ bracketSameLine: true,
82
+ bracketSpacing: true,
83
+ trailingComma: "all",
84
+ tabWidth: 2,
85
+ useTabs: false,
86
+ printWidth: 80,
87
+ filepath
88
+ });
89
+ }
90
+ /** Load the app commerce config */
91
+ async function loadAppManifest() {
92
+ const appConfig = await parseCommerceAppConfig();
93
+ consola.debug("Loaded app commerce config");
94
+ return appConfig;
95
+ }
96
+ /**
97
+ * Path to an extension point's `.generated` directory, relative to the project root.
98
+ * @param extensionPointId - The extension point ID, e.g. "commerce/extensibility/1"
99
+ */
100
+ function getGeneratedDir(extensionPointId) {
101
+ return join(getExtensionPointFolderPath(extensionPointId), GENERATED_PATH);
102
+ }
103
+ /**
104
+ * Path to an extension point's generated actions directory, relative to the project root.
105
+ * @param extensionPointId - The extension point ID, e.g. "commerce/extensibility/1"
106
+ */
107
+ function getActionsDir(extensionPointId) {
108
+ return join(getExtensionPointFolderPath(extensionPointId), GENERATED_ACTIONS_PATH);
109
+ }
110
+ /**
111
+ * Path to a specific generated action file, relative to the project root.
112
+ * @param extensionPointId - The extension point ID, e.g. "commerce/extensibility/1"
113
+ * @param actionName - The name of the action, e.g. "my-action"
114
+ */
115
+ function getActionPath(extensionPointId, actionName) {
116
+ return join(getActionsDir(extensionPointId), `${actionName}.js`);
117
+ }
118
+ /**
119
+ * Path to an extension point's `ext.config.yaml`, relative to the project root.
120
+ * @param extensionPointId - The extension point ID, e.g. "commerce/extensibility/1"
121
+ */
122
+ function getExtConfigPath(extensionPointId) {
123
+ return join(getExtensionPointFolderPath(extensionPointId), "ext.config.yaml");
124
+ }
125
+ /**
126
+ * Path to an Admin UI SDK generated actions directory, relative to the project root.
127
+ * @param extensionPointId - The extension point ID, e.g. "commerce/backend-ui/1"
128
+ */
129
+ function getAdminUiSdkActionsDir(extensionPointId) {
130
+ return join(getExtensionPointFolderPath(extensionPointId), ADMIN_UI_SDK_ACTIONS_PATH);
131
+ }
132
+ /**
133
+ * Path to the generated Admin UI SDK registration action file, relative to the project root.
134
+ * @param extensionPointId - The extension point ID, e.g. "commerce/backend-ui/1"
135
+ */
136
+ function getAdminUiSdkRegistrationActionPath(extensionPointId) {
137
+ return join(getAdminUiSdkActionsDir(extensionPointId), "index.js");
138
+ }
139
+
140
+ //#endregion
141
+ //#region source/commands/generate/actions/config.ts
142
+ /** The list of Commerce variables that are required for the runtime actions */
143
+ const COMMERCE_VARIABLES = [
144
+ "AIO_COMMERCE_AUTH_IMS_CLIENT_ID",
145
+ "AIO_COMMERCE_AUTH_IMS_CLIENT_SECRETS",
146
+ "AIO_COMMERCE_AUTH_IMS_TECHNICAL_ACCOUNT_ID",
147
+ "AIO_COMMERCE_AUTH_IMS_TECHNICAL_ACCOUNT_EMAIL",
148
+ "AIO_COMMERCE_AUTH_IMS_ORG_ID",
149
+ "AIO_COMMERCE_AUTH_IMS_SCOPES"
150
+ ];
151
+ /** The inputs for the generated runtime actions */
152
+ const COMMERCE_ACTION_INPUTS = Object.fromEntries(COMMERCE_VARIABLES.map((variable) => [variable, `$${variable}`]));
153
+ const CUSTOM_IMPORTS_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_IMPORTS}}";
154
+ const CUSTOM_SCRIPTS_MAP_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_MAP}}";
155
+ const CUSTOM_SCRIPTS_LOADER_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_LOADER}}";
156
+ const REGISTRATION_JSON_PLACEHOLDER = "// {{REGISTRATION_JSON}}";
157
+ /**
158
+ * Creates a runtime action configuration.
159
+ * @param actionName - The name of the action.
160
+ * @param options - Optional configuration options.
161
+ */
162
+ function createActionDefinition(actionName, config = {}, options = {}) {
163
+ const def = {
164
+ ...options,
165
+ function: `${GENERATED_ACTIONS_PATH}/${actionName}.js`,
166
+ web: options.web ?? "yes",
167
+ runtime: "nodejs:22",
168
+ annotations: {
169
+ "require-adobe-auth": true,
170
+ final: true
171
+ }
172
+ };
173
+ if (config.requiresEncryptionKey) def.inputs = {
174
+ ...def.inputs,
175
+ AIO_COMMERCE_CONFIG_ENCRYPTION_KEY: "$AIO_COMMERCE_CONFIG_ENCRYPTION_KEY"
176
+ };
177
+ return def;
178
+ }
179
+ /**
180
+ * Gets the runtime actions to be generated from the ext.config.yaml configuration.
181
+ * @param extConfig - The ext.config.yaml configuration.
182
+ */
183
+ function getRuntimeActions(extConfig, dir) {
184
+ return Object.entries(extConfig.runtimeManifest?.packages?.["app-management"]?.actions ?? {}).map(([name, _]) => ({
185
+ name,
186
+ templateFile: join(dir, `${name}.js.template`)
187
+ }));
188
+ }
189
+ /**
190
+ * Builds the ext.config.yaml configuration for the extensibility extension.
191
+ * @param features - The features that are enabled for the app.
192
+ */
193
+ function buildAppManagementExtConfig(appConfig) {
194
+ const features = getConfigDomains(appConfig);
195
+ const hasPasswordFieldsInSchema = hasBusinessConfigSchema(appConfig) && appConfig.businessConfig.schema.some((field) => field.type === "password");
196
+ const extConfig = {
197
+ hooks: { "pre-app-build": "EXTENSION=extensibility/1 $packageExec aio-commerce-lib-app hooks pre-app-build" },
198
+ operations: { workerProcess: [{
199
+ type: "action",
200
+ impl: `${PACKAGE_NAME}/app-config`
201
+ }] },
202
+ runtimeManifest: { packages: { [PACKAGE_NAME]: {
203
+ license: "Apache-2.0",
204
+ actions: { "app-config": createActionDefinition("app-config") }
205
+ } } }
206
+ };
207
+ if ([
208
+ "installation.customInstallationSteps",
209
+ "eventing.commerce",
210
+ "eventing.external",
211
+ "webhooks",
212
+ "adminUiSdk"
213
+ ].some((feature) => features.has(feature))) {
214
+ extConfig.operations.workerProcess.push({
215
+ type: "action",
216
+ impl: `${PACKAGE_NAME}/installation`
217
+ });
218
+ extConfig.runtimeManifest.packages[PACKAGE_NAME].actions.installation = createActionDefinition("installation", { requiresEncryptionKey: hasPasswordFieldsInSchema }, {
219
+ inputs: {
220
+ ...COMMERCE_ACTION_INPUTS,
221
+ LOG_LEVEL: "$LOG_LEVEL"
222
+ },
223
+ limits: { timeout: 6e5 }
224
+ });
225
+ }
226
+ return extConfig;
227
+ }
228
+ /** Builds the ext.config.yaml configuration for the business configuration extension. */
229
+ function buildBusinessConfigurationExtConfig() {
230
+ const actions = [{
231
+ name: "config",
232
+ templateFile: "config.js.template",
233
+ requiresEncryptionKey: true
234
+ }, {
235
+ name: "scope-tree",
236
+ templateFile: "scope-tree.js.template"
237
+ }];
238
+ return {
239
+ hooks: { "pre-app-build": "EXTENSION=configuration/1 $packageExec aio-commerce-lib-app hooks pre-app-build" },
240
+ operations: { workerProcess: actions.map((action) => ({
241
+ type: "action",
242
+ impl: `${PACKAGE_NAME}/${action.name}`
243
+ })) },
244
+ runtimeManifest: { packages: { [PACKAGE_NAME]: {
245
+ license: "Apache-2.0",
246
+ actions: Object.fromEntries(actions.map((action) => [action.name, createActionDefinition(action.name, action)]))
247
+ } } }
248
+ };
249
+ }
250
+ /**
251
+ * Builds the ext.config.yaml configuration for the Admin UI SDK backend-ui extension.
252
+ */
253
+ function buildAdminUiSdkExtConfig() {
254
+ return {
255
+ hooks: { "pre-app-build": "EXTENSION=backend-ui/1 $packageExec aio-commerce-lib-app hooks pre-app-build" },
256
+ operations: { view: [{
257
+ type: "web",
258
+ impl: "index.html"
259
+ }] },
260
+ web: "web-src",
261
+ runtimeManifest: { packages: { [ADMIN_UI_SDK_PACKAGE_NAME]: {
262
+ license: "Apache-2.0",
263
+ actions: { registration: {
264
+ function: `${ADMIN_UI_SDK_ACTIONS_PATH}/index.js`,
265
+ web: "yes",
266
+ runtime: "nodejs:22",
267
+ annotations: {
268
+ "require-adobe-auth": true,
269
+ final: true
270
+ }
271
+ } }
272
+ } } }
273
+ };
274
+ }
35
275
 
276
+ //#endregion
36
277
  //#region ../../packages-private/scripting-utils/source/yaml/helpers.ts
37
278
  /**
38
279
  * Read a YAML file and return a {@link Document}
@@ -42,7 +283,7 @@ async function readYamlFile(path) {
42
283
  let doc = new Document();
43
284
  if (existsSync(path)) try {
44
285
  doc = parseDocument(await readFile(path, "utf-8"), { keepSourceTokens: true });
45
- } catch (_) {
286
+ } catch {
46
287
  const file = basename(path);
47
288
  throw new Error(`Failed to parse ${file}`);
48
289
  }
@@ -107,16 +348,23 @@ function getOrCreateMap(doc, path, options) {
107
348
  */
108
349
  async function createOrUpdateExtConfig(path, config, doc) {
109
350
  const extConfigDoc = doc ?? new Document({});
351
+ if (config.web !== void 0) buildWeb(extConfigDoc, config.web);
110
352
  config.hooks ??= {};
111
- config.operations ??= { workerProcess: [] };
112
353
  config.runtimeManifest ??= { packages: {} };
113
354
  await buildHooks(extConfigDoc, config.hooks);
114
- buildOperations(extConfigDoc, config.operations);
355
+ if (config.operations !== void 0) buildOperations(extConfigDoc, config.operations);
115
356
  buildRuntimeManifest(extConfigDoc, config.runtimeManifest);
116
357
  await writeExtConfig(path, extConfigDoc);
117
358
  return extConfigDoc;
118
359
  }
119
360
  /**
361
+ * Set the top-level `web` scalar only if it's missing.
362
+ * Preserves any user-customized value already present.
363
+ */
364
+ function buildWeb(extConfig, web) {
365
+ if (!extConfig.hasIn(["web"])) extConfig.add(extConfig.createPair("web", web));
366
+ }
367
+ /**
120
368
  * Build the definition for a runtime action
121
369
  * @param name - The name of the action
122
370
  * @param path - The path where the action is located
@@ -154,20 +402,36 @@ function buildActionDefinition(action) {
154
402
  * @param operations - The operations to build
155
403
  */
156
404
  function buildOperations(extConfig, operations) {
157
- getOrCreateMap(extConfig, ["operations"], { onBeforeCreate: (pair) => {
158
- pair.key.spaceBefore = true;
159
- } });
160
- const workerProcess = getOrCreateSeq(extConfig, ["operations", "workerProcess"], { onBeforeCreate: (pair) => {
161
- pair.key.commentBefore = " These worker processes definitions are auto-generated. Do not remove or manually edit.";
162
- } });
163
- workerProcess.items = [];
164
405
  const ourOps = operations.workerProcess ?? [];
165
- workerProcess.items.push(...ourOps.map((op) => {
166
- const map = new YAMLMap();
167
- map.set("type", op.type);
168
- map.set("impl", op.impl);
169
- return map;
170
- }));
406
+ if (ourOps.length > 0) {
407
+ getOrCreateMap(extConfig, ["operations"], { onBeforeCreate: (pair) => {
408
+ pair.key.spaceBefore = true;
409
+ } });
410
+ const workerProcess = getOrCreateSeq(extConfig, ["operations", "workerProcess"], { onBeforeCreate: (pair) => {
411
+ pair.key.commentBefore = " These worker processes definitions are auto-generated. Do not remove or manually edit.";
412
+ } });
413
+ workerProcess.items = [];
414
+ workerProcess.items.push(...ourOps.map((op) => {
415
+ const map = new YAMLMap();
416
+ map.set("type", op.type);
417
+ map.set("impl", op.impl);
418
+ return map;
419
+ }));
420
+ } else if (extConfig.hasIn(["operations", "workerProcess"])) extConfig.deleteIn(["operations", "workerProcess"]);
421
+ if (operations.view !== void 0) {
422
+ getOrCreateMap(extConfig, ["operations"], { onBeforeCreate: (pair) => {
423
+ pair.key.spaceBefore = true;
424
+ } });
425
+ const view = getOrCreateSeq(extConfig, ["operations", "view"]);
426
+ if (view.items.length === 0) view.items.push(...operations.view.map((v) => {
427
+ const map = new YAMLMap();
428
+ map.set("type", v.type);
429
+ map.set("impl", v.impl);
430
+ return map;
431
+ }));
432
+ }
433
+ const operationsMap = extConfig.getIn(["operations"]);
434
+ if (operationsMap instanceof YAMLMap && operationsMap.items.length === 0) extConfig.deleteIn(["operations"]);
171
435
  }
172
436
  /**
173
437
  * Build the `runtimeManifest` section of the `ext.config.yaml` file
@@ -231,177 +495,35 @@ async function writeExtConfig(configPath, doc) {
231
495
  }
232
496
 
233
497
  //#endregion
234
- //#region source/commands/constants.ts
235
- /** The name of the package containing all the generated runtime actions */
236
- const PACKAGE_NAME = "app-management";
237
- /** The path to the directory containing the generated files */
238
- const GENERATED_PATH = ".generated";
239
- /** The folder name containing the runtime actions */
240
- const ACTIONS_FOLDER = `actions/${PACKAGE_NAME}`;
241
- /** The path to the directory containing the generated actions */
242
- const GENERATED_ACTIONS_PATH = `${GENERATED_PATH}/${ACTIONS_FOLDER}`;
243
- /** The name of the extension point for extensibility. */
244
- const EXTENSIBILITY_EXTENSION_POINT_ID = "commerce/extensibility/1";
245
- /** The name of the extension point for configuration. */
246
- const CONFIGURATION_EXTENSION_POINT_ID = "commerce/configuration/1";
247
- /** The name of the configuration schema file */
248
- const APP_MANIFEST_FILE = "app.commerce.manifest.json";
249
- /** The name of the commerce app configuration file */
250
- const COMMERCE_APP_CONFIG_FILE = "app.commerce.config";
251
- /** The name of the configuration schema file */
252
- const CONFIG_SCHEMA_FILE_NAME = "configuration-schema.json";
253
- /** The name of the project package file */
254
- const PACKAGE_JSON_FILE = "package.json";
255
- /** The name of the app configuration file */
256
- const APP_CONFIG_FILE = "app.config.yaml";
257
- /** The name of the install configuration file */
258
- const INSTALL_YAML_FILE = "install.yaml";
259
- /** Get the path to the extension point folder */
260
- function getExtensionPointFolderPath(extensionPointId) {
261
- return `src/${extensionPointId.replaceAll("/", "-")}`;
262
- }
263
-
264
- //#endregion
265
- //#region source/commands/utils.ts
266
- /** Load the app commerce config */
267
- async function loadAppManifest() {
268
- const appConfig = await parseCommerceAppConfig();
269
- consola.debug("Loaded app commerce config");
270
- return appConfig;
271
- }
272
-
273
- //#endregion
274
- //#region source/commands/generate/actions/config.ts
275
- /** The list of Commerce variables that are required for the runtime actions */
276
- const COMMERCE_VARIABLES = [
277
- "AIO_COMMERCE_AUTH_IMS_CLIENT_ID",
278
- "AIO_COMMERCE_AUTH_IMS_CLIENT_SECRETS",
279
- "AIO_COMMERCE_AUTH_IMS_TECHNICAL_ACCOUNT_ID",
280
- "AIO_COMMERCE_AUTH_IMS_TECHNICAL_ACCOUNT_EMAIL",
281
- "AIO_COMMERCE_AUTH_IMS_ORG_ID",
282
- "AIO_COMMERCE_AUTH_IMS_SCOPES"
283
- ];
284
- /** The inputs for the generated runtime actions */
285
- const COMMERCE_ACTION_INPUTS = Object.fromEntries(COMMERCE_VARIABLES.map((variable) => [variable, `$${variable}`]));
286
- const CUSTOM_IMPORTS_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_IMPORTS}}";
287
- const CUSTOM_SCRIPTS_MAP_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_MAP}}";
288
- const CUSTOM_SCRIPTS_LOADER_PLACEHOLDER = "// {{CUSTOM_SCRIPTS_LOADER}}";
289
- /**
290
- * Creates a runtime action configuration.
291
- * @param actionName - The name of the action.
292
- * @param options - Optional configuration options.
293
- */
294
- function createActionDefinition(actionName, config = {}, options = {}) {
295
- const def = {
296
- ...options,
297
- function: `${GENERATED_ACTIONS_PATH}/${actionName}.js`,
298
- web: options.web ?? "yes",
299
- runtime: "nodejs:22",
300
- annotations: {
301
- "require-adobe-auth": true,
302
- final: true
303
- }
304
- };
305
- if (config.requiresEncryptionKey) def.inputs = {
306
- ...def.inputs,
307
- AIO_COMMERCE_CONFIG_ENCRYPTION_KEY: "$AIO_COMMERCE_CONFIG_ENCRYPTION_KEY"
308
- };
309
- return def;
310
- }
311
- /**
312
- * Gets the runtime actions to be generated from the ext.config.yaml configuration.
313
- * @param extConfig - The ext.config.yaml configuration.
314
- */
315
- function getRuntimeActions(extConfig, dir) {
316
- return Object.entries(extConfig.runtimeManifest?.packages?.["app-management"]?.actions ?? {}).map(([name, _]) => ({
317
- name,
318
- templateFile: join(dir, `${name}.js.template`)
319
- }));
320
- }
498
+ //#region source/commands/generate/actions/lib.ts
499
+ const __dirname = dirname(fileURLToPath(import.meta.url));
500
+ /** The path to the action templates directory, sibling to this file. */
501
+ const TEMPLATES_DIR = join(__dirname, "templates");
321
502
  /**
322
- * Builds the ext.config.yaml configuration for the extensibility extension.
323
- * @param features - The features that are enabled for the app.
503
+ * Read an extension point's ext.config.yaml file and return it as a JS object.
504
+ * @param extensionPointId - The extension point ID to read the config for.
324
505
  */
325
- function buildAppManagementExtConfig(appConfig) {
326
- const features = getConfigDomains(appConfig);
327
- const hasPasswordFieldsInSchema = hasBusinessConfigSchema(appConfig) && appConfig.businessConfig.schema.some((field) => field.type === "password");
328
- const extConfig = {
329
- hooks: { "pre-app-build": "EXTENSION=extensibility/1 $packageExec aio-commerce-lib-app hooks pre-app-build" },
330
- operations: { workerProcess: [{
331
- type: "action",
332
- impl: `${PACKAGE_NAME}/app-config`
333
- }] },
334
- runtimeManifest: { packages: { [PACKAGE_NAME]: {
335
- license: "Apache-2.0",
336
- actions: { "app-config": createActionDefinition("app-config") }
337
- } } }
338
- };
339
- if ([
340
- "installation.customInstallationSteps",
341
- "eventing.commerce",
342
- "eventing.external",
343
- "webhooks",
344
- "adminUiSdk"
345
- ].some((feature) => features.has(feature))) {
346
- extConfig.operations.workerProcess.push({
347
- type: "action",
348
- impl: `${PACKAGE_NAME}/installation`
349
- });
350
- extConfig.runtimeManifest.packages[PACKAGE_NAME].actions.installation = createActionDefinition("installation", { requiresEncryptionKey: hasPasswordFieldsInSchema }, {
351
- inputs: {
352
- ...COMMERCE_ACTION_INPUTS,
353
- LOG_LEVEL: "$LOG_LEVEL"
354
- },
355
- limits: { timeout: 6e5 }
356
- });
357
- }
358
- return extConfig;
359
- }
360
- /** Builds the ext.config.yaml configuration for the business configuration extension. */
361
- function buildBusinessConfigurationExtConfig() {
362
- const actions = [{
363
- name: "config",
364
- templateFile: "config.js.template",
365
- requiresEncryptionKey: true
366
- }, {
367
- name: "scope-tree",
368
- templateFile: "scope-tree.js.template"
369
- }];
370
- return {
371
- hooks: { "pre-app-build": "EXTENSION=configuration/1 $packageExec aio-commerce-lib-app hooks pre-app-build" },
372
- operations: { workerProcess: actions.map((action) => ({
373
- type: "action",
374
- impl: `${PACKAGE_NAME}/${action.name}`
375
- })) },
376
- runtimeManifest: { packages: { [PACKAGE_NAME]: {
377
- license: "Apache-2.0",
378
- actions: Object.fromEntries(actions.map((action) => [action.name, createActionDefinition(action.name, action)]))
379
- } } }
380
- };
381
- }
382
-
383
- //#endregion
384
- //#region source/commands/generate/actions/main.ts
385
- const __dirname = dirname(fileURLToPath(import.meta.url));
386
- async function run$4(appManifest) {
387
- await generateActionFiles(appManifest, getRuntimeActions(await updateExtConfig(appManifest, EXTENSIBILITY_EXTENSION_POINT_ID), "app-management"), EXTENSIBILITY_EXTENSION_POINT_ID);
388
- if (hasBusinessConfigSchema(appManifest)) await generateActionFiles(appManifest, getRuntimeActions(await updateExtConfig(appManifest, CONFIGURATION_EXTENSION_POINT_ID), "business-configuration"), CONFIGURATION_EXTENSION_POINT_ID);
389
- }
390
- /** Run the generate actions command */
391
- async function exec$5() {
506
+ async function readExtConfig(extensionPointId) {
507
+ const extConfigPath = join(await getProjectRootDirectory(), getExtConfigPath(extensionPointId));
392
508
  try {
393
- await run$4(await loadAppManifest());
509
+ return {
510
+ path: extConfigPath,
511
+ doc: await readYamlFile(extConfigPath)
512
+ };
394
513
  } catch (error) {
395
- if (error instanceof CommerceSdkValidationError) consola$1.error(error.display());
396
- consola$1.error(error);
397
- process.exit(1);
514
+ const reason = error instanceof Error ? error.message : "Unexpected error while reading the file.";
515
+ throw new Error([
516
+ `Could not read ext.config.yaml for ${extensionPointId} at ${extConfigPath}.`,
517
+ `Make sure the file exists and is valid YAML. You can regenerate it with "aio-commerce-lib-app generate actions".`,
518
+ `Reason: ${reason}`
519
+ ].join("\n"));
398
520
  }
399
521
  }
400
522
  /** Update the ext.config.yaml file */
401
523
  async function updateExtConfig(appConfig, extensionPointId) {
402
524
  consola$1.info(`Updating ext.config.yaml for ${extensionPointId}...`);
403
- const extConfigPath = join(await makeOutputDirFor(getExtensionPointFolderPath(extensionPointId)), "ext.config.yaml");
404
- const extConfigDoc = await readYamlFile(extConfigPath);
525
+ await makeOutputDirFor(getExtensionPointFolderPath(extensionPointId));
526
+ const { path: extConfigPath, doc: extConfigDoc } = await readExtConfig(extensionPointId);
405
527
  let extConfig;
406
528
  switch (extensionPointId) {
407
529
  case EXTENSIBILITY_EXTENSION_POINT_ID:
@@ -410,24 +532,26 @@ async function updateExtConfig(appConfig, extensionPointId) {
410
532
  case CONFIGURATION_EXTENSION_POINT_ID:
411
533
  extConfig = buildBusinessConfigurationExtConfig();
412
534
  break;
413
- default: throw new Error(`Unsupported extension point ID: ${extensionPointId}`);
535
+ case BACKEND_UI_EXTENSION_POINT_ID:
536
+ extConfig = buildAdminUiSdkExtConfig();
537
+ break;
414
538
  }
415
539
  await createOrUpdateExtConfig(extConfigPath, extConfig, extConfigDoc);
416
540
  return extConfig;
417
541
  }
418
542
  /** Generate the action files */
419
- async function generateActionFiles(appManifest, actions, extensionPointId) {
543
+ async function generateActionFiles(appManifest, actions, extensionPointId, templatesDir) {
420
544
  consola$1.start("Generating runtime actions...");
421
- const outputDir = await makeOutputDirFor(join(getExtensionPointFolderPath(extensionPointId), GENERATED_ACTIONS_PATH));
545
+ await makeOutputDirFor(getActionsDir(extensionPointId));
546
+ const projectRoot = await getProjectRootDirectory();
422
547
  const outputFiles = [];
423
- const templatesDir = join(__dirname, "generate/actions/templates");
424
548
  for (const action of actions) {
425
549
  let template = await readFile(join(templatesDir, action.templateFile), "utf-8");
426
550
  if (action.name === "installation") {
427
551
  const scriptsTemplate = await generateCustomScriptsTemplate(await readFile(join(templatesDir, "app-management", "custom-scripts.js.template"), "utf-8"), appManifest);
428
552
  template = applyCustomScripts(template, scriptsTemplate);
429
553
  }
430
- const actionPath = join(outputDir, `${action.name}.js`);
554
+ const actionPath = join(projectRoot, getActionPath(extensionPointId, action.name));
431
555
  await writeFile(actionPath, template, "utf-8");
432
556
  outputFiles.push(` ${relative(process.cwd(), actionPath)}`);
433
557
  }
@@ -450,7 +574,7 @@ function applyCustomScripts(installationTemplate, customScriptsTemplate) {
450
574
  async function generateCustomScriptsTemplate(template, appManifest) {
451
575
  if (!hasCustomInstallationSteps(appManifest)) return null;
452
576
  const projectRoot = await getProjectRootDirectory();
453
- const installationActionDir = join(projectRoot, getExtensionPointFolderPath(EXTENSIBILITY_EXTENSION_POINT_ID), GENERATED_ACTIONS_PATH);
577
+ const installationActionDir = join(projectRoot, getActionsDir(EXTENSIBILITY_EXTENSION_POINT_ID));
454
578
  const customSteps = appManifest.installation.customInstallationSteps;
455
579
  const importStatements = customSteps.map((step, index) => {
456
580
  let relativeImportPath = relative(installationActionDir, join(projectRoot, step.script));
@@ -464,72 +588,128 @@ async function generateCustomScriptsTemplate(template, appManifest) {
464
588
  }).join("\n");
465
589
  return template.replace(CUSTOM_IMPORTS_PLACEHOLDER, importStatements).replace(CUSTOM_SCRIPTS_MAP_PLACEHOLDER, scriptMap);
466
590
  }
591
+ /**
592
+ * Generates `registration/index.js` with the Admin UI SDK registration config
593
+ * inlined as a JS object literal.
594
+ * @param appManifest - The validated app config; must satisfy `hasAdminUiSdk`.
595
+ * @param extensionPointId - The extension point ID that owns the registration action.
596
+ * @param templatesDir - The root directory containing the action templates.
597
+ */
598
+ async function generateRegistrationActionFile(appManifest, extensionPointId, templatesDir = TEMPLATES_DIR) {
599
+ consola$1.start("Generating Admin UI SDK registration action...");
600
+ await makeOutputDirFor(getAdminUiSdkActionsDir(extensionPointId));
601
+ const projectRoot = await getProjectRootDirectory();
602
+ const template = await readFile(join(templatesDir, "admin-ui-sdk", "registration.js.template"), "utf-8");
603
+ const { registration } = appManifest.adminUiSdk;
604
+ const actionPath = join(projectRoot, getAdminUiSdkRegistrationActionPath(extensionPointId));
605
+ await writeFile(actionPath, await prettierFormat(template.replace(REGISTRATION_JSON_PLACEHOLDER, `const registration = ${stringify(registration)};`), actionPath), "utf-8");
606
+ consola$1.success(`Generated registration action at ${relative(process.cwd(), actionPath)}`);
607
+ }
608
+
609
+ //#endregion
610
+ //#region source/commands/generate/actions/main.ts
611
+ /**
612
+ * Runs the generate actions command for the given extension point.
613
+ * @param appManifest - The app manifest, used to determine which actions to generate and their content.
614
+ * @param templatesDir - The directory to load templates from, for testing purposes. Defaults to the generated actions template root.
615
+ */
616
+ async function run$5(appManifest, templatesDir = TEMPLATES_DIR) {
617
+ await generateActionFiles(appManifest, getRuntimeActions(await updateExtConfig(appManifest, EXTENSIBILITY_EXTENSION_POINT_ID), "app-management"), EXTENSIBILITY_EXTENSION_POINT_ID, templatesDir);
618
+ if (hasBusinessConfigSchema(appManifest)) await generateActionFiles(appManifest, getRuntimeActions(await updateExtConfig(appManifest, CONFIGURATION_EXTENSION_POINT_ID), "business-configuration"), CONFIGURATION_EXTENSION_POINT_ID, templatesDir);
619
+ if (hasAdminUiSdk(appManifest)) {
620
+ await updateExtConfig(appManifest, BACKEND_UI_EXTENSION_POINT_ID);
621
+ await generateRegistrationActionFile(appManifest, BACKEND_UI_EXTENSION_POINT_ID, templatesDir);
622
+ }
623
+ }
624
+ /** Run the generate actions command */
625
+ async function exec$5() {
626
+ try {
627
+ await run$5(await loadAppManifest());
628
+ } catch (error) {
629
+ if (error instanceof CommerceSdkValidationError) consola$1.error(error.display());
630
+ else consola$1.error(error);
631
+ process.exit(1);
632
+ }
633
+ }
467
634
 
468
635
  //#endregion
469
636
  //#region source/commands/generate/manifest/main.ts
470
- async function run$3(appConfig) {
637
+ async function run$4(appConfig) {
471
638
  consola$1.info("Generating app manifest...");
472
- const contents = stringify(appConfig, null, 2);
473
- await writeFile(join(await makeOutputDirFor(`${getExtensionPointFolderPath(EXTENSIBILITY_EXTENSION_POINT_ID)}/.generated`), APP_MANIFEST_FILE), contents, "utf-8");
639
+ const contents = stringify$1(appConfig, null, 2);
640
+ await writeFile(join(await makeOutputDirFor(getGeneratedDir(EXTENSIBILITY_EXTENSION_POINT_ID)), APP_MANIFEST_FILE), contents, "utf-8");
474
641
  consola$1.success(`Generated ${APP_MANIFEST_FILE}`);
475
642
  }
476
643
  /** Run the generate manifest command */
477
644
  async function exec$4() {
478
645
  try {
479
- await run$3(await loadAppManifest());
646
+ await run$4(await loadAppManifest());
480
647
  } catch (error) {
481
648
  if (error instanceof CommerceSdkValidationError) consola$1.error(error.display());
482
- consola$1.error(error);
649
+ else consola$1.error(error);
483
650
  process.exit(1);
484
651
  }
485
652
  }
486
653
 
654
+ //#endregion
655
+ //#region ../../packages-private/scripting-utils/source/filesystem/helpers.ts
656
+ /**
657
+ * Touch a file, creating it if it doesn't exist.
658
+ * @param filePath The path of the file to touch
659
+ */
660
+ async function touch(filePath) {
661
+ await writeFile(filePath, "", { flag: "wx" }).catch((e) => {
662
+ if (e.code !== "EEXIST") throw e;
663
+ });
664
+ }
665
+
487
666
  //#endregion
488
667
  //#region source/commands/generate/schema/main.ts
489
- async function run$2(appConfig) {
490
- consola$1.info("Generating configuration schema...");
668
+ async function run$3(appConfig) {
491
669
  if (!hasBusinessConfigSchema(appConfig)) {
492
- consola$1.warn("Business configuration schema not found");
670
+ consola$1.debug("Business configuration schema not found in application configuration. Nothing to do.");
493
671
  return;
494
672
  }
495
- const projectDir = dirname(await findNearestPackageJson() ?? process.cwd());
673
+ consola$1.info("Generating configuration schema...");
674
+ const projectDir = await getProjectRootDirectory();
496
675
  const envPath = join(projectDir, ".env");
676
+ await touch(envPath);
497
677
  process.loadEnvFile(envPath);
498
678
  if (appConfig.businessConfig.schema.some((field) => field.type === "password")) {
499
679
  const packageExec = getExecCommand(await detectPackageManager(projectDir));
500
680
  if ("AIO_COMMERCE_CONFIG_ENCRYPTION_KEY" in process.env && String(process.env.AIO_COMMERCE_CONFIG_ENCRYPTION_KEY).trim().length > 0) execSync(`${packageExec} aio-commerce-lib-config encryption validate`);
501
681
  else execSync(`${packageExec} aio-commerce-lib-config encryption setup`);
502
682
  }
503
- const contents = stringify(appConfig.businessConfig.schema, null, 2);
504
- await writeFile(join(await makeOutputDirFor(`${getExtensionPointFolderPath(CONFIGURATION_EXTENSION_POINT_ID)}/.generated`), CONFIG_SCHEMA_FILE_NAME), contents, "utf-8");
683
+ const contents = stringify$1(appConfig.businessConfig.schema, null, 2);
684
+ await writeFile(join(await makeOutputDirFor(getGeneratedDir(CONFIGURATION_EXTENSION_POINT_ID)), CONFIG_SCHEMA_FILE_NAME), contents, "utf-8");
505
685
  consola$1.success(`Generated ${CONFIG_SCHEMA_FILE_NAME}`);
506
686
  }
507
- /** Run the generate manifest command */
687
+ /** Run the generate schema command */
508
688
  async function exec$3() {
509
689
  try {
510
- await run$2(await loadAppManifest());
690
+ await run$3(await loadAppManifest());
511
691
  } catch (error) {
512
692
  if (error instanceof CommerceSdkValidationError) consola$1.error(error.display());
513
- consola$1.error(error);
693
+ else consola$1.error(error);
514
694
  process.exit(1);
515
695
  }
516
696
  }
517
697
 
518
698
  //#endregion
519
699
  //#region source/commands/hooks/postinstall.ts
520
- async function run$1(appManifest) {
700
+ async function run$2(appManifest, templatesDir) {
701
+ await run$5(appManifest, templatesDir);
521
702
  await run$4(appManifest);
522
703
  await run$3(appManifest);
523
- await run$2(appManifest);
524
704
  }
525
705
  /** Runs the postinstall hook */
526
706
  async function exec$2() {
527
707
  consola.debug("Running lib-app postinstall hook");
528
708
  try {
529
- await run$1(await loadAppManifest());
709
+ await run$2(await loadAppManifest());
530
710
  } catch (error) {
531
711
  if (error instanceof CommerceSdkValidationError) consola.error(error.display());
532
- consola.error(error);
712
+ else consola.error(error);
533
713
  process.exit(1);
534
714
  }
535
715
  }
@@ -585,19 +765,27 @@ function resolveImsS2SContext() {
585
765
  }
586
766
  /**
587
767
  * Syncs the IMS credentials environment variables from the configured IMS context in
588
- * the .env file, in a way that is compatible with `@adobe/aio-commerce-lib-auth`
768
+ * the .env file, in a way that is compatible with `@adobe/aio-commerce-lib-auth`.
589
769
  */
590
770
  async function syncImsCredentials() {
591
771
  const envPath = resolveEnvPath();
772
+ if (!existsSync(envPath)) return {
773
+ ok: false,
774
+ reason: "missing-env"
775
+ };
592
776
  const envVars = dotenv.parse(readFileSync(envPath, "utf8"));
593
777
  const imsContext = await resolveImsS2SContext();
594
- if (!imsContext) return;
778
+ if (!imsContext) return {
779
+ ok: false,
780
+ reason: "no-ims-context"
781
+ };
595
782
  const { data } = imsContext;
596
783
  for (const [key, value] of Object.entries(data)) {
597
784
  const oauthKey = IMS_KEYS[key];
598
785
  if (!oauthKey) continue;
599
786
  if (!envVars[oauthKey] || envVars[oauthKey] !== value) replaceEnvVar(envPath, oauthKey, value);
600
787
  }
788
+ return { ok: true };
601
789
  }
602
790
 
603
791
  //#endregion
@@ -605,17 +793,26 @@ async function syncImsCredentials() {
605
793
  /**
606
794
  * Runs the pre-app-build hook for the given extension.
607
795
  * @param extension - The extension to run the hook for.
796
+ * @param templatesDir - The directory to load templates from, for testing purposes. Defaults to the generated actions template root.
608
797
  */
609
- async function run(extension) {
798
+ async function run$1(extension, templatesDir = TEMPLATES_DIR) {
610
799
  const appManifest = await loadAppManifest();
611
800
  if (extension === "extensibility/1") {
612
- await run$3(appManifest);
801
+ const { doc: extensibilityExtConfig } = await readExtConfig(EXTENSIBILITY_EXTENSION_POINT_ID);
802
+ await run$4(appManifest);
803
+ await generateActionFiles(appManifest, getRuntimeActions(extensibilityExtConfig.toJS(), "app-management"), EXTENSIBILITY_EXTENSION_POINT_ID, templatesDir);
613
804
  consola.info("Syncing IMS credentials...");
614
805
  await syncImsCredentials();
615
806
  return;
616
807
  }
617
808
  if (extension === "configuration/1") {
618
- await run$2(appManifest);
809
+ const { doc: businessConfigExtConfig } = await readExtConfig(CONFIGURATION_EXTENSION_POINT_ID);
810
+ await run$3(appManifest);
811
+ await generateActionFiles(appManifest, getRuntimeActions(businessConfigExtConfig.toJS(), "business-configuration"), CONFIGURATION_EXTENSION_POINT_ID, templatesDir);
812
+ return;
813
+ }
814
+ if (extension === "backend-ui/1") {
815
+ if (hasAdminUiSdk(appManifest)) await generateRegistrationActionFile(appManifest, BACKEND_UI_EXTENSION_POINT_ID);
619
816
  return;
620
817
  }
621
818
  throw new Error(`Unsupported extension: ${extension}`);
@@ -626,10 +823,10 @@ async function exec$1() {
626
823
  const extension = process.env.EXTENSION;
627
824
  try {
628
825
  if (!extension) throw new Error("EXTENSION environment variable is not set");
629
- await run(extension);
826
+ await run$1(extension);
630
827
  } catch (error) {
631
828
  if (error instanceof CommerceSdkValidationError) consola.error(error.display());
632
- consola.error(error);
829
+ else consola.error(error);
633
830
  process.exit(1);
634
831
  }
635
832
  }
@@ -664,7 +861,7 @@ const DOMAIN_DEFAULTS = {
664
861
  },
665
862
  events: [{
666
863
  name: "plugin.sample_event",
667
- fields: [],
864
+ fields: [{ name: "*" }],
668
865
  label: "Sample Event",
669
866
  description: "Use case description for the event.",
670
867
  runtimeActions: ["my-package/handle-sample-event"]
@@ -686,12 +883,50 @@ const DOMAIN_DEFAULTS = {
686
883
  name: "sample-step",
687
884
  description: "Use case description for the step.",
688
885
  script: "./path/to/script.js"
886
+ }],
887
+ webhooks: [{
888
+ label: "Sample Webhook",
889
+ description: "A sample Commerce Webhook handler.",
890
+ runtimeAction: "my-package/handle-webhook",
891
+ category: "modification",
892
+ webhook: {
893
+ webhook_method: "plugin.sample.event",
894
+ webhook_type: "after",
895
+ batch_name: "my_app",
896
+ hook_name: "sample_hook",
897
+ method: "POST"
898
+ }
899
+ }, {
900
+ label: "Sample Webhook with URL",
901
+ description: "A sample Commerce Webhook handler using an explicit URL.",
902
+ category: "modification",
903
+ webhook: {
904
+ webhook_method: "plugin.sample.event",
905
+ webhook_type: "after",
906
+ batch_name: "my_app",
907
+ hook_name: "sample_hook_url",
908
+ method: "POST",
909
+ url: "https://example.com/webhook"
910
+ }
689
911
  }]
690
912
  };
691
913
 
692
914
  //#endregion
693
915
  //#region source/commands/init/utils.ts
694
- /** Prompt the user to scaffold a new Commerce App configuration. */
916
+ /**
917
+ * Convert the given init options to answers for scaffolding the app config. This is useful for non-interactive use cases, such as running the command with a config file or from another script.
918
+ * @param flags - The initialization options
919
+ */
920
+ function initFlagsToScaffoldAppAnswers(flags) {
921
+ const configFile = `${COMMERCE_APP_CONFIG_FILE}.${flags.configFormat}`;
922
+ return {
923
+ appName: flags.appName,
924
+ configFile,
925
+ configFormat: flags.configFormat,
926
+ domains: new Set(flags.domains)
927
+ };
928
+ }
929
+ /** Prompt the user to scaffold a new Commerce App configuration. When options are provided, prompts are skipped. */
695
930
  async function promptForCommerceAppConfig() {
696
931
  const configFormat = await consola.prompt("What format do you want to use for the config file?", {
697
932
  type: "select",
@@ -727,6 +962,10 @@ async function promptForCommerceAppConfig() {
727
962
  label: "Commerce Events",
728
963
  value: "eventing.commerce"
729
964
  },
965
+ {
966
+ label: "Commerce Webhooks",
967
+ value: "webhooks"
968
+ },
730
969
  {
731
970
  label: "External Events",
732
971
  value: "eventing.external"
@@ -737,10 +976,13 @@ async function promptForCommerceAppConfig() {
737
976
  }
738
977
  ]
739
978
  });
979
+ const configFile = `${COMMERCE_APP_CONFIG_FILE}.${configFormat}`;
980
+ const domains = new Set(featuresToAdd);
981
+ consola.log("");
740
982
  return {
741
983
  appName,
742
- configFile: `${COMMERCE_APP_CONFIG_FILE}.${configFormat}`,
743
- domains: new Set(featuresToAdd),
984
+ configFile,
985
+ domains,
744
986
  configFormat
745
987
  };
746
988
  }
@@ -765,6 +1007,7 @@ async function getDefaultCommerceAppConfig(cwd, { appName, configFormat, domains
765
1007
  external: DOMAIN_DEFAULTS["eventing.external"]
766
1008
  };
767
1009
  if (domains.has("installation.customInstallationSteps")) defaultConfig.installation = { customInstallationSteps: DOMAIN_DEFAULTS["installation.customInstallationSteps"] };
1010
+ if (domains.has("webhooks")) defaultConfig.webhooks = DOMAIN_DEFAULTS.webhooks;
768
1011
  return [
769
1012
  importStatement,
770
1013
  "",
@@ -779,9 +1022,8 @@ async function getDefaultCommerceAppConfig(cwd, { appName, configFormat, domains
779
1022
  */
780
1023
  async function addExtensionPointToAppConfig(extensionPointId, rootDirectory, commentBefore) {
781
1024
  const appConfigPath = join(rootDirectory, APP_CONFIG_FILE);
782
- const extensionPointFolderPath = join(rootDirectory, getExtensionPointFolderPath(extensionPointId));
783
1025
  let doc;
784
- const includePath = relative(rootDirectory, join(extensionPointFolderPath, "ext.config.yaml"));
1026
+ const includePath = relative(rootDirectory, join(rootDirectory, getExtConfigPath(extensionPointId)));
785
1027
  try {
786
1028
  doc = await readYamlFile(appConfigPath);
787
1029
  } catch (error) {
@@ -839,12 +1081,12 @@ async function addExtensionPointToInstallYaml(extensionPointId, rootDirectory, c
839
1081
 
840
1082
  //#endregion
841
1083
  //#region source/commands/init/lib.ts
842
- /** Ensure app.commerce.config file exists, allow creating if it doesn't */
843
- async function ensureCommerceAppConfig(cwd = process.cwd()) {
1084
+ /** Ensure app.commerce.config file exists, allow creating if it doesn't. When options are provided, prompts are skipped. */
1085
+ async function ensureCommerceAppConfig(cwd = process.cwd(), formatConfig = true, flags) {
844
1086
  let config = null;
845
1087
  try {
846
1088
  config = await readCommerceAppConfig(cwd);
847
- } catch (_) {}
1089
+ } catch {}
848
1090
  if (config) try {
849
1091
  const validatedConfig = validateCommerceAppConfig(config);
850
1092
  consola$1.success(`${COMMERCE_APP_CONFIG_FILE} found and is valid. Continuing...`);
@@ -855,119 +1097,126 @@ async function ensureCommerceAppConfig(cwd = process.cwd()) {
855
1097
  } catch (error) {
856
1098
  throw new Error(`${COMMERCE_APP_CONFIG_FILE} is invalid`, { cause: error });
857
1099
  }
858
- consola$1.warn(`${COMMERCE_APP_CONFIG_FILE} not found.`);
859
- if (!await consola$1.prompt(`Do you want to create a ${"app.commerce.config"} file? (y/n)`, {
860
- type: "confirm",
861
- initial: true,
862
- default: false
863
- })) throw new Error("Initialization cancelled.");
864
- const answers = await promptForCommerceAppConfig();
1100
+ if (!flags) {
1101
+ consola$1.warn(`${COMMERCE_APP_CONFIG_FILE} not found.`);
1102
+ if (!await consola$1.prompt(`Do you want to create a ${"app.commerce.config"} file? (y/n)`, {
1103
+ type: "confirm",
1104
+ initial: true,
1105
+ default: false
1106
+ })) throw new Error("Initialization cancelled.");
1107
+ }
1108
+ const answers = flags ? initFlagsToScaffoldAppAnswers(flags) : await promptForCommerceAppConfig();
865
1109
  try {
866
1110
  const configContent = await getDefaultCommerceAppConfig(cwd, answers);
867
- consola$1.info(`Creating ${answers.configFile}...`);
868
1111
  const path = join(await getProjectRootDirectory(cwd), answers.configFile);
869
- await writeFile(path, await prettier.format(configContent, {
870
- semi: true,
871
- quoteStyle: "double",
872
- arrowParens: "always",
873
- bracketSameLine: true,
874
- bracketSpacing: true,
875
- trailingComma: "all",
876
- tabWidth: 2,
877
- useTabs: false,
878
- printWidth: 80,
879
- filepath: path
880
- }), "utf-8");
1112
+ consola$1.info(`Creating ${answers.configFile}...`);
1113
+ if (formatConfig) await writeFile(path, await prettierFormat(configContent, path), "utf-8");
1114
+ else await writeFile(path, configContent, "utf-8");
1115
+ const config = await parseCommerceAppConfig(cwd);
881
1116
  consola$1.success(`Created ${answers.configFile}`);
882
1117
  return {
883
- config: await parseCommerceAppConfig(cwd),
1118
+ config,
884
1119
  domains: answers.domains
885
1120
  };
886
1121
  } catch (error) {
887
1122
  throw new Error(`Failed to create ${answers.configFile}`, { cause: error });
888
1123
  }
889
1124
  }
890
- /** Ensure package.json has the postinstall script */
1125
+ /**
1126
+ * Ensure a package.json exists and detect the package manager.
1127
+ * @param cwd - Directory to check; defaults to `process.cwd()`
1128
+ */
891
1129
  async function ensurePackageJson(cwd = process.cwd()) {
892
- const packageJson = await readPackageJson(cwd);
893
- if (!packageJson) {
1130
+ const existing = await readPackageJson(cwd);
1131
+ let packageJson;
1132
+ if (existing) packageJson = existing;
1133
+ else {
894
1134
  consola$1.warn("package.json not found. Creating one...");
895
- const packageJsonContent = {
1135
+ packageJson = {
896
1136
  name: "my-commerce-app",
897
1137
  version: "1.0.0",
898
- private: true,
899
- scripts: { postinstall: "npx aio-commerce-lib-app hooks postinstall" }
1138
+ private: true
900
1139
  };
901
- await writeFile(join(resolve(cwd), PACKAGE_JSON_FILE), JSON.stringify(packageJsonContent, null, 2), "utf-8");
1140
+ await writeFile(join(resolve(cwd), PACKAGE_JSON_FILE), JSON.stringify(packageJson, null, 2), "utf-8");
902
1141
  consola$1.success("Wrote package.json");
903
- return {
904
- packageJson: packageJsonContent,
905
- packageManager: "npm",
906
- execCommand: "npx"
907
- };
908
1142
  }
909
- const packageManager = await detectPackageManager();
1143
+ const packageManager = await detectPackageManager(cwd);
910
1144
  const execCommand = getExecCommand(packageManager);
911
- const postinstallScript = `${execCommand} aio-commerce-lib-app hooks postinstall`;
912
- packageJson.scripts ??= {};
913
- if (packageJson.scripts.postinstall === postinstallScript || packageJson.scripts.postinstall?.includes(postinstallScript)) {
914
- consola$1.success(`postinstall script already configured in ${PACKAGE_JSON_FILE}`);
915
- return {
916
- packageJson,
917
- packageManager,
918
- execCommand
919
- };
920
- }
921
- if (packageJson.scripts.postinstall) {
922
- consola$1.warn(`${PACKAGE_JSON_FILE} already has a postinstall script. Adding a new one...`);
923
- execSync(`npm pkg set scripts.postinstall="${`${packageJson.scripts.postinstall} && ${postinstallScript}`}"`);
924
- } else {
925
- consola$1.info(`Adding postinstall script to ${PACKAGE_JSON_FILE}...`);
926
- execSync(`npm pkg set scripts.postinstall="${postinstallScript}"`);
927
- }
928
- consola$1.success(`Added postinstall script to ${PACKAGE_JSON_FILE}`);
929
1145
  return {
930
1146
  packageJson,
931
1147
  packageManager,
932
1148
  execCommand
933
1149
  };
934
1150
  }
1151
+ /**
1152
+ * Register the `hooks postinstall` script in package.json.
1153
+ * @param execCommand - Prefix for running local binaries (e.g. `pnpm exec`, `npx`)
1154
+ * @param cwd - Directory containing the package.json to update
1155
+ */
1156
+ async function writePostinstallHook(execCommand, cwd = process.cwd()) {
1157
+ const postinstallScript = `${execCommand} aio-commerce-lib-app hooks postinstall`;
1158
+ const pkg = await NpmPackageJson.load(cwd);
1159
+ const existing = pkg.content.scripts?.postinstall;
1160
+ if (existing === postinstallScript || existing?.includes(postinstallScript)) {
1161
+ consola$1.success(`postinstall script already configured in ${PACKAGE_JSON_FILE}`);
1162
+ return;
1163
+ }
1164
+ const nextPostinstall = existing ? `${existing} && ${postinstallScript}` : postinstallScript;
1165
+ if (existing) consola$1.warn(`${PACKAGE_JSON_FILE} already has a postinstall script. Adding a new one...`);
1166
+ else consola$1.info(`Adding postinstall script to ${PACKAGE_JSON_FILE}...`);
1167
+ pkg.update({ scripts: {
1168
+ ...pkg.content.scripts,
1169
+ postinstall: nextPostinstall
1170
+ } });
1171
+ await pkg.save();
1172
+ consola$1.success(`Added postinstall script to ${PACKAGE_JSON_FILE}`);
1173
+ }
935
1174
  /** Ensure app.config.yaml has the extension reference */
936
1175
  async function ensureAppConfig(domains, cwd = process.cwd()) {
937
1176
  const rootDirectory = await getProjectRootDirectory(cwd);
938
1177
  if (domains.has("businessConfig.schema")) await addExtensionPointToAppConfig(CONFIGURATION_EXTENSION_POINT_ID, rootDirectory, " This extension is required for business configuration. Do not remove.");
1178
+ if (domains.has("adminUiSdk")) await addExtensionPointToAppConfig(BACKEND_UI_EXTENSION_POINT_ID, rootDirectory, " This extension is required for Admin UI SDK. Do not remove.");
939
1179
  await addExtensionPointToAppConfig(EXTENSIBILITY_EXTENSION_POINT_ID, rootDirectory, " This extension is required for app management. Do not remove.");
940
1180
  }
941
- /** Install the given dependencies */
1181
+ /**
1182
+ * Install the given dependencies.
1183
+ * @param packageManager - The detected package manager
1184
+ * @param dependencies - Package specifiers to install; no-op when empty
1185
+ * @param cwd - Working directory for the install command
1186
+ */
942
1187
  function runInstall(packageManager, dependencies, cwd = process.cwd()) {
1188
+ if (dependencies.length === 0) return;
943
1189
  const dependencyListString = dependencies.map((dependency) => ` - ${dependency}`).join("\n");
944
- consola$1.info(`Installing the following dependencies with ${packageManager}:\n${dependencyListString}`);
945
- const packagesToInstall = dependencies.join(" ");
946
- const installCommand = {
947
- pnpm: `pnpm add ${packagesToInstall}`,
948
- yarn: `yarn add ${packagesToInstall}`,
949
- bun: `bun add ${packagesToInstall}`,
950
- npm: `npm install ${packagesToInstall}`
951
- }[packageManager];
952
- try {
953
- execSync(installCommand, {
954
- cwd,
955
- stdio: "inherit"
956
- });
957
- consola$1.success("Dependencies installed successfully");
958
- } catch (error) {
959
- throw new Error(`Failed to install dependencies automatically. Please install manually: ${installCommand}`, { cause: error });
960
- }
1190
+ consola$1.start([`Installing the following dependencies with ${packageManager}:\n${dependencyListString}\n`, "This may take a few seconds...\n"].join("\n"));
1191
+ const { command, args } = getInstallCommand(packageManager, dependencies);
1192
+ const displayCommand = [command, ...args].join(" ");
1193
+ const result = spawnSync(command, args, {
1194
+ cwd,
1195
+ stdio: "inherit"
1196
+ });
1197
+ if (result.error || result.status !== 0) throw new Error(`Failed to install dependencies automatically. Please install manually: ${displayCommand}`, { cause: result.error ?? /* @__PURE__ */ new Error(`Install exited with code ${result.status}`) });
1198
+ consola$1.log("");
1199
+ consola$1.success("Dependencies installed successfully");
961
1200
  }
962
- /** Install required dependencies */
1201
+ /**
1202
+ * Install the domain-specific dependencies derived from the selected domains.
1203
+ * @param packageManager - The detected package manager
1204
+ * @param domains - Domains enabled in the commerce app config
1205
+ * @param cwd - Working directory for the install command
1206
+ */
963
1207
  function installDependencies(packageManager, domains, cwd = process.cwd()) {
964
1208
  const packages = [];
965
- if (domains.has("businessConfig.schema")) packages.push("@adobe/aio-commerce-lib-config");
1209
+ if (domains.has("businessConfig.schema")) packages.push(`@adobe/aio-commerce-lib-config@^1.3.0`);
1210
+ if (packages.length === 0) {
1211
+ consola$1.info("No additional domain dependencies to install.");
1212
+ return;
1213
+ }
966
1214
  runInstall(packageManager, packages, cwd);
967
1215
  }
968
1216
  /** Run the generation command */
969
1217
  async function runGeneration(appConfig, execCommand) {
970
1218
  try {
1219
+ await run$5(appConfig);
971
1220
  await run$4(appConfig);
972
1221
  await run$3(appConfig);
973
1222
  } catch (error) {
@@ -978,35 +1227,45 @@ async function runGeneration(appConfig, execCommand) {
978
1227
  async function ensureInstallYaml(domains, cwd = process.cwd()) {
979
1228
  const rootDirectory = await getProjectRootDirectory(cwd);
980
1229
  if (domains.has("businessConfig.schema")) await addExtensionPointToInstallYaml(CONFIGURATION_EXTENSION_POINT_ID, rootDirectory, " This extension is required for business configuration. Do not remove.");
1230
+ if (domains.has("adminUiSdk")) await addExtensionPointToInstallYaml(BACKEND_UI_EXTENSION_POINT_ID, rootDirectory, " This extension is required for Admin UI SDK. Do not remove.");
981
1231
  await addExtensionPointToInstallYaml(EXTENSIBILITY_EXTENSION_POINT_ID, rootDirectory, " This extension is required for app management. Do not remove.");
982
1232
  }
983
1233
 
984
1234
  //#endregion
985
1235
  //#region source/commands/init/main.ts
986
- const REQUIRED_DEPENDENCIES = ["@adobe/aio-commerce-lib-app", "@adobe/aio-commerce-sdk"];
987
- /** Initialize the project with @adobe/aio-commerce-lib-config */
1236
+ const REQUIRED_DEPENDENCIES = ["@adobe/aio-commerce-sdk", `@adobe/aio-commerce-lib-app@1.4.0`];
1237
+ /** Initialize the project */
1238
+ async function run(flags, extraOptions) {
1239
+ consola$1.start("Initializing app...");
1240
+ const { execCommand, packageManager } = await ensurePackageJson();
1241
+ runInstall(packageManager, REQUIRED_DEPENDENCIES);
1242
+ const { config, domains } = await ensureCommerceAppConfig(process.cwd(), extraOptions?.formatConfig ?? true, flags);
1243
+ installDependencies(packageManager, domains);
1244
+ const pkg = await NpmPackageJson.load(process.cwd());
1245
+ pkg.update({
1246
+ name: config.metadata.id,
1247
+ version: config.metadata.version,
1248
+ description: config.metadata.description
1249
+ });
1250
+ await pkg.save();
1251
+ await runGeneration(config, execCommand);
1252
+ await ensureAppConfig(domains);
1253
+ await ensureInstallYaml(domains);
1254
+ await writePostinstallHook(execCommand);
1255
+ consola$1.success("Initialization complete!");
1256
+ consola$1.box([
1257
+ "Next steps:",
1258
+ " - Review and customize app.commerce.config.*",
1259
+ " - Build and deploy your app"
1260
+ ].join("\n"));
1261
+ }
1262
+ /** Run the init command */
988
1263
  async function exec() {
989
1264
  try {
990
- consola$1.start("Initializing app...");
991
- const { execCommand, packageManager } = await ensurePackageJson();
992
- runInstall(packageManager, REQUIRED_DEPENDENCIES);
993
- const { config, domains } = await ensureCommerceAppConfig();
994
- installDependencies(packageManager, domains);
995
- execSync(`npm pkg set name="${config.metadata.id}"`);
996
- execSync(`npm pkg set version="${config.metadata.version}"`);
997
- execSync(`npm pkg set description="${config.metadata.description}"`);
998
- await runGeneration(config, execCommand);
999
- await ensureAppConfig(domains);
1000
- await ensureInstallYaml(domains);
1001
- consola$1.success("Initialization complete!");
1002
- consola$1.box([
1003
- "Next steps:",
1004
- " - Review and customize app.commerce.config.*",
1005
- " - Build and deploy your app"
1006
- ].join("\n"));
1265
+ await run();
1007
1266
  } catch (error) {
1008
1267
  if (error instanceof CommerceSdkValidationError) consola$1.error(error.display());
1009
- consola$1.error(error);
1268
+ else consola$1.error(error);
1010
1269
  process.exit(1);
1011
1270
  }
1012
1271
  }