@aligent/nx-appbuilder 0.2.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 (82) hide show
  1. package/README.md +203 -0
  2. package/generators.json +18 -0
  3. package/package.json +20 -0
  4. package/src/generators/app/files/base/.editorconfig.template +18 -0
  5. package/src/generators/app/files/base/.gitignore.template +9 -0
  6. package/src/generators/app/files/base/.nvmrc.template +1 -0
  7. package/src/generators/app/files/base/README.md.template +44 -0
  8. package/src/generators/app/files/base/app.config.yaml.template +77 -0
  9. package/src/generators/app/files/base/babel.actions.config.js.template +6 -0
  10. package/src/generators/app/files/base/eslint.config.mjs.template +8 -0
  11. package/src/generators/app/files/base/global-types/@adobe/ADOBE_TYPES.md.template +10 -0
  12. package/src/generators/app/files/base/global-types/@adobe/aio-sdk/aio-core-logging.d.ts.template +15 -0
  13. package/src/generators/app/files/base/global-types/@adobe/aio-sdk/aio-lib-core-config.d.ts.template +43 -0
  14. package/src/generators/app/files/base/hooks/check-action-types.sh.template +13 -0
  15. package/src/generators/app/files/base/prettier.config.mjs.template +3 -0
  16. package/src/generators/app/files/base/src/actions/tsconfig.json.template +8 -0
  17. package/src/generators/app/files/base/tests/tsconfig.json.template +13 -0
  18. package/src/generators/app/files/base/tsconfig.base.json.template +14 -0
  19. package/src/generators/app/files/base/tsconfig.json.template +3 -0
  20. package/src/generators/app/files/base/vitest.config.ts.template +19 -0
  21. package/src/generators/app/files/commerce-backend-ui/extension-manifest.json.template +8 -0
  22. package/src/generators/app/files/commerce-backend-ui/hooks/check-web-types.sh.template +11 -0
  23. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/actions/registration/index.ts.template +32 -0
  24. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/actions/tsconfig.json.template +8 -0
  25. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/actions/utils/http.ts.template +18 -0
  26. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/actions/utils/runtime.ts.template +47 -0
  27. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/actions/utils/utils.ts.template +187 -0
  28. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/ext.config.yaml.template +20 -0
  29. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/.gitignore.template +2 -0
  30. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/index.html.template +15 -0
  31. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/components/App.tsx.template +37 -0
  32. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/components/ExtensionRegistration.tsx.template +15 -0
  33. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/config.json.d.ts.template +4 -0
  34. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/constants/extension.ts.template +1 -0
  35. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/context/AdobeRuntimeContextProvider.tsx.template +65 -0
  36. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/context/PageContextProvider.tsx.template +78 -0
  37. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/hooks/useAppBuilderAction.ts.template +41 -0
  38. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/hooks/useLazyAppBuilderAction.ts.template +116 -0
  39. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/index.css.template +4 -0
  40. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/index.tsx.template +20 -0
  41. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/src/types/ActionName.ts.template +9 -0
  42. package/src/generators/app/files/commerce-backend-ui/src/commerce-backend-ui-1/web-src/tsconfig.json.template +14 -0
  43. package/src/generators/app/files/commerce-config/src/commerce-configuration-1/ext.config.yaml.template +37 -0
  44. package/src/generators/app/files/commerce-config/src/commerce-configuration-1/my-webpack-config.cjs.template +24 -0
  45. package/src/generators/app/files/commerce-extensibility/app.commerce.config.ts.template +138 -0
  46. package/src/generators/app/files/commerce-extensibility/install.yaml.template +17 -0
  47. package/src/generators/app/files/commerce-extensibility/src/commerce-extensibility-1/ext.config.yaml.template +25 -0
  48. package/src/generators/app/files/commerce-extensibility/src/commerce-extensibility-1/my-webpack-config.cjs.template +24 -0
  49. package/src/generators/app/files/events/global-types/@adobe/aio-sdk/aio-lib-events.d.ts.template +7 -0
  50. package/src/generators/app/files/events/src/actions/handle-sample-event.ts.template +35 -0
  51. package/src/generators/app/files/install-steps/scripts/install/sample-step.js.template +14 -0
  52. package/src/generators/app/files/rest-actions/src/actions/rest-sample.ts.template +30 -0
  53. package/src/generators/app/files/scheduled/src/actions/cron-sample.ts.template +23 -0
  54. package/src/generators/app/generator.d.ts +3 -0
  55. package/src/generators/app/generator.js +38 -0
  56. package/src/generators/app/lib/apply-feature-files.d.ts +9 -0
  57. package/src/generators/app/lib/apply-feature-files.js +76 -0
  58. package/src/generators/app/lib/compose-package-json.d.ts +3 -0
  59. package/src/generators/app/lib/compose-package-json.js +164 -0
  60. package/src/generators/app/lib/normalize-options.d.ts +3 -0
  61. package/src/generators/app/lib/normalize-options.js +67 -0
  62. package/src/generators/app/lib/template-package/package.json +31 -0
  63. package/src/generators/app/lib/update-root-package.d.ts +6 -0
  64. package/src/generators/app/lib/update-root-package.js +23 -0
  65. package/src/generators/app/lib/update-root-tsconfig.d.ts +8 -0
  66. package/src/generators/app/lib/update-root-tsconfig.js +24 -0
  67. package/src/generators/app/schema.d.ts +32 -0
  68. package/src/generators/app/schema.json +73 -0
  69. package/src/generators/preset/files/.gitignore.template +30 -0
  70. package/src/generators/preset/files/.npmrc.template +2 -0
  71. package/src/generators/preset/files/.nvmrc.template +1 -0
  72. package/src/generators/preset/files/README.md.template +38 -0
  73. package/src/generators/preset/files/tsconfig.json.template +6 -0
  74. package/src/generators/preset/nx-json.d.ts +4 -0
  75. package/src/generators/preset/nx-json.js +36 -0
  76. package/src/generators/preset/preset.d.ts +13 -0
  77. package/src/generators/preset/preset.js +113 -0
  78. package/src/generators/preset/schema.d.ts +4 -0
  79. package/src/generators/preset/schema.json +25 -0
  80. package/src/index.d.ts +2 -0
  81. package/src/index.js +19 -0
  82. package/tsconfig.lib.tsbuildinfo +1 -0
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Helper constant for HTTP status codes
3
+ *
4
+ * Caution: 500 errors will cause an action to be retried for 24 hours with exponential backoff
5
+ *
6
+ * https://developer.adobe.com/events/docs/support/faq/#what-happens-if-my-webhook-is-down-why-is-my-event-registration-marked-as-unstable
7
+ */
8
+ export const STATUS_CODES = {
9
+ OK: 200,
10
+ Created: 201,
11
+ Accepted: 202,
12
+ NoContent: 204,
13
+ BadRequest: 400,
14
+ Unauthorized: 401,
15
+ InternalServerError: 500,
16
+ } as const;
17
+
18
+ export type StatusCode = (typeof STATUS_CODES)[keyof typeof STATUS_CODES];
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Request parameters set by the OpenWhisk framework.
3
+ * https://developer.adobe.com/runtime/docs/guides/reference/environment_variables/
4
+ */
5
+ export interface RequestParameters {
6
+ __ow_method: string;
7
+ __ow_headers: {
8
+ authorization?: string;
9
+ [key: string]: string | number | undefined;
10
+ };
11
+ __ow_path: string;
12
+ __ow_body: string;
13
+ __ow_query: string;
14
+ [key: string]: unknown;
15
+ }
16
+
17
+ /**
18
+ * Environment variables set by the OpenWhisk framework.
19
+ * https://developer.adobe.com/runtime/docs/guides/reference/environment_variables/
20
+ */
21
+ export interface EnvironmentVariables {
22
+ __OW_ACTION_NAME: string;
23
+ __OW_ACTION_VERSION: string;
24
+ __OW_ACTIVATION_ID: string;
25
+ __OW_ALLOW_CONCURRENT: string;
26
+ __OW_API_HOST: string;
27
+ __OW_CLOUD: string;
28
+ __OW_NAMESPACE: string;
29
+ __OW_REGION: string;
30
+ __OW_TRANSACTION_ID: string;
31
+ }
32
+
33
+ interface SuccessResponse<B> {
34
+ statusCode: number;
35
+ body: B;
36
+ }
37
+
38
+ interface ErrorResponse {
39
+ error: {
40
+ statusCode: number;
41
+ body: {
42
+ error: string;
43
+ };
44
+ };
45
+ }
46
+
47
+ export type Response<B> = Promise<SuccessResponse<B> | ErrorResponse>;
@@ -0,0 +1,187 @@
1
+ import type { Paths, SetRequiredDeep, SimplifyDeep, UnionToIntersection } from 'type-fest';
2
+ import { RequestParameters } from './runtime.ts';
3
+
4
+ /**
5
+ *
6
+ * Returns a log ready string of the action input parameters.
7
+ * The `Authorization` header content will be replaced by '<hidden>'.
8
+ *
9
+ * @param {object} params action input parameters.
10
+ *
11
+ * @returns {string} A sanitized string containing the input parameters
12
+ *
13
+ */
14
+ export function stringParameters<T extends RequestParameters>(params: T): string {
15
+ // hide authorization token without overriding params
16
+ let headers = params.__ow_headers || {};
17
+ if (headers.authorization) {
18
+ headers = { ...headers, authorization: '<hidden>' };
19
+ }
20
+ return JSON.stringify({ ...params, __ow_headers: headers });
21
+ }
22
+
23
+ /**
24
+ * Type guard to check if a value is a classic object (i.e. indexable by string keys).
25
+ *
26
+ * @param {unknown} value value to check.
27
+ * @returns {boolean} true if the value is an object, false otherwise.
28
+ */
29
+ function isRecord(value: unknown): value is Record<string, unknown> {
30
+ return typeof value === 'object' && value !== null && value.constructor === Object;
31
+ }
32
+
33
+ /**
34
+ * Safely retrieves a value from an object using a dot-notation path.
35
+ * Returns undefined if the full path does not exist.
36
+ *
37
+ * @param {Record<string, unknown>} obj object to check.
38
+ * @param {string} path dot-notation path to the value.
39
+ * @returns {unknown} the value at the path or undefined if the path does not exist.
40
+ */
41
+ function getValueByPath(obj: Record<string, unknown>, path: string): unknown {
42
+ const keys = path.split('.');
43
+ let currentValue: unknown = obj;
44
+
45
+ for (const key of keys) {
46
+ if (!isRecord(currentValue)) {
47
+ return undefined;
48
+ }
49
+ currentValue = currentValue[key];
50
+ }
51
+
52
+ return currentValue;
53
+ }
54
+
55
+ /**
56
+ * Returns the list of missing keys from an object based on required paths.
57
+ * A key is missing if its value at the specified path is undefined or ''.
58
+ *
59
+ * @param {Record<string, unknown>} obj object to check.
60
+ * @param {string[]} required list of required keys (can use dot notation for nesting).
61
+ * @returns {string[]} Array of missing keys.
62
+ */
63
+ function getMissingKeys(obj: Record<string, unknown>, required: string[]): string[] {
64
+ return required.filter(requiredPath => {
65
+ const value = getValueByPath(obj, requiredPath);
66
+ return value === undefined || value === '';
67
+ });
68
+ }
69
+
70
+ type BuildObject<P extends string> = P extends `${infer U}.${infer Rest}`
71
+ ? { [key in U]: BuildObject<Rest> }
72
+ : { [key in P]: unknown };
73
+
74
+ export type ObjectFromPaths<P extends string[]> = SimplifyDeep<
75
+ UnionToIntersection<BuildObject<P[number]>>
76
+ >;
77
+
78
+ /**
79
+ *
80
+ * Returns the list of missing keys giving an object and its required keys.
81
+ * A parameter is missing if its value is undefined or ''.
82
+ * A value of 0 or null is not considered as missing.
83
+ *
84
+ * @param {object} params action input parameters.
85
+ * @param {array} requiredHeaders list of required input headers.
86
+ * @param {array} requiredParams list of required input parameters.
87
+ * Each element can be multi level deep using a '.' separator e.g. 'myRequiredObj.myRequiredKey'.
88
+ *
89
+ * @returns {object} Returns an object with the following properties:
90
+ * - success: boolean
91
+ * - data: The input parameters with the required keys set
92
+ * - error: A string describing the missing inputs if any
93
+ *
94
+ */
95
+ export function checkMissingRequestInputs<
96
+ const T extends Partial<RequestParameters>,
97
+ const P extends Array<Paths<T> | Paths<T>>,
98
+ const H extends Array<Paths<T> | Paths<T['__ow_headers']>>,
99
+ >(params: T, requiredParams: P, requiredHeaders: H) {
100
+ let errorMessage: string | null = null;
101
+
102
+ // check for missing headers, including those added by OpenWhisk
103
+ const missingHeaders = getMissingKeys({ ...params.__ow_headers }, requiredHeaders);
104
+ if (missingHeaders.length > 0) {
105
+ errorMessage = `missing header(s) '${missingHeaders}'`;
106
+ }
107
+
108
+ // check for missing parameters
109
+ const missingParams = getMissingKeys(params, requiredParams);
110
+ if (missingParams.length > 0) {
111
+ if (errorMessage) {
112
+ errorMessage += ' and ';
113
+ } else {
114
+ errorMessage = '';
115
+ }
116
+ errorMessage += `missing parameter(s) '${missingParams}'`;
117
+ }
118
+
119
+ if (errorMessage) {
120
+ return {
121
+ success: false as const,
122
+ error: errorMessage,
123
+ };
124
+ }
125
+
126
+ return {
127
+ success: true as const,
128
+ data: params as SetRequiredDeep<T, [...P][number]> & {
129
+ __ow_headers: {
130
+ [key in H[number]]: string;
131
+ };
132
+ },
133
+ };
134
+ }
135
+
136
+ /**
137
+ *
138
+ * Extracts the bearer token string from the Authorization header in the request parameters.
139
+ *
140
+ * @param {object} params action input parameters.
141
+ *
142
+ * @returns {string|undefined} the token string or undefined if not set in request headers.
143
+ *
144
+ */
145
+ export function getBearerToken<T extends Pick<RequestParameters, '__ow_headers'>>(
146
+ params: T
147
+ ): string | undefined {
148
+ const bearerPrefix = 'Bearer ';
149
+ if (
150
+ typeof params.__ow_headers?.authorization === 'string' &&
151
+ params.__ow_headers.authorization.startsWith(bearerPrefix)
152
+ ) {
153
+ return params.__ow_headers.authorization.substring(bearerPrefix.length);
154
+ }
155
+ return undefined;
156
+ }
157
+
158
+ /**
159
+ *
160
+ * Returns an error response object and attempts to log.info the status code and error message
161
+ *
162
+ * @param {number} statusCode the error status code.
163
+ * e.g. 400
164
+ * @param {string} message the error message.
165
+ * e.g. 'missing xyz parameter'
166
+ * @param {*} [logger] an optional logger instance object with an `info` method
167
+ * e.g. `new require('@adobe/aio-sdk').Core.Logger('name')`
168
+ *
169
+ * @returns {object} the error object, ready to be returned from the action main's function.
170
+ *
171
+ */
172
+ export function errorResponse(
173
+ statusCode: number,
174
+ message: string,
175
+ logger?: { info: (message: string) => unknown }
176
+ ): object {
177
+ logger?.info(`${statusCode}: ${message}`);
178
+
179
+ return {
180
+ error: {
181
+ statusCode,
182
+ body: {
183
+ error: message,
184
+ },
185
+ },
186
+ };
187
+ }
@@ -0,0 +1,20 @@
1
+ operations:
2
+ view:
3
+ - type: web
4
+ impl: index.html
5
+ actions: actions
6
+ web: web-src
7
+ runtimeManifest:
8
+ packages:
9
+ admin-ui-sdk:
10
+ license: Apache-2.0
11
+ actions:
12
+ registration:
13
+ function: actions/registration/index.ts
14
+ web: 'yes'
15
+ runtime: 'nodejs:22'
16
+ inputs:
17
+ LOG_LEVEL: debug
18
+ annotations:
19
+ require-adobe-auth: true
20
+ final: true
@@ -0,0 +1,2 @@
1
+ # Automatically generated by Aio CLI
2
+ config.json
@@ -0,0 +1,15 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
6
+ <meta name="theme-color" content="#333333">
7
+ <meta http-equiv="X-UA-Compatible" content="ie=edge">
8
+ <title><%= displayName %></title>
9
+ </head>
10
+ <body>
11
+ <noscript>You need to enable JavaScript to run this app.</noscript>
12
+ <div id="root"></div>
13
+ <script src="src/index.tsx" async type="module"></script>
14
+ </body>
15
+ </html>
@@ -0,0 +1,37 @@
1
+ import { Content, Heading, lightTheme, Provider, View } from '@adobe/react-spectrum';
2
+ import { Route, Routes, useNavigate } from 'react-router';
3
+
4
+ import { PageContextProvider } from '@/web/context/PageContextProvider.tsx';
5
+
6
+ function App() {
7
+ const navigate = useNavigate();
8
+
9
+ return (
10
+ <PageContextProvider title="<%= displayName %>">
11
+ <Provider
12
+ theme={lightTheme}
13
+ colorScheme={'light'}
14
+ router={{
15
+ navigate,
16
+ }}
17
+ >
18
+ <View padding="size-300">
19
+ <Routes>
20
+ <Route path="/" element={<LandingPage />} />
21
+ </Routes>
22
+ </View>
23
+ </Provider>
24
+ </PageContextProvider>
25
+ );
26
+ }
27
+
28
+ function LandingPage() {
29
+ return (
30
+ <>
31
+ <Heading level={1}><%= displayName %></Heading>
32
+ <Content><%= description %></Content>
33
+ </>
34
+ );
35
+ }
36
+
37
+ export default App;
@@ -0,0 +1,15 @@
1
+ import { register } from '@adobe/uix-guest';
2
+ import { useEffect, type ReactNode } from 'react';
3
+
4
+ import { EXTENSION_ID } from '@/web/constants/extension.ts';
5
+
6
+ export default function ExtensionRegistration({ children }: { children: ReactNode }) {
7
+ useEffect(() => {
8
+ register({
9
+ id: EXTENSION_ID,
10
+ methods: {},
11
+ }).catch(console.error);
12
+ }, []);
13
+
14
+ return <>{children}</>;
15
+ }
@@ -0,0 +1,4 @@
1
+ // config.json is generated at build time by the Adobe App Builder CLI.
2
+ // This declaration provides types for TypeScript when the file is absent (e.g. CI type checks).
3
+ declare const config: Record<string, string>;
4
+ export default config;
@@ -0,0 +1,65 @@
1
+ import { attach } from '@adobe/uix-guest';
2
+ import { createContext, useContext, useEffect, useState, type ReactNode } from 'react';
3
+
4
+ import { EXTENSION_ID } from '@/web/constants/extension.ts';
5
+
6
+ interface ImsContext {
7
+ token?: string | undefined;
8
+ org?: string | undefined;
9
+ }
10
+
11
+ interface AdobeRuntimeContextType {
12
+ loading: boolean;
13
+ error: Error | null;
14
+ ims: ImsContext;
15
+ }
16
+
17
+ const AdobeRuntimeContext = createContext<AdobeRuntimeContextType>({
18
+ loading: true,
19
+ error: null,
20
+ ims: {},
21
+ });
22
+
23
+ /**
24
+ * Retrieves IMS credentials from the Commerce Admin shared context via attach().
25
+ *
26
+ * register() is handled separately by the ExtensionRegistration component.
27
+ * This provider only calls attach() to access the shared context (imsToken, imsOrgId).
28
+ */
29
+ export const AdobeRuntimeContextProvider = ({ children }: { children: ReactNode }) => {
30
+ const [loading, setLoading] = useState(true);
31
+ const [error, setError] = useState<Error | null>(null);
32
+ const [ims, setIms] = useState<ImsContext>({});
33
+
34
+ useEffect(() => {
35
+ const init = async () => {
36
+ try {
37
+ const guestConnection = await attach({ id: EXTENSION_ID });
38
+ const token = guestConnection.sharedContext.get('imsToken') as string | undefined;
39
+ const org = guestConnection.sharedContext.get('imsOrgId') as string | undefined;
40
+
41
+ setIms({ token, org });
42
+ } catch (err) {
43
+ console.error('Failed to retrieve Admin UI SDK shared context', err);
44
+ setError(err instanceof Error ? err : new Error(String(err)));
45
+ } finally {
46
+ setLoading(false);
47
+ }
48
+ };
49
+
50
+ init();
51
+ }, []);
52
+
53
+ return <AdobeRuntimeContext value={{ loading, error, ims }}>{children}</AdobeRuntimeContext>;
54
+ };
55
+
56
+ /**
57
+ * React hook to access IMS credentials from the Commerce Admin context.
58
+ */
59
+ export const useAdobeRuntimeContext = () => {
60
+ const context = useContext(AdobeRuntimeContext);
61
+ if (!context) {
62
+ throw new Error('AdobeRuntimeContext not found');
63
+ }
64
+ return context;
65
+ };
@@ -0,0 +1,78 @@
1
+ import page from '@adobe/exc-app/page';
2
+ import topbar from '@adobe/exc-app/topbar';
3
+ import { createContext, useContext, useEffect, type ReactNode } from 'react';
4
+
5
+ import { useAdobeRuntimeContext } from '@/web/context/AdobeRuntimeContextProvider';
6
+
7
+ interface PageContextType {
8
+ title: string;
9
+ shortTitle: string;
10
+ }
11
+
12
+ export const PageContext = createContext<PageContextType>({
13
+ title: 'Adobe App Builder',
14
+ shortTitle: 'AAB',
15
+ });
16
+
17
+ /**
18
+ * Format a short title from the full title.
19
+ *
20
+ * If the title is less than 3 words, use the first 5 characters.
21
+ * If the title is 3 or more words, make an acronym from the first 5 words.
22
+ */
23
+ function formatShortTitle(title: string) {
24
+ return title.split(' ').length >= 3
25
+ ? title.split(' ').slice(0, 5).join('').toUpperCase()
26
+ : title.slice(0, 5);
27
+ }
28
+
29
+ /**
30
+ * Provides page properties to the application.
31
+ *
32
+ * If wrapped with AdobeRuntimeContextProvider, updates the topbar and page
33
+ * title once the Adobe runtime is ready.
34
+ */
35
+ export const PageContextProvider = ({
36
+ children,
37
+ title,
38
+ shortTitle,
39
+ }: {
40
+ children: ReactNode;
41
+ title?: string;
42
+ shortTitle?: string;
43
+ }) => {
44
+ const formattedTitle = title?.trim() || 'Adobe App Builder';
45
+ const formattedShortTitle = shortTitle?.trim() || formatShortTitle(formattedTitle);
46
+
47
+ const { loading, error } = useAdobeRuntimeContext();
48
+ useEffect(() => {
49
+ document.title = formattedTitle;
50
+
51
+ if (loading || error) {
52
+ return;
53
+ }
54
+
55
+ // topbar/page APIs are only available when running inside the Experience Cloud Shell.
56
+ // In the Commerce Admin iframe they are not available, so we catch and ignore.
57
+ try {
58
+ topbar.solution = {
59
+ icon: 'AdobeExperienceCloud',
60
+ title: formattedTitle,
61
+ shortTitle: formattedShortTitle,
62
+ };
63
+ page.title = formattedTitle;
64
+ } catch {
65
+ // Not running in Experience Cloud Shell — topbar APIs unavailable.
66
+ }
67
+ }, [loading, error, formattedTitle, formattedShortTitle]);
68
+
69
+ return (
70
+ <PageContext value={{ title: formattedTitle, shortTitle: formattedShortTitle }}>
71
+ {children}
72
+ </PageContext>
73
+ );
74
+ };
75
+
76
+ export const usePageContext = () => {
77
+ return useContext(PageContext);
78
+ };
@@ -0,0 +1,41 @@
1
+ import { useEffect, useRef } from 'react';
2
+
3
+ import {
4
+ useLazyAppBuilderAction,
5
+ type UseLazyAppBuilderActionOptions,
6
+ } from '@/web/hooks/useLazyAppBuilderAction';
7
+
8
+ /**
9
+ * React hook that invokes an App Builder action immediately on mount.
10
+ *
11
+ * Use this when you need to fetch data as soon as the component renders.
12
+ * For manual invocation, use `useLazyAppBuilderAction` directly.
13
+ *
14
+ * @example
15
+ * ```tsx
16
+ * const { response, loading } = useAppBuilderAction<ConfigData>({
17
+ * name: 'my-package/get-config',
18
+ * method: 'GET',
19
+ * });
20
+ *
21
+ * if (loading) return <Spinner />;
22
+ * return <ConfigForm initialValues={response} />;
23
+ * ```
24
+ */
25
+ export function useAppBuilderAction<T = unknown>(options: UseLazyAppBuilderActionOptions) {
26
+ const hasInvokedRef = useRef(false);
27
+
28
+ const { response, loading: isInvoking, error, invoke } = useLazyAppBuilderAction<T>(options);
29
+
30
+ useEffect(() => {
31
+ if (hasInvokedRef.current) {
32
+ return;
33
+ }
34
+ hasInvokedRef.current = true;
35
+ invoke();
36
+ }, [invoke]);
37
+
38
+ const loading = isInvoking || (!response && !error);
39
+
40
+ return { response, loading, error };
41
+ }
@@ -0,0 +1,116 @@
1
+ import { useCallback, useState } from 'react';
2
+
3
+ import ActionRegistry from '@/web/config.json';
4
+ import { type ActionName } from '@/web/types/ActionName.ts';
5
+
6
+ /**
7
+ * Payload options that can be passed when invoking an action.
8
+ * These values merge with and override the options set during hook initialization.
9
+ */
10
+ export interface ActionPayload {
11
+ /** URL search parameters to append to the action URL */
12
+ searchParams?: Record<string, string>;
13
+
14
+ /** Request body for non-GET/HEAD requests */
15
+ body?: object;
16
+
17
+ /** Custom headers to include in the request */
18
+ headers?: Record<string, string>;
19
+ }
20
+
21
+ /**
22
+ * Configuration options for the useLazyAppBuilderAction / useAppBuilderAction hooks.
23
+ */
24
+ export type UseLazyAppBuilderActionOptions = ActionPayload & {
25
+ /** The name of the action to invoke. Must be a key from the action registry in `config.json`. */
26
+ name: ActionName;
27
+
28
+ /**
29
+ * HTTP method to use for the request.
30
+ * @default 'GET'
31
+ */
32
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
33
+
34
+ /** Adobe IMS authentication credentials */
35
+ ims?: {
36
+ token?: string | undefined;
37
+ org?: string | undefined;
38
+ };
39
+ };
40
+
41
+ /**
42
+ * React hook for invoking App Builder actions with built-in state management.
43
+ *
44
+ * Returns `{ invoke, response, loading, error }`. Call `invoke()` to fire the
45
+ * request manually. For automatic invocation on mount, use `useAppBuilderAction`.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * const { invoke, response, loading, error } = useLazyAppBuilderAction<MyData>({
50
+ * name: 'my-package/my-action',
51
+ * method: 'POST',
52
+ * ims: { token, org },
53
+ * });
54
+ *
55
+ * await invoke({ body: { id: '123' } });
56
+ * ```
57
+ */
58
+ export function useLazyAppBuilderAction<T = unknown>(options: UseLazyAppBuilderActionOptions) {
59
+ const [response, setResponse] = useState<T | null>(null);
60
+ const [loading, setLoading] = useState<boolean>(false);
61
+ const [error, setError] = useState<string | null>(null);
62
+
63
+ const invoke = useCallback(
64
+ async (invokeOptions?: ActionPayload) => {
65
+ setLoading(true);
66
+ setError(null);
67
+ setResponse(null);
68
+
69
+ try {
70
+ const actionUrl = ActionRegistry[options.name];
71
+ if (!actionUrl) {
72
+ throw new Error(`Action "${options.name}" not found in config.json`);
73
+ }
74
+
75
+ const headers = new Headers({
76
+ 'content-type': 'application/json',
77
+ ...(options.ims?.org && { 'x-gw-ims-org-id': options.ims.org }),
78
+ ...(options.ims?.token && { Authorization: `Bearer ${options.ims.token}` }),
79
+ ...options.headers,
80
+ ...invokeOptions?.headers,
81
+ });
82
+
83
+ const url = new URL(actionUrl);
84
+ const allSearchParams = { ...options.searchParams, ...invokeOptions?.searchParams };
85
+ for (const [k, v] of Object.entries(allSearchParams)) {
86
+ if (v != null) url.searchParams.set(k, v);
87
+ }
88
+
89
+ const fetchOptions: RequestInit = {
90
+ method: options.method || 'GET',
91
+ headers,
92
+ };
93
+
94
+ if (fetchOptions.method !== 'GET' && fetchOptions.method !== 'HEAD') {
95
+ const body = invokeOptions?.body || options.body;
96
+ if (body) fetchOptions.body = JSON.stringify(body);
97
+ }
98
+
99
+ const response = await fetch(url.toString(), fetchOptions);
100
+ if (!response.ok) {
101
+ throw new Error(`Error: ${response.status} ${response.statusText}`);
102
+ }
103
+
104
+ const data = await response.json();
105
+ setResponse(data);
106
+ } catch (err) {
107
+ setError(err instanceof Error ? err.message : String(err));
108
+ } finally {
109
+ setLoading(false);
110
+ }
111
+ },
112
+ [options]
113
+ );
114
+
115
+ return { response, loading, error, invoke };
116
+ }
@@ -0,0 +1,20 @@
1
+ import { createRoot } from 'react-dom/client';
2
+ import { HashRouter } from 'react-router';
3
+
4
+ import App from '@/web/components/App.tsx';
5
+ import ExtensionRegistration from '@/web/components/ExtensionRegistration.tsx';
6
+ import { AdobeRuntimeContextProvider } from '@/web/context/AdobeRuntimeContextProvider.tsx';
7
+
8
+ import './index.css';
9
+
10
+ const root = createRoot(document.getElementById('root')!);
11
+
12
+ root.render(
13
+ <HashRouter>
14
+ <ExtensionRegistration>
15
+ <AdobeRuntimeContextProvider>
16
+ <App />
17
+ </AdobeRuntimeContextProvider>
18
+ </ExtensionRegistration>
19
+ </HashRouter>
20
+ );