@aws-amplify/amplify-util-uibuilder 1.8.5-rds.1 → 1.9.0-cdk228withdata.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 (37) hide show
  1. package/CHANGELOG.md +3 -8
  2. package/lib/clients/amplify-studio-client.d.ts +4 -1
  3. package/lib/clients/amplify-studio-client.d.ts.map +1 -1
  4. package/lib/clients/amplify-studio-client.js +43 -3
  5. package/lib/clients/amplify-studio-client.js.map +1 -1
  6. package/lib/commands/generateComponents.d.ts.map +1 -1
  7. package/lib/commands/generateComponents.js +32 -33
  8. package/lib/commands/generateComponents.js.map +1 -1
  9. package/lib/commands/utils/codegenResources.d.ts +1 -15
  10. package/lib/commands/utils/codegenResources.d.ts.map +1 -1
  11. package/lib/commands/utils/codegenResources.js +1 -123
  12. package/lib/commands/utils/codegenResources.js.map +1 -1
  13. package/lib/commands/utils/detachedFormCleanup.d.ts +3 -4
  14. package/lib/commands/utils/detachedFormCleanup.d.ts.map +1 -1
  15. package/lib/commands/utils/detachedFormCleanup.js.map +1 -1
  16. package/lib/commands/utils/storageManager.d.ts +3 -4
  17. package/lib/commands/utils/storageManager.d.ts.map +1 -1
  18. package/lib/commands/utils/storageManager.js +1 -5
  19. package/lib/commands/utils/storageManager.js.map +1 -1
  20. package/lib/commands/utils/syncAmplifyUiBuilderComponents.d.ts +8 -16
  21. package/lib/commands/utils/syncAmplifyUiBuilderComponents.d.ts.map +1 -1
  22. package/lib/commands/utils/syncAmplifyUiBuilderComponents.js +186 -74
  23. package/lib/commands/utils/syncAmplifyUiBuilderComponents.js.map +1 -1
  24. package/package.json +7 -6
  25. package/src/__tests__/__snapshots__/syncAmplifyUiComponents.test.ts.snap +46 -0
  26. package/src/__tests__/detachedFormCleanup.test.ts +7 -5
  27. package/src/__tests__/generateComponents.test.ts +101 -28
  28. package/src/__tests__/storageManager.test.ts +7 -4
  29. package/src/__tests__/syncAmplifyUiComponents.test.ts +98 -96
  30. package/src/clients/amplify-studio-client.ts +44 -4
  31. package/src/commands/generateComponents.ts +43 -55
  32. package/src/commands/utils/codegenResources.ts +1 -226
  33. package/src/commands/utils/detachedFormCleanup.ts +3 -4
  34. package/src/commands/utils/storageManager.ts +3 -7
  35. package/src/commands/utils/syncAmplifyUiBuilderComponents.ts +210 -123
  36. package/tsconfig.tsbuildinfo +1 -1
  37. package/src/__tests__/createUiBuilderComponent.test.ts +0 -90
@@ -1,8 +1,11 @@
1
- import { StudioSchema } from '@aws-amplify/codegen-ui';
2
1
  import { hasStorageField } from '../commands/utils';
2
+ import { Form } from 'aws-sdk/clients/amplifyuibuilder';
3
3
 
4
- const formWithNoCustomization: StudioSchema = {
4
+ const formWithNoCustomization: Form = {
5
5
  id: 'f-123456',
6
+ appId: '123',
7
+ environmentName: 'staging',
8
+ schemaVersion: '1.0',
6
9
  name: 'BlogCreateForm',
7
10
  formActionType: 'create',
8
11
  dataType: {
@@ -15,7 +18,7 @@ const formWithNoCustomization: StudioSchema = {
15
18
  cta: {},
16
19
  };
17
20
 
18
- const formWithNoStorageField: StudioSchema = {
21
+ const formWithNoStorageField: Form = {
19
22
  ...formWithNoCustomization,
20
23
  dataType: { dataSourceType: 'Custom', dataTypeName: 'Blog' },
21
24
  fields: {
@@ -23,7 +26,7 @@ const formWithNoStorageField: StudioSchema = {
23
26
  },
24
27
  };
25
28
 
26
- const formWithStorageField: StudioSchema = {
29
+ const formWithStorageField: Form = {
27
30
  ...formWithNoCustomization,
28
31
  dataType: { dataSourceType: 'Custom', dataTypeName: 'Blog' },
29
32
  fields: {
@@ -1,18 +1,19 @@
1
- /* eslint-disable spellcheck/spell-checker */
2
- import { AmplifyCategories, AmplifySupportedService, stateManager } from '@aws-amplify/amplify-cli-core'; // eslint-disable-line import/no-extraneous-dependencies
3
- import aws from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies
1
+ import { AmplifyCategories, AmplifySupportedService, stateManager } from '@aws-amplify/amplify-cli-core';
2
+ import aws from 'aws-sdk';
4
3
  import {
5
- generateUiBuilderComponents,
6
- generateUiBuilderThemes,
7
4
  getEnvName,
8
5
  getAppId,
9
6
  resolveAppId,
10
- generateUiBuilderForms,
7
+ mapGenericDataSchemaToCodegen,
8
+ waitForSucceededJob,
9
+ extractUIComponents,
10
+ fetchWithRetries,
11
11
  } from '../commands/utils';
12
12
  import { AmplifyStudioClient } from '../clients';
13
- import * as createUiBuilderComponentDependency from '../commands/utils/codegenResources';
14
- import { exampleSchema } from './utils';
15
13
  import { isDataStoreEnabled } from '@aws-amplify/amplify-category-api';
14
+ import type { GenericDataSchema } from '@aws-amplify/codegen-ui';
15
+ import fetch, { Response } from 'node-fetch';
16
+ import { existsSync, writeFileSync } from 'fs';
16
17
 
17
18
  jest.mock('@aws-amplify/amplify-cli-core', () => ({
18
19
  ...jest.requireActual('@aws-amplify/amplify-cli-core'),
@@ -24,21 +25,57 @@ jest.mock('@aws-amplify/amplify-cli-core', () => ({
24
25
  jest.mock('../commands/utils/featureFlags', () => ({
25
26
  getTransformerVersion: jest.fn().mockReturnValue(2),
26
27
  }));
27
-
28
28
  jest.mock('@aws-amplify/amplify-category-api', () => ({
29
29
  ...jest.requireActual('@aws-amplify/amplify-category-api'),
30
30
  isDataStoreEnabled: jest.fn(),
31
31
  }));
32
+ jest.mock('fs');
33
+ jest.mock('node-fetch');
32
34
 
33
35
  const awsMock = aws as any;
34
36
  const stateManagerMock = stateManager as any;
35
- const createUiBuilderComponentDependencyMock = createUiBuilderComponentDependency as any;
36
37
  const isDataStoreEnabledMocked = jest.mocked(isDataStoreEnabled);
38
+ const mockWriteFileSync = jest.mocked(writeFileSync);
39
+ const mockExistsSync = jest.mocked(existsSync);
40
+ const mockNodeFetch = jest.mocked(fetch);
37
41
 
38
42
  describe('should sync amplify ui builder components', () => {
39
43
  let context: any;
44
+ const env = process.env;
45
+
46
+ const mockGenericDataSchema: GenericDataSchema = {
47
+ dataSourceType: 'DataStore',
48
+ models: {
49
+ Blog: {
50
+ fields: {
51
+ id: { dataType: 'ID', required: true, readOnly: false, isArray: false },
52
+ },
53
+ primaryKeys: ['id'],
54
+ },
55
+ },
56
+ enums: {
57
+ Status: { values: ['ENABLED', 'DISABLED', 'HIDDEN'] },
58
+ },
59
+ nonModels: {
60
+ Metadata: {
61
+ fields: {
62
+ name: {
63
+ dataType: 'String',
64
+ required: false,
65
+ readOnly: false,
66
+ isArray: false,
67
+ },
68
+ },
69
+ },
70
+ },
71
+ };
72
+
40
73
  beforeEach(() => {
74
+ mockNodeFetch.mockReset();
75
+ process.env = { ...env };
76
+
41
77
  isDataStoreEnabledMocked.mockResolvedValue(true);
78
+ mockExistsSync.mockReturnValue(false);
42
79
  context = {
43
80
  exeInfo: {
44
81
  projectConfig: {
@@ -156,29 +193,10 @@ describe('should sync amplify ui builder components', () => {
156
193
  })),
157
194
  })),
158
195
  }));
159
-
160
- createUiBuilderComponentDependencyMock.createUiBuilderComponent = jest.fn();
161
- createUiBuilderComponentDependencyMock.createUiBuilderTheme = jest.fn();
162
- createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn();
163
- createUiBuilderComponentDependencyMock.createUiBuilderView = jest.fn();
164
- });
165
-
166
- it('pulls components from aws-sdk and passes them to createUiBuilderComponent', () => {
167
- generateUiBuilderComponents(context, []);
168
- });
169
-
170
- it('does not throw an error when createUiBuilderComponent fails', () => {
171
- createUiBuilderComponentDependencyMock.createUiBuilderComponent = jest.fn(() => {
172
- throw new Error('ahhh!'); // eslint-disable-line spellcheck/spell-checker
173
- });
174
- expect(async () => generateUiBuilderComponents(context, [])).not.toThrow();
175
196
  });
176
197
 
177
- it('does not throw an error when createUiBuilderThemes fails', () => {
178
- createUiBuilderComponentDependencyMock.createUiBuilderComponent = jest.fn(() => {
179
- throw new Error('ahhh!'); // eslint-disable-line spellcheck/spell-checker
180
- });
181
- expect(async () => generateUiBuilderThemes(context, [])).not.toThrow();
198
+ afterEach(() => {
199
+ process.env = env;
182
200
  });
183
201
 
184
202
  it('can getAmplifyUIBuilderService', async () => {
@@ -199,110 +217,94 @@ describe('should sync amplify ui builder components', () => {
199
217
  }),
200
218
  );
201
219
  });
220
+
202
221
  it('can list components', async () => {
203
222
  const client = await AmplifyStudioClient.setClientInfo(context);
204
223
  const components = await client.listComponents();
205
224
  expect(components.entities).toHaveLength(1);
206
225
  });
226
+
207
227
  it('can list themes', async () => {
208
228
  const client = await AmplifyStudioClient.setClientInfo(context);
209
229
  const themes = await client.listThemes();
210
230
  expect(themes.entities).toHaveLength(1);
211
231
  });
232
+
212
233
  it('can list forms', async () => {
213
234
  const client = await AmplifyStudioClient.setClientInfo(context);
214
235
  const forms = await client.listForms();
215
236
  expect(forms.entities).toHaveLength(1);
216
237
  });
238
+
217
239
  it('can getAppId', async () => {
218
240
  const appId = getAppId(context);
219
241
  expect(appId).toBe('testAppId');
220
242
  });
243
+
221
244
  it('can getEnvName', () => {
222
245
  const envName = getEnvName(context);
223
246
  expect(envName).toBe('testEnvName');
224
247
  });
248
+
225
249
  it('can resolveAppId', async () => {
226
250
  const appId = resolveAppId();
227
251
  expect(appId).toBe('testAppId');
228
252
  });
253
+
229
254
  it('can throw on getAppId', async () => {
230
255
  context.input.options.appId = null;
231
256
  context.amplify.invokePluginMethod = () => null;
232
257
  stateManagerMock.getMeta = () => ({});
233
258
  expect(() => getAppId(context)).toThrowError();
234
259
  });
235
- it('can generate ui builder components', async () => {
236
- createUiBuilderComponentDependencyMock.createUiBuilderComponent = jest.fn().mockImplementation(() => ({}));
237
- const components = generateUiBuilderComponents(context, [{}, {}]);
238
- expect(components.every((component) => component.resultType === 'SUCCESS')).toBeTruthy();
239
- });
240
- it('can handle failed generation generate ui builder components', async () => {
241
- createUiBuilderComponentDependencyMock.createUiBuilderComponent = jest.fn().mockImplementation(() => {
242
- throw new Error('ahh!');
243
- });
244
- const components = generateUiBuilderComponents(context, [{}, {}]);
245
- expect(components.every((component) => component.resultType === 'FAILURE')).toBeTruthy();
246
- });
247
- it('can generate ui builder themes', async () => {
248
- createUiBuilderComponentDependencyMock.createUiBuilderTheme = jest.fn().mockImplementation(() => ({}));
249
- const themes = generateUiBuilderThemes(context, [{}, {}]);
250
- expect(themes.every((theme) => theme.resultType === 'SUCCESS')).toBeTruthy();
251
- });
252
- it('can handle failed generation generate ui builder themes', async () => {
253
- createUiBuilderComponentDependencyMock.createUiBuilderTheme = jest.fn().mockImplementation(() => {
254
- throw new Error('ahh!');
255
- });
256
- const themes = generateUiBuilderThemes(context, [{}, {}]);
257
- expect(themes.every((theme) => theme.resultType === 'FAILURE')).toBeTruthy();
258
- });
259
- it('can generate ui builder default theme when no themes are passed', async () => {
260
- createUiBuilderComponentDependencyMock.createUiBuilderTheme = jest.fn().mockImplementation(() => ({}));
261
- const themes = generateUiBuilderThemes(context, []);
262
- expect(themes.every((theme) => theme.resultType === 'SUCCESS')).toBeTruthy();
263
- });
264
- it('can handle failed generation generate ui builder default theme when no themes are passed', async () => {
265
- createUiBuilderComponentDependencyMock.createUiBuilderTheme = jest.fn().mockImplementation(() => {
266
- throw new Error('ahh!');
267
- });
268
- const themes = generateUiBuilderThemes(context, []);
269
- expect(themes.every((theme) => theme.resultType === 'FAILURE')).toBeTruthy();
260
+
261
+ it('can map generic data schema to start codegen job request', () => {
262
+ expect(mapGenericDataSchemaToCodegen(mockGenericDataSchema)).toMatchSnapshot();
270
263
  });
271
- it('can generate ui builder forms', async () => {
272
- createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(() => ({}));
273
- const forms = generateUiBuilderForms(context, [{}, {}]);
274
- expect(forms.every((form) => form.resultType === 'SUCCESS')).toBeTruthy();
264
+
265
+ it('can wait for a succeeded job', async () => {
266
+ const succeededJob = { status: 'succeeded' };
267
+ const getJob = jest.fn().mockResolvedValueOnce({ status: 'in_progress' }).mockResolvedValueOnce(succeededJob);
268
+ const job = await waitForSucceededJob(getJob, { pollInterval: 1 });
269
+ expect(job).toEqual(succeededJob);
275
270
  });
276
- it('can handle failed generation generate ui builder forms', async () => {
277
- createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(() => {
278
- throw new Error('ahh!');
279
- });
280
- const forms = generateUiBuilderForms(context, [{}, {}]);
281
- expect(forms.every((form) => form.resultType === 'FAILURE')).toBeTruthy();
271
+
272
+ it('can throw if job failed', async () => {
273
+ const statusMessage = 'failed status';
274
+ const getJob = jest.fn().mockResolvedValueOnce({ status: 'in_progress' }).mockResolvedValueOnce({ status: 'failed', statusMessage });
275
+ await expect(waitForSucceededJob(getJob, { pollInterval: 2 })).rejects.toThrow(statusMessage);
282
276
  });
283
- it('can generate uibuilder forms from data schema if autogenerate true', async () => {
284
- createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(() => ({}));
285
- const forms = generateUiBuilderForms(context, [], exampleSchema, true);
286
- expect(forms.every((form) => form.resultType === 'SUCCESS')).toBeTruthy();
287
- // create & update form for author model
288
- expect(forms.length).toEqual(2);
277
+
278
+ it('can throw after timeout when waiting for a succeeded job', async () => {
279
+ process.env.UI_BUILDER_CODEGENJOB_TIMEOUT = '1';
280
+ const succeededJob = { status: 'succeeded' };
281
+ const getJob = jest.fn().mockResolvedValueOnce({ status: 'in_progress' }).mockResolvedValueOnce(succeededJob);
282
+ await expect(waitForSucceededJob(getJob, { pollInterval: 2 })).rejects.toThrow('Failed to return codegen job');
289
283
  });
290
284
 
291
- it('should not autogenerate forms for join tables or unsupported models', async () => {
292
- expect(Object.keys(exampleSchema.models)).toStrictEqual(['Author', 'JoinTable', 'EmptyModel']);
293
- createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(
294
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
295
- (_ctx, schema, _dataSchema) => ({ name: schema.dataType.dataTypeName }),
296
- );
297
- const forms = generateUiBuilderForms(context, [], exampleSchema, true);
298
- expect(forms.every((form) => form.resultType === 'SUCCESS')).toBeTruthy();
299
- // only create & update form for author model
300
- expect(forms.map((f) => (f.schema as any).name)).toStrictEqual(['Author', 'Author']);
285
+ it('can extract ui components from archive', async () => {
286
+ const mockedFetchResponse = {
287
+ ok: true,
288
+ json: jest.fn().mockResolvedValue({
289
+ Output: [
290
+ {
291
+ downloadUrl: 'https://s3.com',
292
+ fileName: 'MyComponent.jsx',
293
+ schemaName: 'MyComponent',
294
+ },
295
+ ],
296
+ }),
297
+ text: jest.fn().mockResolvedValue('source code'),
298
+ } as unknown as Response;
299
+ mockNodeFetch.mockResolvedValue(mockedFetchResponse);
300
+ mockExistsSync.mockReturnValue(true);
301
+ await extractUIComponents('https:://example.aws', 'tmp/ui-components');
302
+ expect(mockWriteFileSync).toHaveBeenCalledWith('tmp/ui-components/MyComponent.jsx', 'source code');
301
303
  });
302
304
 
303
- it('does not generate uibuilder forms from data schema if autogenerate false', async () => {
304
- createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(() => ({}));
305
- const forms = generateUiBuilderForms(context, [], exampleSchema, false);
306
- expect(forms.length).toEqual(0);
305
+ it('fetchWithRetries retries 3 times', async () => {
306
+ mockNodeFetch.mockRejectedValue(new Error('Server dropped the connection'));
307
+ await expect(fetchWithRetries('https://amazon.com')).rejects.toThrowError('Fetch reached max number of retries without succeeding');
308
+ expect(mockNodeFetch).toHaveBeenCalledTimes(3);
307
309
  });
308
310
  });
@@ -1,6 +1,6 @@
1
1
  import { $TSContext, CloudformationProviderFacade } from '@aws-amplify/amplify-cli-core';
2
2
  import type { ServiceConfigurationOptions } from 'aws-sdk/lib/service';
3
- import type { CreateComponentData, Component, Theme, Form } from 'aws-sdk/clients/amplifyuibuilder';
3
+ import type { CreateComponentData, Component, Theme, Form, StartCodegenJobData } from 'aws-sdk/clients/amplifyuibuilder';
4
4
  import { AmplifyUIBuilder, AmplifyBackend } from 'aws-sdk';
5
5
  import { printer } from '@aws-amplify/amplify-prompts';
6
6
  import { getAppId, getEnvName } from '../commands/utils/environmentHelpers';
@@ -171,7 +171,6 @@ export default class AmplifyStudioClient {
171
171
  uiBuilderComponents.push(...response.entities);
172
172
  nextToken = response.nextToken;
173
173
  } while (nextToken);
174
- printer.debug(JSON.stringify(uiBuilderComponents, null, 2));
175
174
  return { entities: uiBuilderComponents };
176
175
  } catch (err) {
177
176
  throw new Error(`Failed to list components: ${err.message}`);
@@ -199,7 +198,6 @@ export default class AmplifyStudioClient {
199
198
  uiBuilderThemes.push(...response.entities);
200
199
  nextToken = response.nextToken;
201
200
  } while (nextToken);
202
- printer.debug(JSON.stringify(uiBuilderThemes, null, 2));
203
201
  return { entities: uiBuilderThemes };
204
202
  } catch (err) {
205
203
  throw new Error(`Failed to list themes: ${err.message}`);
@@ -223,7 +221,6 @@ export default class AmplifyStudioClient {
223
221
  uibuilderForms.push(...response.entities);
224
222
  nextToken = response.nextToken;
225
223
  } while (nextToken);
226
- printer.debug(JSON.stringify(uibuilderForms, null, 2));
227
224
  return { entities: uibuilderForms };
228
225
  } catch (err) {
229
226
  throw new Error(`Failed to list forms: ${err.message}`);
@@ -274,4 +271,47 @@ export default class AmplifyStudioClient {
274
271
  throw new Error(`Models not found in AmplifyBackend:GetBackendAPIModels response: ${err.message}`);
275
272
  }
276
273
  };
274
+
275
+ startCodegenJob = async (codegenJobToCreate: StartCodegenJobData, appId?: string, envName?: string) => {
276
+ const environmentName = envName || this.#envName;
277
+ const resolvedAppId = appId || this.#appId;
278
+ try {
279
+ const response = await this.#amplifyUiBuilder
280
+ .startCodegenJob({
281
+ appId: resolvedAppId,
282
+ environmentName,
283
+ codegenJobToCreate,
284
+ })
285
+ .promise();
286
+ if (!response.entity || !response.entity.id) {
287
+ throw new Error('Error starting codegen job');
288
+ }
289
+ return response.entity.id;
290
+ } catch (err) {
291
+ printer.debug('Failed to start job');
292
+ printer.debug(err.stack);
293
+ throw err;
294
+ }
295
+ };
296
+
297
+ getCodegenJob = async (jobId: string, appId?: string, envName?: string) => {
298
+ const environmentName = envName || this.#envName;
299
+ const resolvedAppId = appId || this.#appId;
300
+ try {
301
+ const { job } = await this.#amplifyUiBuilder
302
+ .getCodegenJob({
303
+ id: jobId,
304
+ appId: resolvedAppId,
305
+ environmentName,
306
+ })
307
+ .promise();
308
+ if (!job) {
309
+ throw new Error('Error getting codegen job');
310
+ }
311
+ return job;
312
+ } catch (err) {
313
+ printer.debug(err.toString());
314
+ throw err;
315
+ }
316
+ };
277
317
  }
@@ -1,5 +1,4 @@
1
1
  /* eslint-disable spellcheck/spell-checker */
2
- import { StudioSchema } from '@aws-amplify/codegen-ui';
3
2
  import ora from 'ora';
4
3
  import { printer } from '@aws-amplify/amplify-prompts';
5
4
  import { $TSContext } from '@aws-amplify/amplify-cli-core';
@@ -7,18 +6,16 @@ import { AmplifyStudioClient } from '../clients';
7
6
  import {
8
7
  notifyMissingPackages,
9
8
  shouldRenderComponents,
10
- generateUiBuilderComponents,
11
- generateUiBuilderThemes,
12
9
  getAmplifyDataSchema,
13
- generateAmplifyUiBuilderIndexFile,
14
- generateUiBuilderForms,
15
- generateAmplifyUiBuilderUtilFile,
16
- isStudioForm,
17
10
  isFormDetachedFromModel,
18
11
  deleteDetachedForms,
19
12
  hasStorageField,
20
- isFormSchema,
13
+ mapGenericDataSchemaToCodegen,
14
+ waitForSucceededJob,
15
+ getUiBuilderComponentsPath,
16
+ extractUIComponents,
21
17
  } from './utils';
18
+ import { AmplifyUIBuilder } from 'aws-sdk';
22
19
 
23
20
  /**
24
21
  * Pulls ui components from Studio backend and generates the code in the user's file system
@@ -46,61 +43,51 @@ export const run = async (context: $TSContext, eventType: 'PostPush' | 'PostPull
46
43
  }
47
44
  spinner.start('Generating UI components...');
48
45
 
49
- const generatedResults = {
50
- component: generateUiBuilderComponents(context, componentSchemas.entities, dataSchema),
51
- theme: generateUiBuilderThemes(context, themeSchemas.entities),
52
- form: generateUiBuilderForms(
53
- context,
54
- formSchemas.entities,
55
- dataSchema,
56
- studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported && studioClient.isDataStoreEnabled,
57
- studioClient.metadata.formFeatureFlags,
58
- ),
46
+ const genericDataSchema = dataSchema ? mapGenericDataSchemaToCodegen(dataSchema) : undefined;
47
+
48
+ const job: AmplifyUIBuilder.StartCodegenJobData = {
49
+ renderConfig: {
50
+ react: {
51
+ module: 'es2020',
52
+ target: 'es2020',
53
+ script: 'jsx',
54
+ renderTypeDeclarations: true,
55
+ },
56
+ },
57
+ autoGenerateForms: studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported,
58
+ features: studioClient.metadata.formFeatureFlags,
59
59
  };
60
+ // SDK will throw if this is undefined
61
+ // even though it's optional
62
+ if (genericDataSchema) {
63
+ job.genericDataSchema = genericDataSchema;
64
+ }
65
+ const jobId = await studioClient.startCodegenJob(job);
66
+ const finishedJob = await waitForSucceededJob(() => studioClient.getCodegenJob(jobId), { pollInterval: 2000 });
67
+ if (!finishedJob.asset?.downloadUrl) {
68
+ throw new Error('No manifest file in codegen job');
69
+ }
70
+ const uiBuilderComponentsPath = getUiBuilderComponentsPath(context);
71
+ await extractUIComponents(finishedJob.asset.downloadUrl, uiBuilderComponentsPath);
60
72
 
61
- const successfulSchemas: StudioSchema[] = [];
62
73
  const detachedForms: { id: string; name: string }[] = [];
63
- let hasSuccessfulForm = false;
64
74
  const failedResponseNames: string[] = [];
65
75
  const modelNames = dataSchema?.models ? new Set(Object.keys(dataSchema.models)) : new Set<string>();
66
- let hasStorageManagerField = false;
67
-
68
- Object.entries(generatedResults).forEach(([key, results]) => {
69
- results.forEach((result) => {
70
- if (result.resultType === 'SUCCESS') {
71
- successfulSchemas.push(result.schema);
72
- if (key === 'form') {
73
- hasSuccessfulForm = true;
76
+ const hasStorageManagerField = formSchemas.entities.some((formSchema) => hasStorageField(formSchema));
74
77
 
75
- if (!hasStorageManagerField && isFormSchema(result.schema) && hasStorageField(result.schema)) {
76
- hasStorageManagerField = true;
77
- }
78
- }
79
- } else {
80
- const failedSchema = result.schema;
81
- /**
82
- * A form resource may fail to generate because it's DataStore model type
83
- * no longer exists.
84
- */
85
- if (
86
- isStudioForm(failedSchema) &&
87
- failedSchema.id &&
88
- dataSchema &&
89
- eventType === 'PostPush' &&
90
- isFormDetachedFromModel(failedSchema, modelNames)
91
- ) {
92
- // Don't need to add form to failedResponseNames if it is going to be deleted
93
- detachedForms.push({ id: failedSchema.id, name: failedSchema.name });
94
- return;
95
- }
96
- failedResponseNames.push(result.schemaName);
97
- }
78
+ if (dataSchema && eventType === 'PostPush') {
79
+ formSchemas.entities.forEach((formSchema) => {
80
+ isFormDetachedFromModel(formSchema, modelNames) && detachedForms.push({ id: formSchema.id, name: formSchema.name });
98
81
  });
99
- });
100
-
101
- generateAmplifyUiBuilderIndexFile(context, successfulSchemas);
82
+ }
102
83
 
103
- generateAmplifyUiBuilderUtilFile(context, { hasForms: hasSuccessfulForm, hasViews: false });
84
+ if (finishedJob.statusMessage) {
85
+ const errorStack = JSON.parse(finishedJob.statusMessage)?.codegenErrors as {
86
+ schemaName: string;
87
+ error: string;
88
+ }[];
89
+ errorStack.forEach((e) => failedResponseNames.push(e.schemaName));
90
+ }
104
91
 
105
92
  if (failedResponseNames.length > 0) {
106
93
  spinner.fail(`Failed to sync the following components: ${failedResponseNames.join(', ')}`);
@@ -112,6 +99,7 @@ export const run = async (context: $TSContext, eventType: 'PostPush' | 'PostPull
112
99
  ...componentSchemas.entities.filter((component) => !component.schemaVersion).map((component) => component.name),
113
100
  ...formSchemas.entities.filter((form) => !form.schemaVersion).map((form) => form.name),
114
101
  ];
102
+
115
103
  if (invalidComponentNames.length) {
116
104
  printer.warn(
117
105
  `The components ${invalidComponentNames.join(