@superblocksteam/sdk 1.14.2 → 2.0.3-next.100

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 (152) hide show
  1. package/.mocharc.json +7 -0
  2. package/.prettierrc +18 -0
  3. package/dist/cli-replacement/dev.d.mts +19 -0
  4. package/dist/cli-replacement/dev.d.mts.map +1 -0
  5. package/dist/cli-replacement/dev.mjs +129 -0
  6. package/dist/cli-replacement/dev.mjs.map +1 -0
  7. package/dist/cli-replacement/init.d.ts +14 -0
  8. package/dist/cli-replacement/init.d.ts.map +1 -0
  9. package/dist/cli-replacement/init.js +26 -0
  10. package/dist/cli-replacement/init.js.map +1 -0
  11. package/dist/client.d.ts +31 -17
  12. package/dist/client.d.ts.map +1 -0
  13. package/dist/client.js +137 -155
  14. package/dist/client.js.map +1 -0
  15. package/dist/dbfs/client.d.ts +6 -0
  16. package/dist/dbfs/client.d.ts.map +1 -0
  17. package/dist/dbfs/client.js +117 -0
  18. package/dist/dbfs/client.js.map +1 -0
  19. package/dist/dbfs/local.d.ts +15 -0
  20. package/dist/dbfs/local.d.ts.map +1 -0
  21. package/dist/dbfs/local.js +126 -0
  22. package/dist/dbfs/local.js.map +1 -0
  23. package/dist/dev-utils/custom-build.d.mts +4 -0
  24. package/dist/dev-utils/custom-build.d.mts.map +1 -0
  25. package/dist/dev-utils/custom-build.mjs +99 -0
  26. package/dist/dev-utils/custom-build.mjs.map +1 -0
  27. package/dist/dev-utils/custom-config.d.mts +2 -0
  28. package/dist/dev-utils/custom-config.d.mts.map +1 -0
  29. package/dist/dev-utils/custom-config.mjs +57 -0
  30. package/dist/dev-utils/custom-config.mjs.map +1 -0
  31. package/dist/dev-utils/dev-logger.d.mts +8 -0
  32. package/dist/dev-utils/dev-logger.d.mts.map +1 -0
  33. package/dist/dev-utils/dev-logger.mjs +25 -0
  34. package/dist/dev-utils/dev-logger.mjs.map +1 -0
  35. package/dist/dev-utils/dev-server.d.mts +18 -0
  36. package/dist/dev-utils/dev-server.d.mts.map +1 -0
  37. package/dist/dev-utils/dev-server.mjs +265 -0
  38. package/dist/dev-utils/dev-server.mjs.map +1 -0
  39. package/dist/dev-utils/dev-tracer.d.ts +3 -0
  40. package/dist/dev-utils/dev-tracer.d.ts.map +1 -0
  41. package/dist/dev-utils/dev-tracer.js +28 -0
  42. package/dist/dev-utils/dev-tracer.js.map +1 -0
  43. package/dist/dev-utils/vite-plugin-dd-rum.d.mts +10 -0
  44. package/dist/dev-utils/vite-plugin-dd-rum.d.mts.map +1 -0
  45. package/dist/dev-utils/vite-plugin-dd-rum.mjs +34 -0
  46. package/dist/dev-utils/vite-plugin-dd-rum.mjs.map +1 -0
  47. package/dist/dev-utils/vite-plugin-react-transform.d.mts +7 -0
  48. package/dist/dev-utils/vite-plugin-react-transform.d.mts.map +1 -0
  49. package/dist/dev-utils/vite-plugin-react-transform.mjs +110 -0
  50. package/dist/dev-utils/vite-plugin-react-transform.mjs.map +1 -0
  51. package/dist/dev-utils/vite-plugin-sb-cdn.d.mts +34 -0
  52. package/dist/dev-utils/vite-plugin-sb-cdn.d.mts.map +1 -0
  53. package/dist/dev-utils/vite-plugin-sb-cdn.mjs +720 -0
  54. package/dist/dev-utils/vite-plugin-sb-cdn.mjs.map +1 -0
  55. package/dist/errors.d.ts +1 -0
  56. package/dist/errors.d.ts.map +1 -0
  57. package/dist/errors.js +4 -9
  58. package/dist/errors.js.map +1 -0
  59. package/dist/flag.d.ts +2 -2
  60. package/dist/flag.d.ts.map +1 -0
  61. package/dist/flag.js +5 -9
  62. package/dist/flag.js.map +1 -0
  63. package/dist/index.d.ts +10 -4
  64. package/dist/index.d.ts.map +1 -0
  65. package/dist/index.js +10 -20
  66. package/dist/index.js.map +1 -0
  67. package/dist/sdk.d.ts +42 -18
  68. package/dist/sdk.d.ts.map +1 -0
  69. package/dist/sdk.js +47 -43
  70. package/dist/sdk.js.map +1 -0
  71. package/dist/socket/handlers.d.ts +3 -128
  72. package/dist/socket/handlers.d.ts.map +1 -0
  73. package/dist/socket/handlers.js +7 -9
  74. package/dist/socket/handlers.js.map +1 -0
  75. package/dist/socket/index.d.ts +4 -3
  76. package/dist/socket/index.d.ts.map +1 -0
  77. package/dist/socket/index.js +12 -21
  78. package/dist/socket/index.js.map +1 -0
  79. package/dist/socket/signing.d.ts +3 -1
  80. package/dist/socket/signing.d.ts.map +1 -0
  81. package/dist/socket/signing.js +8 -17
  82. package/dist/socket/signing.js.map +1 -0
  83. package/dist/socket/socket.d.ts +3 -2
  84. package/dist/socket/socket.d.ts.map +1 -0
  85. package/dist/socket/socket.js +9 -19
  86. package/dist/socket/socket.js.map +1 -0
  87. package/dist/types/common.d.ts +2 -103
  88. package/dist/types/common.d.ts.map +1 -0
  89. package/dist/types/common.js +8 -24
  90. package/dist/types/common.js.map +1 -0
  91. package/dist/types/index.d.ts +5 -4
  92. package/dist/types/index.d.ts.map +1 -0
  93. package/dist/types/index.js +5 -20
  94. package/dist/types/index.js.map +1 -0
  95. package/dist/types/plugin.d.ts +1 -0
  96. package/dist/types/plugin.d.ts.map +1 -0
  97. package/dist/types/plugin.js +3 -5
  98. package/dist/types/plugin.js.map +1 -0
  99. package/dist/types/signing.d.ts +2 -1
  100. package/dist/types/signing.d.ts.map +1 -0
  101. package/dist/types/signing.js +2 -2
  102. package/dist/types/signing.js.map +1 -0
  103. package/dist/types/socket.d.ts +1 -0
  104. package/dist/types/socket.d.ts.map +1 -0
  105. package/dist/types/socket.js +2 -5
  106. package/dist/types/socket.js.map +1 -0
  107. package/dist/utils.d.ts +3 -1
  108. package/dist/utils.d.ts.map +1 -0
  109. package/dist/utils.js +14 -23
  110. package/dist/utils.js.map +1 -0
  111. package/dist/version-control.d.mts +59 -0
  112. package/dist/version-control.d.mts.map +1 -0
  113. package/dist/version-control.mjs +899 -0
  114. package/dist/version-control.mjs.map +1 -0
  115. package/eslint.config.js +85 -0
  116. package/package.json +76 -30
  117. package/src/cli-replacement/dev.mts +194 -0
  118. package/src/cli-replacement/init.ts +47 -0
  119. package/src/client.ts +114 -38
  120. package/src/dbfs/client.ts +162 -0
  121. package/src/dbfs/local.ts +163 -0
  122. package/src/dev-utils/custom-build.mts +113 -0
  123. package/src/dev-utils/custom-config.mts +66 -0
  124. package/src/dev-utils/dev-logger.mts +39 -0
  125. package/src/dev-utils/dev-server.mts +342 -0
  126. package/src/dev-utils/dev-tracer.ts +31 -0
  127. package/src/dev-utils/vite-plugin-dd-rum.mts +47 -0
  128. package/src/dev-utils/vite-plugin-react-transform.mts +130 -0
  129. package/src/dev-utils/vite-plugin-sb-cdn.mts +988 -0
  130. package/src/flag.ts +2 -3
  131. package/src/index.ts +119 -4
  132. package/src/sdk.ts +91 -17
  133. package/src/socket/handlers.ts +9 -147
  134. package/src/socket/index.ts +6 -9
  135. package/src/socket/signing.ts +7 -8
  136. package/src/socket/socket.ts +8 -9
  137. package/src/types/common.ts +2 -119
  138. package/src/types/index.ts +4 -4
  139. package/src/types/signing.ts +1 -1
  140. package/src/types/socket.ts +1 -1
  141. package/src/utils.ts +5 -6
  142. package/src/version-control.mts +1351 -0
  143. package/test/dev-utils/fixture/index.html +12 -0
  144. package/test/dev-utils/fixture/main.jsx +22 -0
  145. package/test/dev-utils/fixture/package-lock.json +25 -0
  146. package/test/dev-utils/fixture/package.json +9 -0
  147. package/test/dev-utils/vite-plugin-sb-cdn.test.mts +74 -0
  148. package/test/tsconfig.json +9 -0
  149. package/test/version-control.test.mts +1412 -0
  150. package/tsconfig.json +15 -4
  151. package/tsconfig.tsbuildinfo +1 -1
  152. package/.eslintrc.json +0 -55
@@ -0,0 +1,1412 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { readAppApiYamlFile, LoopType } from "@superblocksteam/util";
4
+ import { expect } from "chai";
5
+ import fs from "fs-extra";
6
+ import { describe, it } from "mocha";
7
+ import { stringify as ymlstringify } from "yaml";
8
+ import { FeatureFlags } from "../src/flag.js";
9
+ import {
10
+ addExistingFilePathsForApi,
11
+ validateLocalResource,
12
+ writeAppApi,
13
+ writeApplicationToDisk,
14
+ writeResourceToDisk,
15
+ } from "../src/version-control.mjs";
16
+ import type {
17
+ ApplicationWrapper,
18
+ MultiPageApplicationWrapperWithComponents,
19
+ } from "../src/client.js";
20
+ import type { ApiWithPb } from "../src/types/common.js";
21
+ import type { ApiRepresentation } from "../src/version-control.mjs";
22
+ import type {
23
+ SuperblocksApplicationConfig,
24
+ SuperblocksBackendConfig,
25
+ SuperblocksConfig,
26
+ } from "@superblocksteam/util";
27
+ import type { ApiWrapper } from "@superblocksteam/util";
28
+
29
+ // set to false to use the `test-output` workspace directory for debugging
30
+ const USE_TEMP_TEST_DIR = true;
31
+
32
+ const JS_LINE = "const x = 1;\n";
33
+ const SMALL_JS_CONTENT = JS_LINE.repeat(2);
34
+ const LARGE_JS_CONTENT = JS_LINE.repeat(20);
35
+ const LARGER_JS_CONTENT = JS_LINE.repeat(30);
36
+
37
+ const EXTRACT_SOURCE_CODE: ApiRepresentation = {
38
+ extractLargeSourceFiles: true,
39
+ minLinesForExtraction: 0,
40
+ };
41
+ const NO_EXTRACT_SOURCE_CODE: ApiRepresentation = {
42
+ extractLargeSourceFiles: false,
43
+ minLinesForExtraction: 0,
44
+ };
45
+
46
+ describe("version-control", function () {
47
+ const testDir = USE_TEMP_TEST_DIR
48
+ ? // eslint-disable-next-line mocha/no-setup-in-describe
49
+ fs.mkdtempSync(path.join(os.tmpdir(), "test-output"))
50
+ : // eslint-disable-next-line mocha/no-setup-in-describe
51
+ new URL("../../test-output", import.meta.url).pathname;
52
+
53
+ beforeEach(async function () {
54
+ // Clean up the temporary directory before each test, then create it
55
+ await fs.rm(testDir, { recursive: true, force: true });
56
+ await fs.ensureDir(testDir);
57
+ });
58
+
59
+ afterEach(async function () {
60
+ // Clean up the temporary directory after each test
61
+ await fs.rm(testDir, { recursive: true, force: true });
62
+ });
63
+
64
+ describe("writeAppApi", function () {
65
+ it("should write app api to separate directory with correct name", async function () {
66
+ // Setup
67
+ const api: ApiWrapper = {
68
+ apiPb: {
69
+ metadata: {
70
+ id: "test-id",
71
+ name: "Test API",
72
+ organization: "test-org-id",
73
+ },
74
+ blocks: [],
75
+ trigger: {},
76
+ },
77
+ };
78
+ const apiPromises: Promise<void>[] = [];
79
+
80
+ // Execute
81
+ const existingFilePaths = new Set<string>(); // empty since not overwriting existing files
82
+ const apiInfo = await writeAppApi(
83
+ api,
84
+ testDir,
85
+ existingFilePaths,
86
+ apiPromises,
87
+ EXTRACT_SOURCE_CODE,
88
+ );
89
+ await Promise.all(apiPromises);
90
+
91
+ // Verify the ApiInfo object is correct
92
+ expect(apiInfo.name).to.equal("Test API");
93
+ expect(apiInfo.sourceFiles).to.deep.equal([]);
94
+
95
+ // Verify the files exist and match expectations: one YAML file in directory nested under `apis`
96
+ const apiDir = path.join(testDir, "apis");
97
+ await expectFileExists(apiDir, "test_api/api.yaml");
98
+
99
+ // And does not have a YAML file in the `apis` directory
100
+ await expectNoFileExists(apiDir, "test_api.yaml");
101
+
102
+ // and can be read back in
103
+ (await readApiFile(apiDir, "Test API")).to.deep.equal({
104
+ api: api.apiPb,
105
+ stepPathMap: {},
106
+ });
107
+ });
108
+
109
+ it("should write app api to separate directory and extract large code blocks to separate files", async function () {
110
+ // Setup
111
+ const api: ApiWrapper = {
112
+ apiPb: {
113
+ metadata: {
114
+ id: "test-id",
115
+ name: "Test API",
116
+ organization: "test-org-id",
117
+ },
118
+ blocks: [
119
+ {
120
+ name: "Large Block",
121
+ step: {
122
+ integration: "javascript",
123
+ javascript: {
124
+ body: LARGE_JS_CONTENT,
125
+ },
126
+ },
127
+ },
128
+ ],
129
+ trigger: {},
130
+ },
131
+ };
132
+ const apiPromises: Promise<void>[] = [];
133
+
134
+ // Execute
135
+ const existingFilePaths = new Set<string>(); // empty since not overwriting existing files
136
+ const apiInfo = await writeAppApi(
137
+ api,
138
+ testDir,
139
+ existingFilePaths,
140
+ apiPromises,
141
+ EXTRACT_SOURCE_CODE,
142
+ );
143
+ await Promise.all(apiPromises);
144
+
145
+ // Verify the ApiInfo object is correct
146
+ expect(apiInfo.name).to.equal("Test API");
147
+ expect(apiInfo.sourceFiles).to.deep.equal(["large_block.js"]);
148
+
149
+ // Verify the files exist and match expectations
150
+ const apiDir = path.join(testDir, "apis");
151
+ (await expectFileExists(apiDir, "test_api/api.yaml")).and.contains(
152
+ "path: ./large_block.js",
153
+ );
154
+ (await expectFileExists(apiDir, "test_api/large_block.js")).and.includes(
155
+ LARGE_JS_CONTENT,
156
+ );
157
+
158
+ // And does not have a YAML file in the `apis` directory
159
+ await expectNoFileExists(apiDir, "test_api.yaml");
160
+
161
+ // and can be read back in
162
+ (await readApiFile(apiDir, api.apiPb.metadata.name)).to.deep.equal({
163
+ api: api.apiPb,
164
+ stepPathMap: {
165
+ "Large Block": "./large_block.js",
166
+ },
167
+ });
168
+ });
169
+
170
+ it("should write app api with similarly-named steps to separate directory and extract large code blocks to separate files", async function () {
171
+ const stepName1 = "Large Block";
172
+ const stepName2 = stepName1.toLocaleLowerCase();
173
+
174
+ // Setup
175
+ const api: ApiWrapper = {
176
+ apiPb: {
177
+ metadata: {
178
+ id: "test-id",
179
+ name: "Test API",
180
+ organization: "test-org-id",
181
+ },
182
+ blocks: [
183
+ {
184
+ name: stepName1,
185
+ step: {
186
+ integration: "javascript",
187
+ javascript: {
188
+ body: LARGE_JS_CONTENT,
189
+ },
190
+ },
191
+ },
192
+ {
193
+ name: stepName2,
194
+ step: {
195
+ integration: "javascript",
196
+ javascript: {
197
+ body: LARGER_JS_CONTENT,
198
+ },
199
+ },
200
+ },
201
+ ],
202
+ trigger: {},
203
+ },
204
+ };
205
+ const apiPromises: Promise<void>[] = [];
206
+
207
+ // Execute
208
+ const existingFilePaths = new Set<string>(); // empty since not overwriting existing files
209
+ const apiInfo = await writeAppApi(
210
+ api,
211
+ testDir,
212
+ existingFilePaths,
213
+ apiPromises,
214
+ EXTRACT_SOURCE_CODE,
215
+ );
216
+ await Promise.all(apiPromises);
217
+
218
+ // Verify the ApiInfo object is correct
219
+ expect(apiInfo.name).to.equal("Test API");
220
+ expect(apiInfo.sourceFiles).to.deep.equal([
221
+ "large_block.js",
222
+ "large_block_1.js",
223
+ ]);
224
+
225
+ // Verify the files exist and match expectations
226
+ const apiDir = path.join(testDir, "apis");
227
+ (await expectFileExists(apiDir, "test_api/api.yaml")).and.contains(
228
+ "path: ./large_block.js",
229
+ );
230
+ (await expectFileExists(apiDir, "test_api/large_block.js")).and.includes(
231
+ LARGE_JS_CONTENT,
232
+ );
233
+ (
234
+ await expectFileExists(apiDir, "test_api/large_block_1.js")
235
+ ).and.includes(LARGER_JS_CONTENT);
236
+
237
+ // And does not have a YAML file in the `apis` directory
238
+ await expectNoFileExists(apiDir, "test_api.yaml");
239
+
240
+ // and can be read back in
241
+ (await readApiFile(apiDir, api.apiPb.metadata.name)).to.deep.equal({
242
+ api: api.apiPb,
243
+ stepPathMap: {
244
+ "Large Block": "./large_block.js",
245
+ "large block": "./large_block_1.js",
246
+ },
247
+ });
248
+ });
249
+
250
+ it("should write app api to separate directory and extract small code blocks to separate files", async function () {
251
+ // Setup
252
+ const api: ApiWrapper = {
253
+ apiPb: {
254
+ metadata: {
255
+ id: "test-id",
256
+ name: "Test API",
257
+ organization: "test-org-id",
258
+ },
259
+ blocks: [
260
+ {
261
+ name: "Small Block",
262
+ step: {
263
+ integration: "javascript",
264
+ javascript: {
265
+ body: SMALL_JS_CONTENT,
266
+ },
267
+ },
268
+ },
269
+ ],
270
+ trigger: {},
271
+ },
272
+ };
273
+ const apiPromises: Promise<void>[] = [];
274
+
275
+ // Execute
276
+ const existingFilePaths = new Set<string>(); // empty since not overwriting existing files
277
+ const apiInfo = await writeAppApi(
278
+ api,
279
+ testDir,
280
+ existingFilePaths,
281
+ apiPromises,
282
+ EXTRACT_SOURCE_CODE,
283
+ );
284
+ await Promise.all(apiPromises);
285
+
286
+ // Verify the ApiInfo object is correct
287
+ expect(apiInfo.name).to.equal("Test API");
288
+ expect(apiInfo.sourceFiles).to.deep.equal(["small_block.js"]);
289
+
290
+ // Verify the files exist and match expectations: one YAML file in directory nested under `apis`
291
+ const apisDir = path.join(testDir, "apis");
292
+ (await expectFileExists(apisDir, "test_api/api.yaml")).and.contains(
293
+ "path: ./small_block.js",
294
+ );
295
+ (await expectFileExists(apisDir, "test_api/small_block.js")).and.contains(
296
+ SMALL_JS_CONTENT,
297
+ );
298
+ // And no separate code file and no nested directory with the files
299
+ await expectNoFileExists(apisDir, "small_block.js");
300
+ await expectNoFileExists(apisDir, "test_api.yaml");
301
+ await expectNoFileExists(apisDir, "test_api/test_api.yaml");
302
+
303
+ // and can be read back in
304
+ (await readApiFile(apisDir, api.apiPb.metadata.name)).to.deep.equal({
305
+ api: api.apiPb,
306
+ stepPathMap: {
307
+ "Small Block": "./small_block.js",
308
+ },
309
+ });
310
+ });
311
+
312
+ it("should write app api to apis directory and not extract code blocks to separate files", async function () {
313
+ // Setup
314
+ const api: ApiWrapper = {
315
+ apiPb: {
316
+ metadata: {
317
+ id: "test-id",
318
+ name: "Test API",
319
+ organization: "test-org-id",
320
+ },
321
+ blocks: [
322
+ {
323
+ name: "Large Block",
324
+ step: {
325
+ integration: "javascript",
326
+ javascript: {
327
+ body: LARGE_JS_CONTENT,
328
+ },
329
+ },
330
+ },
331
+ ],
332
+ trigger: {},
333
+ },
334
+ };
335
+ const apiPromises: Promise<void>[] = [];
336
+
337
+ // Execute
338
+ const existingFilePaths = new Set<string>(); // empty since not overwriting existing files
339
+ const apiInfo = await writeAppApi(
340
+ api,
341
+ testDir,
342
+ existingFilePaths,
343
+ apiPromises,
344
+ NO_EXTRACT_SOURCE_CODE,
345
+ );
346
+ await Promise.all(apiPromises);
347
+
348
+ // Verify the ApiInfo object is correct
349
+ expect(apiInfo.name).to.equal("Test API");
350
+ expect(apiInfo.sourceFiles).to.deep.equal([]);
351
+
352
+ // Compute the expected YAML file content
353
+ const expectedYamlContent = ymlstringify(api.apiPb, {
354
+ sortMapEntries: true,
355
+ blockQuote: "literal",
356
+ });
357
+
358
+ // Verify the files exist and match expectations
359
+ const apisDir = path.join(testDir, "apis");
360
+ (await expectFileExists(apisDir, "test_api.yaml")).and.equals(
361
+ expectedYamlContent,
362
+ );
363
+ await expectNoFileExists(apisDir, "large_block.js");
364
+ await expectNoFileExists(apisDir, "test_api/test_api.yaml");
365
+ await expectNoFileExists(apisDir, "test_api/large_block.js");
366
+
367
+ // and can be read back in
368
+ (await readApiFile(apisDir, api.apiPb.metadata.name)).to.deep.equal({
369
+ api: api.apiPb,
370
+ stepPathMap: {},
371
+ });
372
+ });
373
+
374
+ it("should write api with try-catch, conditional, loop, and parallel JavaScript steps", async function () {
375
+ const api: ApiWrapper = {
376
+ apiPb: {
377
+ metadata: {
378
+ id: "test-id",
379
+ name: "Try Catch API",
380
+ organization: "test-org-id",
381
+ },
382
+ blocks: [
383
+ {
384
+ name: "Try Catch Block",
385
+ tryCatch: {
386
+ catch: {
387
+ blocks: [
388
+ {
389
+ name: "Catch Block",
390
+ loop: {
391
+ range: "{{[1,2,3]}}",
392
+ type: LoopType.FOR_EACH,
393
+ variables: {
394
+ index: "indexVar",
395
+ item: "itemVar",
396
+ },
397
+ blocks: [
398
+ {
399
+ name: "Loop Step",
400
+ step: {
401
+ integration: "javascript",
402
+ javascript: {
403
+ body: LARGER_JS_CONTENT,
404
+ },
405
+ },
406
+ },
407
+ ],
408
+ },
409
+ },
410
+ ],
411
+ },
412
+ try: {
413
+ blocks: [
414
+ {
415
+ name: "Conditional Block",
416
+ conditional: {
417
+ if: {
418
+ condition: "{{2 > 1}}",
419
+ blocks: [
420
+ {
421
+ name: "If Step",
422
+ step: {
423
+ integration: "javascript",
424
+ javascript: {
425
+ body: LARGER_JS_CONTENT,
426
+ },
427
+ },
428
+ },
429
+ ],
430
+ },
431
+ elseIf: [
432
+ {
433
+ condition: "{{}}",
434
+ blocks: [
435
+ {
436
+ name: "Else If Step",
437
+ step: {
438
+ integration: "javascript",
439
+ javascript: {
440
+ body: LARGE_JS_CONTENT,
441
+ },
442
+ },
443
+ },
444
+ ],
445
+ },
446
+ ],
447
+ else: {
448
+ blocks: [
449
+ {
450
+ name: "Else Step",
451
+ step: {
452
+ integration: "javascript",
453
+ javascript: {
454
+ body: LARGE_JS_CONTENT,
455
+ },
456
+ },
457
+ },
458
+ ],
459
+ },
460
+ },
461
+ },
462
+ ],
463
+ },
464
+ variables: {
465
+ indexVar: "indexVar",
466
+ itemVar: "itemVar",
467
+ },
468
+ },
469
+ },
470
+ {
471
+ name: "Second Block",
472
+ step: {
473
+ integration: "javascript",
474
+ javascript: {
475
+ body: LARGE_JS_CONTENT,
476
+ },
477
+ },
478
+ },
479
+ {
480
+ name: "Parallel Block",
481
+ parallel: {
482
+ static: {
483
+ paths: {
484
+ Path1: {
485
+ blocks: [
486
+ {
487
+ name: "Path1 step",
488
+ step: {
489
+ integration: "javascript",
490
+ javascript: {
491
+ body: LARGE_JS_CONTENT,
492
+ },
493
+ },
494
+ },
495
+ ],
496
+ },
497
+ Path2: {
498
+ blocks: [
499
+ {
500
+ name: "Path2 step",
501
+ step: {
502
+ integration: "javascript",
503
+ javascript: {
504
+ body: SMALL_JS_CONTENT,
505
+ },
506
+ },
507
+ },
508
+ ],
509
+ },
510
+ },
511
+ },
512
+ },
513
+ },
514
+ ],
515
+ trigger: {},
516
+ },
517
+ };
518
+ // Compute the expected YAML file content
519
+ const expectedYamlContent = ymlstringify(api.apiPb, {
520
+ sortMapEntries: true,
521
+ blockQuote: "literal",
522
+ });
523
+
524
+ // This test writes the API several times, so we need to keep track of the existing files
525
+ const existingFilePaths = new Set<string>();
526
+
527
+ {
528
+ // Write the API and extract code blocks
529
+ const apiPromises: Promise<void>[] = [];
530
+ const apiInfo = await writeAppApi(
531
+ api,
532
+ testDir,
533
+ existingFilePaths,
534
+ apiPromises,
535
+ EXTRACT_SOURCE_CODE,
536
+ );
537
+ await Promise.all(apiPromises);
538
+
539
+ // Verify the ApiInfo object is correct
540
+ expect(apiInfo.name).to.equal("Try Catch API");
541
+ expect(apiInfo.sourceFiles).to.deep.equal([
542
+ "else_if_step.js",
543
+ "else_step.js",
544
+ "if_step.js",
545
+ "loop_step.js",
546
+ "path1_step.js",
547
+ "path2_step.js",
548
+ "second_block.js",
549
+ ]);
550
+
551
+ // Verify the files exist and match expectations
552
+ const apiDir = path.join(testDir, "apis");
553
+ (await expectFileExists(apiDir, "try_catch_api/api.yaml")).and.contains(
554
+ "path: ./loop_step.js",
555
+ );
556
+ (
557
+ await expectFileExists(apiDir, "try_catch_api/loop_step.js")
558
+ ).and.includes(LARGER_JS_CONTENT);
559
+ (
560
+ await expectFileExists(apiDir, "try_catch_api/second_block.js")
561
+ ).and.includes(LARGE_JS_CONTENT);
562
+ (
563
+ await expectFileExists(apiDir, "try_catch_api/if_step.js")
564
+ ).and.includes(LARGER_JS_CONTENT);
565
+ (
566
+ await expectFileExists(apiDir, "try_catch_api/else_if_step.js")
567
+ ).and.includes(LARGE_JS_CONTENT);
568
+ (
569
+ await expectFileExists(apiDir, "try_catch_api/else_step.js")
570
+ ).and.includes(LARGE_JS_CONTENT);
571
+ (
572
+ await expectFileExists(apiDir, "try_catch_api/path1_step.js")
573
+ ).and.includes(LARGE_JS_CONTENT);
574
+ (
575
+ await expectFileExists(apiDir, "try_catch_api/path2_step.js")
576
+ ).and.includes(SMALL_JS_CONTENT);
577
+
578
+ // and can be read back in
579
+ (await readApiFile(apiDir, "Try Catch API")).to.deep.equal({
580
+ api: api.apiPb,
581
+ stepPathMap: {
582
+ "Else If Step": "./else_if_step.js",
583
+ "Else Step": "./else_step.js",
584
+ "If Step": "./if_step.js",
585
+ "Loop Step": "./loop_step.js",
586
+ "Path1 step": "./path1_step.js",
587
+ "Path2 step": "./path2_step.js",
588
+ "Second Block": "./second_block.js",
589
+ },
590
+ });
591
+
592
+ // Finally add the existing files to the set to be removed later
593
+ addExistingFilePathsForApi(apiInfo, apiDir, existingFilePaths, true);
594
+ }
595
+
596
+ {
597
+ // Write the API and do NOT extract code blocks
598
+ const apiPromises: Promise<void>[] = [];
599
+ const apiInfo = await writeAppApi(
600
+ api,
601
+ testDir,
602
+ existingFilePaths,
603
+ apiPromises,
604
+ NO_EXTRACT_SOURCE_CODE,
605
+ );
606
+ await Promise.all(apiPromises);
607
+ await removeExistingFiles(existingFilePaths);
608
+
609
+ // Verify the ApiInfo object is correct
610
+ expect(apiInfo.name).to.equal("Try Catch API");
611
+ expect(apiInfo.sourceFiles).to.deep.equal([]);
612
+
613
+ // Verify the files exist and match expectations
614
+ const apiDir = path.join(testDir, "apis");
615
+ (await expectFileExists(apiDir, "try_catch_api.yaml")).and.equals(
616
+ expectedYamlContent,
617
+ );
618
+
619
+ // And no code files are created in a nested directory
620
+ await expectNoFileExists(apiDir, "try_catch_api/api.yaml");
621
+ await expectNoFileExists(apiDir, "try_catch_api/try_catch_api.yaml");
622
+ await expectNoFileExists(apiDir, "try_catch_api/loop_step.js");
623
+ await expectNoFileExists(apiDir, "try_catch_api/second_block.js");
624
+ await expectNoFileExists(apiDir, "try_catch_api/if_step.js");
625
+ await expectNoFileExists(apiDir, "try_catch_api/else_if_step.js");
626
+ await expectNoFileExists(apiDir, "try_catch_api/else_step.js");
627
+ await expectNoFileExists(apiDir, "try_catch_api/path1_step.js");
628
+ await expectNoFileExists(apiDir, "try_catch_api/path2_step.js");
629
+ await expectNoFileExists(apiDir, "try_catch_api");
630
+
631
+ // and can be read back in
632
+ (await readApiFile(apiDir, "Try Catch API")).to.deep.equal({
633
+ api: api.apiPb,
634
+ stepPathMap: {},
635
+ });
636
+ }
637
+ });
638
+ });
639
+
640
+ describe("writeResourcesToDisk", function () {
641
+ const mockFeatureFlags = new FeatureFlags({
642
+ "superblocks.git.split.large.steps.enabled": true,
643
+ "superblocks.git.split.large.steps.new.enabled": true,
644
+ "superblocks.git.split.large.step.lines": 10,
645
+ });
646
+
647
+ it("should write single page app to separate directory and extract code blocks to separate files", async function () {
648
+ // Setup
649
+ const resourceId = "9736f700-88d6-49fc-a519-245a20af248b";
650
+ const testApi: ApiWithPb = {
651
+ id: "test-id",
652
+ apiPb: {
653
+ metadata: {
654
+ id: "test-id",
655
+ name: "Test API",
656
+ organization: "test-org-id",
657
+ },
658
+ blocks: [
659
+ {
660
+ name: "Large Block",
661
+ step: {
662
+ integration: "javascript",
663
+ javascript: {
664
+ body: LARGE_JS_CONTENT,
665
+ },
666
+ },
667
+ },
668
+ ],
669
+ trigger: {},
670
+ },
671
+ };
672
+ const singlePageApp: ApplicationWrapper = {
673
+ application: {
674
+ id: resourceId,
675
+ name: "Test App",
676
+ metadata: {
677
+ fileVersion: "0.2.0", // this and the feature flag drive whether we split files or not
678
+ },
679
+ },
680
+ page: {
681
+ id: "test-page-id",
682
+ name: "Test Page",
683
+ apis: [],
684
+ applicationId: resourceId,
685
+ isHidden: false,
686
+ layouts: [],
687
+ },
688
+ apis: [testApi],
689
+ };
690
+ const apiPromises: Promise<void>[] = [];
691
+
692
+ // Execute
693
+ const resourceConfig = await writeResourceToDisk(
694
+ "APPLICATION",
695
+ resourceId,
696
+ singlePageApp,
697
+ testDir,
698
+ mockFeatureFlags,
699
+ undefined,
700
+ EXTRACT_SOURCE_CODE,
701
+ );
702
+ await Promise.all(apiPromises);
703
+
704
+ // Verify the superblocks.json file is correct for a SINGLE PAGE APP (no source files)
705
+ const config = (await expectSuperblocksJsonFileExists(
706
+ `${testDir}/apps/test_app`,
707
+ )) as SuperblocksApplicationConfig;
708
+ // Verify the apis is undefined for the new format
709
+ expect(config.apis).to.be.undefined;
710
+ // and the appApis is defined
711
+ expect(config.appApis?.[testApi.id].name).to.equal(
712
+ testApi.apiPb.metadata.name,
713
+ );
714
+
715
+ // Verify the files exist and match expectations
716
+ expect(resourceConfig.location).to.equal("apps/test_app");
717
+ await expectFileExists(testDir, "apps/test_app/application.yaml");
718
+ await expectFileExists(testDir, "apps/test_app/page.yaml");
719
+ (
720
+ await expectFileExists(testDir, "apps/test_app/apis/test_api/api.yaml")
721
+ ).and.contains("path: ./large_block.js");
722
+ (
723
+ await expectFileExists(
724
+ testDir,
725
+ "apps/test_app/apis/test_api/large_block.js",
726
+ )
727
+ ).and.includes(LARGE_JS_CONTENT);
728
+ // and can be read back in
729
+ (
730
+ await readApiFile(`${testDir}/apps/test_app/apis`, "Test API")
731
+ ).to.deep.equal({
732
+ api: testApi.apiPb,
733
+ stepPathMap: {
734
+ "Large Block": "./large_block.js",
735
+ },
736
+ });
737
+ });
738
+
739
+ it("should write backend to separate directory and extract code blocks to separate files", async function () {
740
+ // Setup
741
+ const resourceId = "9736f700-88d6-49fc-a519-245a20af248b";
742
+ const testApi: ApiWrapper = {
743
+ apiPb: {
744
+ metadata: {
745
+ id: "test-id",
746
+ name: "Test Backend",
747
+ organization: "test-org-id",
748
+ },
749
+ blocks: [
750
+ {
751
+ name: "Large Block",
752
+ step: {
753
+ integration: "javascript",
754
+ javascript: {
755
+ body: LARGE_JS_CONTENT,
756
+ },
757
+ },
758
+ },
759
+ ],
760
+ trigger: {},
761
+ },
762
+ };
763
+ // Compute the expected YAML file content
764
+ const expectedYamlContent = ymlstringify(testApi.apiPb, {
765
+ sortMapEntries: true,
766
+ blockQuote: "literal",
767
+ });
768
+ // Write out the backend without extracting any code blocks
769
+ {
770
+ // Execute
771
+ const apiPromises: Promise<void>[] = [];
772
+ const resourceConfig = await writeResourceToDisk(
773
+ "BACKEND",
774
+ resourceId,
775
+ testApi,
776
+ testDir,
777
+ mockFeatureFlags,
778
+ undefined,
779
+ NO_EXTRACT_SOURCE_CODE,
780
+ );
781
+ await Promise.all(apiPromises);
782
+
783
+ // Verify the superblocks.json file is correct for a BACKEND that does not extract source files
784
+ const config = (await expectSuperblocksJsonFileExists(
785
+ `${testDir}/backends/test_backend`,
786
+ )) as SuperblocksBackendConfig;
787
+ expect(config.sourceFiles).to.be.undefined;
788
+
789
+ // Verify the files exist and match expectations
790
+ expect(resourceConfig.location).to.equal("backends/test_backend");
791
+ await expectFileExists(testDir, "backends/test_backend/api.yaml");
792
+ (
793
+ await expectFileExists(testDir, "backends/test_backend/api.yaml")
794
+ ).and.contains(expectedYamlContent);
795
+ await expectNoFileExists(
796
+ testDir,
797
+ "backends/test_backend/large_block.js",
798
+ );
799
+
800
+ // and can be read back in
801
+ (await readApiFile(`${testDir}/backends/test_backend`)).to.deep.equal({
802
+ api: testApi.apiPb,
803
+ stepPathMap: {},
804
+ });
805
+ // and a push operation would consider it valid
806
+ expect(await validateLocalResource(testDir, resourceConfig)).to.be
807
+ .undefined;
808
+ }
809
+ // Write out the backend AND extract any code blocks
810
+ {
811
+ // Execute
812
+ const apiPromises: Promise<void>[] = [];
813
+ const resourceConfig = await writeResourceToDisk(
814
+ "BACKEND",
815
+ resourceId,
816
+ testApi,
817
+ testDir,
818
+ mockFeatureFlags,
819
+ undefined,
820
+ EXTRACT_SOURCE_CODE,
821
+ );
822
+ await Promise.all(apiPromises);
823
+
824
+ // Verify the superblocks.json file is correct for a BACKEND that does extract source files
825
+ const config = (await expectSuperblocksJsonFileExists(
826
+ `${testDir}/backends/test_backend`,
827
+ )) as SuperblocksBackendConfig;
828
+ expect(config.sourceFiles).to.deep.equal(["large_block.js"]);
829
+
830
+ // Verify the files exist and match expectations
831
+ expect(resourceConfig.location).to.equal("backends/test_backend");
832
+ (
833
+ await expectFileExists(testDir, "backends/test_backend/api.yaml")
834
+ ).and.contains("path: ./large_block.js");
835
+ (
836
+ await expectFileExists(
837
+ testDir,
838
+ "backends/test_backend/large_block.js",
839
+ )
840
+ ).and.contains(LARGE_JS_CONTENT);
841
+ // and can be read back in
842
+ (await readApiFile(`${testDir}/backends/test_backend`)).to.deep.equal({
843
+ api: testApi.apiPb,
844
+ stepPathMap: {
845
+ "Large Block": "./large_block.js",
846
+ },
847
+ });
848
+ // and a push operation would consider it valid
849
+ expect(await validateLocalResource(testDir, resourceConfig)).to.be
850
+ .undefined;
851
+ }
852
+ // Write out the backend AGAIN without extracting any code blocks
853
+ {
854
+ // Execute
855
+ const apiPromises: Promise<void>[] = [];
856
+ const resourceConfig = await writeResourceToDisk(
857
+ "BACKEND",
858
+ resourceId,
859
+ testApi,
860
+ testDir,
861
+ mockFeatureFlags,
862
+ undefined,
863
+ NO_EXTRACT_SOURCE_CODE,
864
+ );
865
+ await Promise.all(apiPromises);
866
+
867
+ // Verify the superblocks.json file is correct for a BACKEND that does not extract source files
868
+ const config = (await expectSuperblocksJsonFileExists(
869
+ `${testDir}/backends/test_backend`,
870
+ )) as SuperblocksBackendConfig;
871
+ expect(config.sourceFiles).to.be.undefined;
872
+
873
+ // Verify the files exist and match expectations
874
+ expect(resourceConfig.location).to.equal("backends/test_backend");
875
+ await expectFileExists(testDir, "backends/test_backend/api.yaml");
876
+ (
877
+ await expectFileExists(testDir, "backends/test_backend/api.yaml")
878
+ ).and.contains(expectedYamlContent);
879
+ await expectNoFileExists(
880
+ testDir,
881
+ "backends/test_backend/large_block.js",
882
+ );
883
+ // and can be read back in
884
+ (await readApiFile(`${testDir}/backends/test_backend`)).to.deep.equal({
885
+ api: testApi.apiPb,
886
+ stepPathMap: {},
887
+ });
888
+ // and a push operation would consider it valid
889
+ expect(await validateLocalResource(testDir, resourceConfig)).to.be
890
+ .undefined;
891
+ }
892
+ });
893
+ });
894
+
895
+ describe("writeApplicationToDIsk", function () {
896
+ const mockFeatureFlags = new FeatureFlags({
897
+ "superblocks.git.split.large.steps.enabled": true,
898
+ "superblocks.git.split.large.steps.new.enabled": false,
899
+ "superblocks.git.split.large.step.lines": 10,
900
+ });
901
+
902
+ it("should write multi-pageapp to disk", async function () {
903
+ // Setup
904
+ const resourceId = "9736f700-88d6-49fc-a519-245a20af248b";
905
+ const testApi: ApiWithPb = {
906
+ id: "test-id",
907
+ apiPb: {
908
+ metadata: {
909
+ id: "test-id",
910
+ name: "Test API",
911
+ organization: "test-org-id",
912
+ },
913
+ blocks: [
914
+ {
915
+ name: "Large Block",
916
+ step: {
917
+ integration: "javascript",
918
+ javascript: {
919
+ body: LARGE_JS_CONTENT,
920
+ },
921
+ },
922
+ },
923
+ ],
924
+ trigger: {},
925
+ },
926
+ };
927
+ const multiPageApp: MultiPageApplicationWrapperWithComponents = {
928
+ type: "multi-page",
929
+ application: {
930
+ id: resourceId,
931
+ name: "Test App",
932
+ },
933
+ pages: [
934
+ {
935
+ id: "test-page-id",
936
+ name: "Test Page",
937
+ apis: [testApi],
938
+ applicationId: resourceId,
939
+ isHidden: false,
940
+ layouts: [],
941
+ },
942
+ ],
943
+ apis: [],
944
+ componentFiles: [],
945
+ };
946
+ const apiPromises: Promise<void>[] = [];
947
+
948
+ // Execute
949
+ const resourceConfig = await writeApplicationToDisk({
950
+ sdk: {} as any,
951
+ resource: multiPageApp,
952
+ projectRootFolder: testDir,
953
+ featureFlags: mockFeatureFlags,
954
+ migrateFromSinglePage: false,
955
+ preferredApiRepresentation: EXTRACT_SOURCE_CODE,
956
+ });
957
+ await Promise.all(apiPromises);
958
+
959
+ // Verify the superblocks.json file is correct for a multipage app that does extract source files
960
+ const config = (await expectSuperblocksJsonFileExists(
961
+ `${testDir}/apps/test_app/`,
962
+ )) as SuperblocksApplicationConfig;
963
+ const page = config.pages?.[multiPageApp.pages[0].id];
964
+ expect(page?.apis).to.be.undefined;
965
+ expect(page?.pageApis?.[testApi.id]?.sourceFiles).to.deep.equal([
966
+ "large_block.js",
967
+ ]);
968
+
969
+ // Verify the files exist and match expectations
970
+ expect(resourceConfig.location).to.equal("apps/test_app");
971
+ await expectFileExists(testDir, "apps/test_app/application.yaml");
972
+ await expectFileExists(
973
+ testDir,
974
+ "apps/test_app/pages/test_page/page.yaml",
975
+ );
976
+ (
977
+ await expectFileExists(
978
+ testDir,
979
+ "apps/test_app/pages/test_page/apis/test_api/api.yaml",
980
+ )
981
+ ).and.includes("path: ./large_block.js");
982
+ (
983
+ await expectFileExists(
984
+ testDir,
985
+ "apps/test_app/pages/test_page/apis/test_api/large_block.js",
986
+ )
987
+ ).and.includes(LARGE_JS_CONTENT);
988
+
989
+ // and does not have the files in the old format
990
+ await expectNoFileExists(
991
+ testDir,
992
+ "apps/test_app/pages/test_page/apis/test_api.yaml",
993
+ );
994
+ await expectNoFileExists(
995
+ testDir,
996
+ "apps/test_app/pages/test_page/apis/test_api/test_api.yaml",
997
+ );
998
+
999
+ // and can be read back in
1000
+ (
1001
+ await readApiFile(
1002
+ `${testDir}/apps/test_app/pages/test_page/apis`,
1003
+ "Test API",
1004
+ )
1005
+ ).to.deep.equal({
1006
+ api: testApi.apiPb,
1007
+ stepPathMap: {
1008
+ "Large Block": "./large_block.js",
1009
+ },
1010
+ });
1011
+ // and a push operation would consider it valid
1012
+ expect(await validateLocalResource(testDir, resourceConfig)).to.be
1013
+ .undefined;
1014
+ });
1015
+
1016
+ it("should write multi-pageapp to disk and overwrite existing files", async function () {
1017
+ // Setup
1018
+ const resourceId = "9736f700-88d6-49fc-a519-245a20af248b";
1019
+ const testApi: ApiWithPb = {
1020
+ id: "test-id",
1021
+ apiPb: {
1022
+ metadata: {
1023
+ id: "test-id",
1024
+ name: "Test API",
1025
+ organization: "test-org-id",
1026
+ },
1027
+ blocks: [
1028
+ {
1029
+ name: "Large Block",
1030
+ step: {
1031
+ integration: "javascript",
1032
+ javascript: {
1033
+ body: LARGE_JS_CONTENT,
1034
+ },
1035
+ },
1036
+ },
1037
+ ],
1038
+ trigger: {},
1039
+ },
1040
+ };
1041
+ const multiPageApp: MultiPageApplicationWrapperWithComponents = {
1042
+ type: "multi-page",
1043
+ application: {
1044
+ id: resourceId,
1045
+ name: "Test App",
1046
+ },
1047
+ pages: [
1048
+ {
1049
+ id: "test-page-id",
1050
+ name: "Test Page",
1051
+ apis: [testApi],
1052
+ applicationId: resourceId,
1053
+ isHidden: false,
1054
+ layouts: [],
1055
+ },
1056
+ ],
1057
+ apis: [],
1058
+ componentFiles: [],
1059
+ };
1060
+ const apiPromises: Promise<void>[] = [];
1061
+
1062
+ // Write out the multi-page app without extracting any code blocks
1063
+ {
1064
+ const resourceConfig = await writeApplicationToDisk({
1065
+ sdk: {} as any,
1066
+ resource: multiPageApp,
1067
+ projectRootFolder: testDir,
1068
+ featureFlags: mockFeatureFlags,
1069
+ migrateFromSinglePage: false,
1070
+ preferredApiRepresentation: NO_EXTRACT_SOURCE_CODE,
1071
+ });
1072
+ await Promise.all(apiPromises);
1073
+
1074
+ // Compute the expected YAML file content with embedded source code
1075
+ const expectedYamlContent = ymlstringify(testApi.apiPb, {
1076
+ sortMapEntries: true,
1077
+ blockQuote: "literal",
1078
+ });
1079
+
1080
+ // Verify the superblocks.json file is correct for a multipage app that does NOT extract source files
1081
+ const config = (await expectSuperblocksJsonFileExists(
1082
+ `${testDir}/apps/test_app/`,
1083
+ )) as SuperblocksApplicationConfig;
1084
+ const page = config.pages?.[multiPageApp.pages[0].id];
1085
+ expect(page?.pageApis).to.be.undefined;
1086
+ expect(page?.apis).to.be.deep.equal({ "test-id": "Test API" });
1087
+
1088
+ // Verify the files exist and match expectations
1089
+ expect(resourceConfig.location).to.equal("apps/test_app");
1090
+ await expectFileExists(testDir, "apps/test_app/application.yaml");
1091
+ await expectFileExists(
1092
+ testDir,
1093
+ "apps/test_app/pages/test_page/page.yaml",
1094
+ );
1095
+ (
1096
+ await expectFileExists(
1097
+ testDir,
1098
+ "apps/test_app/pages/test_page/apis/test_api.yaml",
1099
+ )
1100
+ ).and.contains(expectedYamlContent);
1101
+ // And does not have the source code extracted
1102
+ await expectNoFileExists(
1103
+ testDir,
1104
+ "apps/test_app/pages/test_page/apis/test_api/large_block.js",
1105
+ );
1106
+ // and can be read back in
1107
+ (
1108
+ await readApiFile(
1109
+ `${testDir}/apps/test_app/pages/test_page/apis`,
1110
+ "Test API",
1111
+ )
1112
+ ).to.deep.equal({ api: testApi.apiPb, stepPathMap: {} });
1113
+ // and a push operation would consider it valid
1114
+ expect(await validateLocalResource(testDir, resourceConfig)).to.be
1115
+ .undefined;
1116
+ }
1117
+
1118
+ // Write out the same multi-page app again, but this time extract any code blocks
1119
+ {
1120
+ const resourceConfig = await writeApplicationToDisk({
1121
+ sdk: {} as any,
1122
+ resource: multiPageApp,
1123
+ projectRootFolder: testDir,
1124
+ featureFlags: mockFeatureFlags,
1125
+ migrateFromSinglePage: false,
1126
+ preferredApiRepresentation: EXTRACT_SOURCE_CODE,
1127
+ });
1128
+ await Promise.all(apiPromises);
1129
+
1130
+ // Verify the superblocks.json file is correct for a multipage app that does extract source files
1131
+ const config = (await expectSuperblocksJsonFileExists(
1132
+ `${testDir}/apps/test_app/`,
1133
+ )) as SuperblocksApplicationConfig;
1134
+ const page = config.pages?.[multiPageApp.pages[0].id];
1135
+ expect(page?.apis).to.be.undefined;
1136
+ expect(page?.pageApis?.[testApi.id]?.sourceFiles).to.deep.equal([
1137
+ "large_block.js",
1138
+ ]);
1139
+
1140
+ // Verify the files exist and match expectations
1141
+ expect(resourceConfig.location).to.equal("apps/test_app");
1142
+ await expectFileExists(testDir, "apps/test_app/application.yaml");
1143
+ await expectFileExists(
1144
+ testDir,
1145
+ "apps/test_app/pages/test_page/page.yaml",
1146
+ );
1147
+ (
1148
+ await expectFileExists(
1149
+ testDir,
1150
+ "apps/test_app/pages/test_page/apis/test_api/api.yaml",
1151
+ )
1152
+ ).and.contains("path: ./large_block.js");
1153
+ (
1154
+ await expectFileExists(
1155
+ testDir,
1156
+ "apps/test_app/pages/test_page/apis/test_api/large_block.js",
1157
+ )
1158
+ ).and.includes(LARGE_JS_CONTENT);
1159
+ // And the old file no longer exists
1160
+ await expectNoFileExists(
1161
+ testDir,
1162
+ "apps/test_app/pages/test_page/apis/test_api.yaml",
1163
+ );
1164
+ await expectNoFileExists(
1165
+ testDir,
1166
+ "apps/test_app/pages/test_page/apis/test_api/test_api.yaml",
1167
+ );
1168
+ // and can be read back in
1169
+ (
1170
+ await readApiFile(
1171
+ `${testDir}/apps/test_app/pages/test_page/apis`,
1172
+ "Test API",
1173
+ )
1174
+ ).to.deep.equal({
1175
+ api: testApi.apiPb,
1176
+ stepPathMap: {
1177
+ "Large Block": "./large_block.js",
1178
+ },
1179
+ });
1180
+ // and a push operation would consider it valid
1181
+ expect(await validateLocalResource(testDir, resourceConfig)).to.be
1182
+ .undefined;
1183
+ }
1184
+
1185
+ // Write out the same multi-page app again, but this time with shorter code that should be extracted
1186
+ {
1187
+ // Change the JS code to be shorter
1188
+ if (testApi.apiPb?.blocks?.[0]?.step?.javascript) {
1189
+ testApi.apiPb.blocks[0].step.javascript.body = SMALL_JS_CONTENT;
1190
+ }
1191
+ const resourceConfig = await writeApplicationToDisk({
1192
+ sdk: {} as any,
1193
+ resource: multiPageApp,
1194
+ projectRootFolder: testDir,
1195
+ featureFlags: mockFeatureFlags,
1196
+ migrateFromSinglePage: false,
1197
+ preferredApiRepresentation: EXTRACT_SOURCE_CODE,
1198
+ });
1199
+ await Promise.all(apiPromises);
1200
+
1201
+ // Verify the superblocks.json file is correct for a multipage app that does extract source files
1202
+ const config = (await expectSuperblocksJsonFileExists(
1203
+ `${testDir}/apps/test_app/`,
1204
+ )) as SuperblocksApplicationConfig;
1205
+ const page = config.pages?.[multiPageApp.pages[0].id];
1206
+ expect(page?.apis).to.be.undefined;
1207
+ expect(page?.pageApis?.[testApi.id]?.sourceFiles).to.deep.equal([
1208
+ "large_block.js",
1209
+ ]);
1210
+
1211
+ // Verify the files exist and match expectations
1212
+ expect(resourceConfig.location).to.equal("apps/test_app");
1213
+ await expectFileExists(testDir, "apps/test_app/application.yaml");
1214
+ await expectFileExists(
1215
+ testDir,
1216
+ "apps/test_app/pages/test_page/page.yaml",
1217
+ );
1218
+ (
1219
+ await expectFileExists(
1220
+ testDir,
1221
+ "apps/test_app/pages/test_page/apis/test_api/api.yaml",
1222
+ )
1223
+ ).and.contains("path: ./large_block.js");
1224
+ // And does have the source code extracted
1225
+ (
1226
+ await expectFileExists(
1227
+ testDir,
1228
+ "apps/test_app/pages/test_page/apis/test_api/large_block.js",
1229
+ )
1230
+ ).and.includes(SMALL_JS_CONTENT);
1231
+ // And does not have the old file
1232
+ await expectNoFileExists(
1233
+ testDir,
1234
+ "apps/test_app/pages/test_page/apis/test_api.yaml",
1235
+ );
1236
+ // and can be read back in
1237
+ (
1238
+ await readApiFile(
1239
+ `${testDir}/apps/test_app/pages/test_page/apis`,
1240
+ "Test API",
1241
+ )
1242
+ ).to.deep.equal({
1243
+ api: testApi.apiPb,
1244
+ stepPathMap: {
1245
+ "Large Block": "./large_block.js",
1246
+ },
1247
+ });
1248
+ // and a push operation would consider it valid
1249
+ expect(await validateLocalResource(testDir, resourceConfig)).to.be
1250
+ .undefined;
1251
+ }
1252
+
1253
+ // Write out the same multi-page app again, again with large code so files are extracted
1254
+ {
1255
+ // Change the JS code to be shorter so no files are extracted
1256
+ if (testApi.apiPb?.blocks?.[0]?.step?.javascript) {
1257
+ testApi.apiPb.blocks[0].step.javascript.body = LARGE_JS_CONTENT;
1258
+ }
1259
+ const resourceConfig = await writeApplicationToDisk({
1260
+ sdk: {} as any,
1261
+ resource: multiPageApp,
1262
+ projectRootFolder: testDir,
1263
+ featureFlags: mockFeatureFlags,
1264
+ migrateFromSinglePage: false,
1265
+ preferredApiRepresentation: EXTRACT_SOURCE_CODE,
1266
+ });
1267
+ await Promise.all(apiPromises);
1268
+
1269
+ // Verify the superblocks.json file is correct for a multipage app that does extract source files
1270
+ const config = (await expectSuperblocksJsonFileExists(
1271
+ `${testDir}/apps/test_app/`,
1272
+ )) as SuperblocksApplicationConfig;
1273
+ const page = config.pages?.[multiPageApp.pages[0].id];
1274
+ expect(page?.apis).to.be.undefined;
1275
+ expect(page?.pageApis?.[testApi.id]?.sourceFiles).to.deep.equal([
1276
+ "large_block.js",
1277
+ ]);
1278
+
1279
+ // Verify the files exist and match expectations
1280
+ expect(resourceConfig.location).to.equal("apps/test_app");
1281
+ await expectFileExists(testDir, "apps/test_app/application.yaml");
1282
+ await expectFileExists(
1283
+ testDir,
1284
+ "apps/test_app/pages/test_page/page.yaml",
1285
+ );
1286
+ (
1287
+ await expectFileExists(
1288
+ testDir,
1289
+ "apps/test_app/pages/test_page/apis/test_api/api.yaml",
1290
+ )
1291
+ ).and.contains("path: ./large_block.js");
1292
+ (
1293
+ await expectFileExists(
1294
+ testDir,
1295
+ "apps/test_app/pages/test_page/apis/test_api/large_block.js",
1296
+ )
1297
+ ).and.includes(LARGE_JS_CONTENT);
1298
+ // And the old file no longer exists
1299
+ await expectNoFileExists(
1300
+ testDir,
1301
+ "apps/test_app/pages/test_page/apis/test_api.yaml",
1302
+ );
1303
+ // and the file with the wrong name does not exist
1304
+ await expectNoFileExists(
1305
+ testDir,
1306
+ "apps/test_app/pages/test_page/apis/test_api/test_api.yaml",
1307
+ );
1308
+ // and can be read back in
1309
+ (
1310
+ await readApiFile(
1311
+ `${testDir}/apps/test_app/pages/test_page/apis`,
1312
+ "Test API",
1313
+ )
1314
+ ).to.deep.equal({
1315
+ api: testApi.apiPb,
1316
+ stepPathMap: {
1317
+ "Large Block": "./large_block.js",
1318
+ },
1319
+ });
1320
+ // and a push operation would consider it valid
1321
+ expect(await validateLocalResource(testDir, resourceConfig)).to.be
1322
+ .undefined;
1323
+ }
1324
+
1325
+ // Write out the multi-page app again without extracting any code blocks
1326
+ {
1327
+ const resourceConfig = await writeApplicationToDisk({
1328
+ sdk: {} as any,
1329
+ resource: multiPageApp,
1330
+ projectRootFolder: testDir,
1331
+ featureFlags: mockFeatureFlags,
1332
+ migrateFromSinglePage: false,
1333
+ preferredApiRepresentation: NO_EXTRACT_SOURCE_CODE,
1334
+ });
1335
+ await Promise.all(apiPromises);
1336
+
1337
+ // Compute the expected YAML file content with embedded source code
1338
+ const expectedYamlContent = ymlstringify(testApi.apiPb, {
1339
+ sortMapEntries: true,
1340
+ blockQuote: "literal",
1341
+ });
1342
+
1343
+ // Verify the files exist and match expectations
1344
+ expect(resourceConfig.location).to.equal("apps/test_app");
1345
+ await expectFileExists(testDir, "apps/test_app/application.yaml");
1346
+ await expectFileExists(
1347
+ testDir,
1348
+ "apps/test_app/pages/test_page/page.yaml",
1349
+ );
1350
+ (
1351
+ await expectFileExists(
1352
+ testDir,
1353
+ "apps/test_app/pages/test_page/apis/test_api.yaml",
1354
+ )
1355
+ ).and.contains(expectedYamlContent);
1356
+ // And does not have the source code extracted
1357
+ await expectNoFileExists(
1358
+ testDir,
1359
+ "apps/test_app/pages/test_page/apis/test_api/large_block.js",
1360
+ );
1361
+ // and can be read back in
1362
+ (
1363
+ await readApiFile(
1364
+ `${testDir}/apps/test_app/pages/test_page/apis`,
1365
+ "Test API",
1366
+ )
1367
+ ).to.deep.equal({
1368
+ api: testApi.apiPb,
1369
+ stepPathMap: {},
1370
+ });
1371
+ }
1372
+ });
1373
+ });
1374
+ });
1375
+
1376
+ async function expectFileExists(
1377
+ testDir: string,
1378
+ relativePath: string,
1379
+ ): Promise<Chai.Assertion> {
1380
+ const filePath = path.join(testDir, relativePath);
1381
+ await expect(await fs.pathExists(filePath)).to.be.true;
1382
+ const content = await fs.readFile(filePath, "utf8");
1383
+ return expect(content);
1384
+ }
1385
+
1386
+ async function expectNoFileExists(testDir: string, relativePath: string) {
1387
+ const filePath = path.join(testDir, relativePath);
1388
+ expect(await fs.pathExists(filePath)).to.be.false;
1389
+ }
1390
+
1391
+ async function readApiFile(
1392
+ testDir: string,
1393
+ apiName?: string,
1394
+ ): Promise<Chai.Assertion> {
1395
+ const api = await readAppApiYamlFile(testDir, apiName);
1396
+ return expect(api);
1397
+ }
1398
+
1399
+ async function expectSuperblocksJsonFileExists(
1400
+ testDir: string,
1401
+ ): Promise<SuperblocksConfig> {
1402
+ const filePath = path.join(testDir, ".superblocks/superblocks.json");
1403
+ await expect(await fs.pathExists(filePath)).to.be.true;
1404
+ const content = await fs.readFile(filePath, "utf8");
1405
+ return JSON.parse(content) as SuperblocksConfig;
1406
+ }
1407
+
1408
+ async function removeExistingFiles(existingFilePaths: Set<string>) {
1409
+ for (const filePath of existingFilePaths) {
1410
+ await fs.remove(filePath);
1411
+ }
1412
+ }