@lowdefy/build 0.0.0-experimental-20260114142524 → 0.0.0-experimental-20260122121633

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 (58) hide show
  1. package/dist/build/addKeys.js +31 -18
  2. package/dist/build/buildApi/validateStepReferences.js +3 -4
  3. package/dist/build/buildAuth/buildAuth.js +2 -1
  4. package/dist/build/buildAuth/buildAuthPlugins.js +13 -13
  5. package/dist/build/buildAuth/validateAuthConfig.js +40 -8
  6. package/dist/build/buildAuth/validateMutualExclusivity.js +7 -7
  7. package/dist/build/buildConnections.js +20 -39
  8. package/dist/build/buildMenu.js +9 -14
  9. package/dist/build/buildPages/buildBlock/buildEvents.js +13 -13
  10. package/dist/build/buildPages/buildBlock/buildRequests.js +15 -15
  11. package/dist/build/buildPages/buildBlock/validateBlock.js +13 -13
  12. package/dist/build/buildPages/buildPage.js +5 -5
  13. package/dist/build/buildPages/validateLinkReferences.js +8 -9
  14. package/dist/build/buildPages/validatePayloadReferences.js +3 -4
  15. package/dist/build/buildPages/validateRequestReferences.js +7 -8
  16. package/dist/build/buildPages/validateStateReferences.js +3 -4
  17. package/dist/build/buildRefs/buildRefs.js +8 -0
  18. package/dist/build/buildRefs/evaluateBuildOperators.js +11 -1
  19. package/dist/build/buildRefs/evaluateStaticOperators.js +54 -0
  20. package/dist/build/buildRefs/getConfigFile.js +18 -13
  21. package/dist/build/buildRefs/getRefContent.js +15 -5
  22. package/dist/build/buildRefs/makeRefDefinition.js +1 -1
  23. package/dist/build/buildRefs/parseRefContent.js +32 -2
  24. package/dist/build/buildRefs/populateRefs.js +43 -5
  25. package/dist/build/buildRefs/recursiveBuild.js +9 -7
  26. package/dist/build/buildRefs/runRefResolver.js +13 -2
  27. package/dist/build/buildTypes.js +9 -0
  28. package/dist/build/collectDynamicIdentifiers.js +35 -0
  29. package/dist/{utils/formatConfigError.js → build/collectTypeNames.js} +20 -8
  30. package/dist/build/formatBuildError.js +1 -1
  31. package/dist/build/testSchema.js +45 -6
  32. package/dist/build/validateOperatorsDynamic.js +28 -0
  33. package/dist/build/writeRequests.js +3 -3
  34. package/dist/createContext.js +42 -1
  35. package/dist/defaultTypesMap.js +403 -403
  36. package/dist/index.js +43 -4
  37. package/dist/lowdefySchema.js +60 -0
  38. package/dist/test-utils/parseTestYaml.js +110 -0
  39. package/dist/test-utils/runBuild.js +270 -0
  40. package/dist/test-utils/runBuildForSnapshots.js +698 -0
  41. package/dist/{test → test-utils}/testContext.js +15 -1
  42. package/dist/utils/collectConfigError.js +6 -6
  43. package/dist/utils/countOperators.js +5 -3
  44. package/dist/utils/createCheckDuplicateId.js +1 -1
  45. package/dist/utils/findConfigKey.js +37 -0
  46. package/dist/utils/makeId.js +12 -7
  47. package/dist/utils/tryBuildStep.js +12 -5
  48. package/package.json +39 -39
  49. package/dist/utils/formatConfigMessage.js +0 -33
  50. package/dist/utils/formatConfigWarning.js +0 -24
  51. package/dist/utils/formatErrorMessage.js +0 -56
  52. /package/dist/{test → test-utils}/buildRefs/testBuildRefsAsyncFunction.js +0 -0
  53. /package/dist/{test → test-utils}/buildRefs/testBuildRefsErrorResolver.js +0 -0
  54. /package/dist/{test → test-utils}/buildRefs/testBuildRefsNullResolver.js +0 -0
  55. /package/dist/{test → test-utils}/buildRefs/testBuildRefsParsingResolver.js +0 -0
  56. /package/dist/{test → test-utils}/buildRefs/testBuildRefsResolver.js +0 -0
  57. /package/dist/{test → test-utils}/buildRefs/testBuildRefsTransform.js +0 -0
  58. /package/dist/{test → test-utils}/buildRefs/testBuildRefsTransformIdentity.js +0 -0
package/dist/index.js CHANGED
@@ -12,8 +12,10 @@
12
12
  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
- */ import createContext from './createContext.js';
15
+ */ import { ConfigError } from '@lowdefy/node-utils';
16
+ import createContext from './createContext.js';
16
17
  import createPluginTypesMap from './utils/createPluginTypesMap.js';
18
+ import makeId from './utils/makeId.js';
17
19
  import tryBuildStep from './utils/tryBuildStep.js';
18
20
  import addDefaultPages from './build/addDefaultPages/addDefaultPages.js';
19
21
  import addKeys from './build/addKeys.js';
@@ -48,15 +50,44 @@ import writePluginImports from './build/writePluginImports/writePluginImports.js
48
50
  import writeRequests from './build/writeRequests.js';
49
51
  import writeTypes from './build/writeTypes.js';
50
52
  async function build(options) {
53
+ // Reset makeId counter for each build (dev server may run multiple builds)
54
+ makeId.reset();
51
55
  const context = createContext(options);
52
- const components = await buildRefs({
56
+ let components;
57
+ try {
58
+ components = await buildRefs({
59
+ context
60
+ });
61
+ } catch (err) {
62
+ // Handle ConfigError from buildRefs (e.g., missing _ref files)
63
+ if (err instanceof ConfigError) {
64
+ context.logger.error(err.message);
65
+ const error = new Error('Build failed with 1 error(s). See above for details.');
66
+ error.isFormatted = true;
67
+ error.hideStack = true;
68
+ throw error;
69
+ }
70
+ throw err;
71
+ }
72
+ // Build steps - collect all errors before stopping
73
+ // addKeys runs first so testSchema has ~k markers for error location info
74
+ tryBuildStep(addKeys, 'addKeys', {
75
+ components,
53
76
  context
54
77
  });
55
- // Build steps - collect all errors before stopping
56
78
  tryBuildStep(testSchema, 'testSchema', {
57
79
  components,
58
80
  context
59
81
  });
82
+ // Schema errors mean structurally invalid data - stop before processing further
83
+ if (context.errors.length > 0) {
84
+ // Log all errors together before summary to ensure proper ordering
85
+ context.errors.forEach((errorMsg)=>context.logger.error(errorMsg));
86
+ const error = new Error(`Build failed with ${context.errors.length} error(s).`);
87
+ error.isFormatted = true;
88
+ error.hideStack = true;
89
+ throw error;
90
+ }
60
91
  tryBuildStep(buildApp, 'buildApp', {
61
92
  components,
62
93
  context
@@ -73,6 +104,7 @@ async function build(options) {
73
104
  components,
74
105
  context
75
106
  });
107
+ // addKeys runs again to add keys to any new objects created by earlier build steps
76
108
  tryBuildStep(addKeys, 'addKeys', {
77
109
  components,
78
110
  context
@@ -109,9 +141,16 @@ async function build(options) {
109
141
  components,
110
142
  context
111
143
  });
144
+ // Final addKeys pass to ensure all objects (including those created by build steps) have ~k
145
+ tryBuildStep(addKeys, 'addKeys', {
146
+ components,
147
+ context
148
+ });
112
149
  // Check if there are any collected errors before writing
113
150
  if (context.errors.length > 0) {
114
- const error = new Error(`Build failed with ${context.errors.length} error(s). See above for details.`);
151
+ // Log all errors together before summary to ensure proper ordering
152
+ context.errors.forEach((errorMsg)=>context.logger.error(errorMsg));
153
+ const error = new Error(`Build failed with ${context.errors.length} error(s).`);
115
154
  // Mark this error as already formatted so stack trace isn't shown
116
155
  error.isFormatted = true;
117
156
  error.hideStack = true;
@@ -12,6 +12,9 @@ export default {
12
12
  'type'
13
13
  ],
14
14
  properties: {
15
+ '~ignoreBuildCheck': {},
16
+ '~r': {},
17
+ '~l': {},
15
18
  async: {
16
19
  type: 'boolean',
17
20
  errorMessage: {
@@ -46,6 +49,9 @@ export default {
46
49
  type: 'object',
47
50
  additionalProperties: false,
48
51
  properties: {
52
+ '~ignoreBuildCheck': {},
53
+ '~r': {},
54
+ '~l': {},
49
55
  html: {
50
56
  type: 'object',
51
57
  errorMessage: {
@@ -75,6 +81,9 @@ export default {
75
81
  type: 'App "auth" should be an object.'
76
82
  },
77
83
  properties: {
84
+ '~ignoreBuildCheck': {},
85
+ '~r': {},
86
+ '~l': {},
78
87
  advanced: {
79
88
  type: 'object',
80
89
  properties: {
@@ -121,6 +130,9 @@ export default {
121
130
  type: 'App "config.auth.api" should be an object.'
122
131
  },
123
132
  properties: {
133
+ '~ignoreBuildCheck': {},
134
+ '~r': {},
135
+ '~l': {},
124
136
  protected: {
125
137
  type: [
126
138
  'array',
@@ -176,6 +188,9 @@ export default {
176
188
  type: 'object',
177
189
  additionalProperties: false,
178
190
  properties: {
191
+ '~ignoreBuildCheck': {},
192
+ '~r': {},
193
+ '~l': {},
179
194
  signIn: {
180
195
  type: 'string',
181
196
  default: '/auth/signin'
@@ -282,6 +297,9 @@ export default {
282
297
  type: 'App "config.auth.pages" should be an object.'
283
298
  },
284
299
  properties: {
300
+ '~ignoreBuildCheck': {},
301
+ '~r': {},
302
+ '~l': {},
285
303
  protected: {
286
304
  type: [
287
305
  'array',
@@ -386,6 +404,9 @@ export default {
386
404
  'type'
387
405
  ],
388
406
  properties: {
407
+ '~ignoreBuildCheck': {},
408
+ '~r': {},
409
+ '~l': {},
389
410
  id: {
390
411
  type: 'string',
391
412
  errorMessage: {
@@ -473,6 +494,9 @@ export default {
473
494
  type: 'object',
474
495
  additionalProperties: false,
475
496
  properties: {
497
+ '~ignoreBuildCheck': {},
498
+ '~r': {},
499
+ '~l': {},
476
500
  try: {
477
501
  type: 'array',
478
502
  items: {
@@ -489,6 +513,9 @@ export default {
489
513
  type: 'object',
490
514
  additionalProperties: false,
491
515
  properties: {
516
+ '~ignoreBuildCheck': {},
517
+ '~r': {},
518
+ '~l': {},
492
519
  immediate: {
493
520
  type: 'boolean',
494
521
  errorMessage: {
@@ -554,6 +581,9 @@ export default {
554
581
  'type'
555
582
  ],
556
583
  properties: {
584
+ '~ignoreBuildCheck': {},
585
+ '~r': {},
586
+ '~l': {},
557
587
  id: {
558
588
  type: 'string',
559
589
  errorMessage: {
@@ -592,6 +622,9 @@ export default {
592
622
  'type'
593
623
  ],
594
624
  properties: {
625
+ '~ignoreBuildCheck': {},
626
+ '~r': {},
627
+ '~l': {},
595
628
  id: {
596
629
  type: 'string',
597
630
  errorMessage: {
@@ -626,6 +659,9 @@ export default {
626
659
  'id'
627
660
  ],
628
661
  properties: {
662
+ '~ignoreBuildCheck': {},
663
+ '~r': {},
664
+ '~l': {},
629
665
  id: {
630
666
  type: 'string',
631
667
  errorMessage: {
@@ -663,6 +699,9 @@ export default {
663
699
  'type'
664
700
  ],
665
701
  properties: {
702
+ '~ignoreBuildCheck': {},
703
+ '~r': {},
704
+ '~l': {},
666
705
  id: {
667
706
  type: 'string',
668
707
  errorMessage: {
@@ -717,6 +756,9 @@ export default {
717
756
  'type'
718
757
  ],
719
758
  properties: {
759
+ '~ignoreBuildCheck': {},
760
+ '~r': {},
761
+ '~l': {},
720
762
  id: {
721
763
  type: 'string',
722
764
  errorMessage: {
@@ -776,6 +818,9 @@ export default {
776
818
  'version'
777
819
  ],
778
820
  properties: {
821
+ '~ignoreBuildCheck': {},
822
+ '~r': {},
823
+ '~l': {},
779
824
  name: {
780
825
  type: 'string',
781
826
  errorMessage: {
@@ -812,6 +857,9 @@ export default {
812
857
  'connectionId'
813
858
  ],
814
859
  properties: {
860
+ '~ignoreBuildCheck': {},
861
+ '~r': {},
862
+ '~l': {},
815
863
  id: {
816
864
  type: 'string',
817
865
  errorMessage: {
@@ -858,6 +906,9 @@ export default {
858
906
  'lowdefy'
859
907
  ],
860
908
  properties: {
909
+ '~ignoreBuildCheck': {},
910
+ '~r': {},
911
+ '~l': {},
861
912
  name: {
862
913
  type: 'string',
863
914
  errorMessage: {
@@ -901,6 +952,9 @@ export default {
901
952
  },
902
953
  additionalProperties: false,
903
954
  properties: {
955
+ '~ignoreBuildCheck': {},
956
+ '~r': {},
957
+ '~l': {},
904
958
  basePath: {
905
959
  type: 'string',
906
960
  description: 'App base path to apply to all routes. Base path must start with "/".',
@@ -975,6 +1029,9 @@ export default {
975
1029
  type: 'App "logger" should be an object.'
976
1030
  },
977
1031
  properties: {
1032
+ '~ignoreBuildCheck': {},
1033
+ '~r': {},
1034
+ '~l': {},
978
1035
  sentry: {
979
1036
  type: 'object',
980
1037
  additionalProperties: false,
@@ -982,6 +1039,9 @@ export default {
982
1039
  type: 'App "logger.sentry" should be an object.'
983
1040
  },
984
1041
  properties: {
1042
+ '~ignoreBuildCheck': {},
1043
+ '~r': {},
1044
+ '~l': {},
985
1045
  client: {
986
1046
  type: 'boolean',
987
1047
  errorMessage: {
@@ -0,0 +1,110 @@
1
+ /*
2
+ Copyright 2020-2024 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { type } from '@lowdefy/helpers';
16
+ import YAML, { isMap, isSeq, isPair, isScalar } from 'yaml';
17
+ function getLineNumber(content, offset) {
18
+ if (offset == null || offset < 0) return null;
19
+ return content.substring(0, offset).split('\n').length;
20
+ }
21
+ function addLineNumbersAndRefs(node, content, refCounter) {
22
+ if (isMap(node)) {
23
+ const obj = {};
24
+ const refId = String(refCounter.next++);
25
+ obj['~r'] = refId;
26
+ if (node.range) {
27
+ Object.defineProperty(obj, '~l', {
28
+ value: getLineNumber(content, node.range[0]),
29
+ enumerable: false,
30
+ writable: true,
31
+ configurable: true
32
+ });
33
+ }
34
+ for (const pair of node.items){
35
+ if (isPair(pair) && isScalar(pair.key)) {
36
+ const key = pair.key.value;
37
+ const value = pair.value;
38
+ const keyLineNumber = pair.key.range ? getLineNumber(content, pair.key.range[0]) : null;
39
+ if (isMap(value)) {
40
+ const mapResult = addLineNumbersAndRefs(value, content, refCounter);
41
+ if (keyLineNumber) {
42
+ Object.defineProperty(mapResult, '~l', {
43
+ value: keyLineNumber,
44
+ enumerable: false,
45
+ writable: true,
46
+ configurable: true
47
+ });
48
+ }
49
+ obj[key] = mapResult;
50
+ } else if (isSeq(value)) {
51
+ // Don't add ~l to arrays - only objects need line numbers for error reporting
52
+ obj[key] = addLineNumbersAndRefs(value, content, refCounter);
53
+ } else if (isScalar(value)) {
54
+ obj[key] = value.value;
55
+ } else {
56
+ obj[key] = value?.toJSON?.() ?? value;
57
+ }
58
+ }
59
+ }
60
+ return obj;
61
+ }
62
+ if (isSeq(node)) {
63
+ const arr = [];
64
+ // Note: We don't add ~l to arrays to keep serialized output simpler.
65
+ // Line numbers on arrays aren't needed for error reporting - objects are the error sources.
66
+ for (const item of node.items){
67
+ if (isMap(item)) {
68
+ arr.push(addLineNumbersAndRefs(item, content, refCounter));
69
+ } else if (isSeq(item)) {
70
+ arr.push(addLineNumbersAndRefs(item, content, refCounter));
71
+ } else if (isScalar(item)) {
72
+ arr.push(item.value);
73
+ } else {
74
+ arr.push(item?.toJSON?.() ?? item);
75
+ }
76
+ }
77
+ return arr;
78
+ }
79
+ if (isScalar(node)) {
80
+ return node.value;
81
+ }
82
+ return node?.toJSON?.() ?? node;
83
+ }
84
+ /**
85
+ * Parse YAML string for testing, adding ~l (line numbers) and ~r (ref IDs)
86
+ * to simulate real buildRefs output.
87
+ *
88
+ * @param {string} yamlContent - YAML string to parse (use template literals for multiline)
89
+ * @returns {object} Parsed object with ~l and ~r properties
90
+ *
91
+ * @example
92
+ * const components = parseTestYaml(`
93
+ * pages:
94
+ * - id: home
95
+ * type: Box
96
+ * blocks:
97
+ * - id: title
98
+ * type: Title
99
+ * `);
100
+ */ function parseTestYaml(yamlContent) {
101
+ const doc = YAML.parseDocument(yamlContent);
102
+ if (doc.errors && doc.errors.length > 0) {
103
+ throw new Error(doc.errors[0].message);
104
+ }
105
+ const refCounter = {
106
+ next: 1
107
+ };
108
+ return addLineNumbersAndRefs(doc.contents, yamlContent, refCounter);
109
+ }
110
+ export default parseTestYaml;
@@ -0,0 +1,270 @@
1
+ /*
2
+ Copyright 2020-2024 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */ import { jest } from '@jest/globals';
16
+ import path from 'path';
17
+ /**
18
+ * Custom types map for testing - includes common types used in fixtures.
19
+ * Contains all basic blocks, loaders, common actions, operators, and connections.
20
+ */ const testTypesMap = {
21
+ actions: {
22
+ Link: {
23
+ package: '@lowdefy/actions-core'
24
+ },
25
+ SetState: {
26
+ package: '@lowdefy/actions-core'
27
+ },
28
+ Request: {
29
+ package: '@lowdefy/actions-core'
30
+ },
31
+ Reset: {
32
+ package: '@lowdefy/actions-core'
33
+ },
34
+ Throw: {
35
+ package: '@lowdefy/actions-core'
36
+ },
37
+ Log: {
38
+ package: '@lowdefy/actions-core'
39
+ },
40
+ Return: {
41
+ package: '@lowdefy/api'
42
+ }
43
+ },
44
+ blocks: {
45
+ Anchor: {
46
+ package: '@lowdefy/blocks-basic'
47
+ },
48
+ Box: {
49
+ package: '@lowdefy/blocks-basic'
50
+ },
51
+ Button: {
52
+ package: '@lowdefy/blocks-basic'
53
+ },
54
+ DangerousHtml: {
55
+ package: '@lowdefy/blocks-basic'
56
+ },
57
+ Html: {
58
+ package: '@lowdefy/blocks-basic'
59
+ },
60
+ Icon: {
61
+ package: '@lowdefy/blocks-basic'
62
+ },
63
+ Img: {
64
+ package: '@lowdefy/blocks-basic'
65
+ },
66
+ List: {
67
+ package: '@lowdefy/blocks-basic'
68
+ },
69
+ Message: {
70
+ package: '@lowdefy/blocks-antd'
71
+ },
72
+ Paragraph: {
73
+ package: '@lowdefy/blocks-basic'
74
+ },
75
+ ProgressBar: {
76
+ package: '@lowdefy/blocks-loaders'
77
+ },
78
+ Result: {
79
+ package: '@lowdefy/blocks-antd'
80
+ },
81
+ Skeleton: {
82
+ package: '@lowdefy/blocks-loaders'
83
+ },
84
+ SkeletonAvatar: {
85
+ package: '@lowdefy/blocks-loaders'
86
+ },
87
+ SkeletonButton: {
88
+ package: '@lowdefy/blocks-loaders'
89
+ },
90
+ SkeletonInput: {
91
+ package: '@lowdefy/blocks-loaders'
92
+ },
93
+ SkeletonParagraph: {
94
+ package: '@lowdefy/blocks-loaders'
95
+ },
96
+ Span: {
97
+ package: '@lowdefy/blocks-basic'
98
+ },
99
+ Spinner: {
100
+ package: '@lowdefy/blocks-loaders'
101
+ },
102
+ TextInput: {
103
+ package: '@lowdefy/blocks-antd'
104
+ },
105
+ Throw: {
106
+ package: '@lowdefy/blocks-basic'
107
+ },
108
+ Title: {
109
+ package: '@lowdefy/blocks-basic'
110
+ }
111
+ },
112
+ connections: {
113
+ AxiosHttp: {
114
+ package: '@lowdefy/connection-axios-http'
115
+ },
116
+ MongoDBCollection: {
117
+ package: '@lowdefy/connection-mongodb'
118
+ }
119
+ },
120
+ requests: {
121
+ AxiosHttp: {
122
+ package: '@lowdefy/connection-axios-http'
123
+ },
124
+ MongoDBInsertOne: {
125
+ package: '@lowdefy/connection-mongodb'
126
+ }
127
+ },
128
+ auth: {
129
+ adapters: {},
130
+ callbacks: {},
131
+ events: {},
132
+ providers: {
133
+ GoogleProvider: {
134
+ package: 'next-auth'
135
+ }
136
+ }
137
+ },
138
+ operators: {
139
+ client: {
140
+ _state: {
141
+ package: '@lowdefy/operators-js'
142
+ },
143
+ _if: {
144
+ package: '@lowdefy/operators-js'
145
+ },
146
+ _eq: {
147
+ package: '@lowdefy/operators-js'
148
+ },
149
+ _not: {
150
+ package: '@lowdefy/operators-js'
151
+ },
152
+ _type: {
153
+ package: '@lowdefy/operators-js'
154
+ },
155
+ _payload: {
156
+ package: '@lowdefy/operators-js'
157
+ },
158
+ _step: {
159
+ package: '@lowdefy/operators-js'
160
+ }
161
+ },
162
+ server: {
163
+ _payload: {
164
+ package: '@lowdefy/operators-js'
165
+ },
166
+ _step: {
167
+ package: '@lowdefy/operators-js'
168
+ }
169
+ }
170
+ },
171
+ controls: {
172
+ Endpoint: {
173
+ package: '@lowdefy/api'
174
+ },
175
+ Log: {
176
+ package: '@lowdefy/api'
177
+ },
178
+ Return: {
179
+ package: '@lowdefy/api'
180
+ }
181
+ }
182
+ };
183
+ /**
184
+ * Creates a runBuild helper function for testing build errors.
185
+ *
186
+ * @param {Function} build - The build function (imported after mocking writeBuildArtifact)
187
+ * @param {string} fixturesDir - Absolute path to the fixtures directory
188
+ * @returns {Function} runBuild helper function
189
+ *
190
+ * @example
191
+ * // In test file:
192
+ * jest.unstable_mockModule('./utils/writeBuildArtifact.js', () => ({
193
+ * default: () => jest.fn(),
194
+ * }));
195
+ * const { default: build } = await import('./index.js');
196
+ * const runBuild = createRunBuild(build, path.join(__dirname, 'build-errors'));
197
+ *
198
+ * // Then in tests:
199
+ * const result = await runBuild('A1-invalid-connection-type', 'prod');
200
+ * expect(result.errors).toContain('...');
201
+ */ function createRunBuild(build, fixturesDir) {
202
+ /**
203
+ * Runs build with a specific fixture directory and stage.
204
+ * Returns captured errors, warnings, and the thrown error (if any).
205
+ *
206
+ * @param {string} fixtureDir - Name of the fixture directory (e.g., 'A1-invalid-connection-type')
207
+ * @param {string} [stage='prod'] - Build stage ('dev' or 'prod')
208
+ * @returns {Promise<{errors: string[], warnings: string[], thrownError: Error|null, logger: Object}>}
209
+ */ return async function runBuild(fixtureDir, stage = 'prod') {
210
+ const configDir = path.join(fixturesDir, fixtureDir);
211
+ const errors = [];
212
+ const warnings = [];
213
+ const logger = {
214
+ info: jest.fn(),
215
+ log: jest.fn(),
216
+ warn: jest.fn((msg)=>warnings.push(msg)),
217
+ error: jest.fn((msg)=>errors.push(msg)),
218
+ succeed: jest.fn()
219
+ };
220
+ let thrownError = null;
221
+ try {
222
+ await build({
223
+ customTypesMap: testTypesMap,
224
+ directories: {
225
+ config: configDir,
226
+ build: path.join(configDir, '.lowdefy'),
227
+ server: path.join(configDir, '.lowdefy', 'server')
228
+ },
229
+ logger,
230
+ stage
231
+ });
232
+ } catch (err) {
233
+ thrownError = err;
234
+ // Extract errors embedded in the thrown message (format: "✖ [Config Error] ...")
235
+ // This handles the case where errors are bundled in the thrown message to avoid interleaving
236
+ // Each error is multi-line: message line + indented location lines
237
+ if (err.message) {
238
+ const lines = err.message.split('\n');
239
+ let currentError = null;
240
+ for (const line of lines){
241
+ if (line.startsWith('✖ [Config Error]')) {
242
+ // Start of a new error - save previous if exists
243
+ if (currentError !== null) {
244
+ errors.push(currentError);
245
+ }
246
+ currentError = line.slice(2); // Remove "✖ " prefix
247
+ } else if (currentError !== null && line.startsWith(' ')) {
248
+ // Continuation line (indented) - append to current error
249
+ currentError += '\n' + line;
250
+ } else if (currentError !== null) {
251
+ // Non-continuation line - save current error and reset
252
+ errors.push(currentError);
253
+ currentError = null;
254
+ }
255
+ }
256
+ // Don't forget the last error
257
+ if (currentError !== null) {
258
+ errors.push(currentError);
259
+ }
260
+ }
261
+ }
262
+ return {
263
+ errors,
264
+ warnings,
265
+ thrownError,
266
+ logger
267
+ };
268
+ };
269
+ }
270
+ export { testTypesMap, createRunBuild };