@lowdefy/build 4.6.0 → 4.7.1

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.
@@ -12,36 +12,7 @@
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
- */ const iconPackages = {
16
- 'react-icons/ai': /"(Ai[A-Z0-9]\w*)"/gm,
17
- 'react-icons/bi': /"(Bi[A-Z0-9]\w*)"/gm,
18
- 'react-icons/bs': /"(Bs[A-Z0-9]\w*)"/gm,
19
- 'react-icons/cg': /"(Cg[A-Z0-9]\w*)"/gm,
20
- 'react-icons/ci': /"(Ci[A-Z0-9]\w*)"/gm,
21
- 'react-icons/di': /"(Di[A-Z0-9]\w*)"/gm,
22
- 'react-icons/fa': /"(Fa[A-Z0-9]\w*)"/gm,
23
- 'react-icons/fc': /"(Fc[A-Z0-9]\w*)"/gm,
24
- 'react-icons/fi': /"(Fi[A-Z0-9]\w*)"/gm,
25
- 'react-icons/gi': /"(Gi[A-Z0-9]\w*)"/gm,
26
- 'react-icons/go': /"(Go[A-Z0-9]\w*)"/gm,
27
- 'react-icons/gr': /"(Gr[A-Z0-9]\w*)"/gm,
28
- 'react-icons/hi': /"(Hi[A-Z0-9]\w*)"/gm,
29
- 'react-icons/im': /"(Im[A-Z0-9]\w*)"/gm,
30
- 'react-icons/io': /"(IoIos[A-Z0-9]\w*)"/gm,
31
- 'react-icons/io5': /"(Io[A-Z0-9]\w*)"/gm,
32
- 'react-icons/lu': /"(Lu[A-Z0-9]\w*)"/gm,
33
- 'react-icons/md': /"(Md[A-Z0-9]\w*)"/gm,
34
- 'react-icons/pi': /"(Pi[A-Z0-9]\w*)"/gm,
35
- 'react-icons/ri': /"(Ri[A-Z0-9]\w*)"/gm,
36
- 'react-icons/rx': /"(Rx[A-Z0-9]\w*)"/gm,
37
- 'react-icons/si': /"(Si[A-Z0-9]\w*)"/gm,
38
- 'react-icons/sl': /"(Sl[A-Z0-9]\w*)"/gm,
39
- 'react-icons/tb': /"(Tb[A-Z0-9]\w*)"/gm,
40
- 'react-icons/tfi': /"(Tfi[A-Z0-9]\w*)"/gm,
41
- 'react-icons/ti': /"(Ti[A-Z0-9]\w*)"/gm,
42
- 'react-icons/vsc': /"(Vsc[A-Z0-9]\w*)"/gm,
43
- 'react-icons/wi': /"(Wi[A-Z0-9]\w*)"/gm
44
- };
15
+ */ import iconPackages from './iconPackages.js';
45
16
  function getConfigIcons({ components, icons, regex }) {
46
17
  [
47
18
  ...JSON.stringify(components.global || {}).matchAll(regex)
@@ -65,7 +36,6 @@ function getBlockDefaultIcons({ blocks, context, icons, regex }) {
65
36
  function buildIconImports({ blocks, components, context, defaults = {} }) {
66
37
  const iconImports = [];
67
38
  Object.entries(iconPackages).forEach(([iconPackage, regex])=>{
68
- defaults;
69
39
  const icons = new Set(defaults[iconPackage]);
70
40
  getConfigIcons({
71
41
  components,
@@ -0,0 +1,49 @@
1
+ /*
2
+ Copyright 2020-2026 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
+ */ // Icon name regex patterns keyed by react-icons package.
16
+ // Note: 'react-icons/io' and 'react-icons/io5' overlap — IoIos* icons match
17
+ // both packages. This is intentional and matches the existing buildIconImports
18
+ // behavior where both packages receive the IoIos* icons.
19
+ const iconPackages = {
20
+ 'react-icons/ai': /"(Ai[A-Z0-9]\w*)"/gm,
21
+ 'react-icons/bi': /"(Bi[A-Z0-9]\w*)"/gm,
22
+ 'react-icons/bs': /"(Bs[A-Z0-9]\w*)"/gm,
23
+ 'react-icons/cg': /"(Cg[A-Z0-9]\w*)"/gm,
24
+ 'react-icons/ci': /"(Ci[A-Z0-9]\w*)"/gm,
25
+ 'react-icons/di': /"(Di[A-Z0-9]\w*)"/gm,
26
+ 'react-icons/fa': /"(Fa[A-Z0-9]\w*)"/gm,
27
+ 'react-icons/fc': /"(Fc[A-Z0-9]\w*)"/gm,
28
+ 'react-icons/fi': /"(Fi[A-Z0-9]\w*)"/gm,
29
+ 'react-icons/gi': /"(Gi[A-Z0-9]\w*)"/gm,
30
+ 'react-icons/go': /"(Go[A-Z0-9]\w*)"/gm,
31
+ 'react-icons/gr': /"(Gr[A-Z0-9]\w*)"/gm,
32
+ 'react-icons/hi': /"(Hi[A-Z0-9]\w*)"/gm,
33
+ 'react-icons/im': /"(Im[A-Z0-9]\w*)"/gm,
34
+ 'react-icons/io': /"(IoIos[A-Z0-9]\w*)"/gm,
35
+ 'react-icons/io5': /"(Io[A-Z0-9]\w*)"/gm,
36
+ 'react-icons/lu': /"(Lu[A-Z0-9]\w*)"/gm,
37
+ 'react-icons/md': /"(Md[A-Z0-9]\w*)"/gm,
38
+ 'react-icons/pi': /"(Pi[A-Z0-9]\w*)"/gm,
39
+ 'react-icons/ri': /"(Ri[A-Z0-9]\w*)"/gm,
40
+ 'react-icons/rx': /"(Rx[A-Z0-9]\w*)"/gm,
41
+ 'react-icons/si': /"(Si[A-Z0-9]\w*)"/gm,
42
+ 'react-icons/sl': /"(Sl[A-Z0-9]\w*)"/gm,
43
+ 'react-icons/tb': /"(Tb[A-Z0-9]\w*)"/gm,
44
+ 'react-icons/tfi': /"(Tfi[A-Z0-9]\w*)"/gm,
45
+ 'react-icons/ti': /"(Ti[A-Z0-9]\w*)"/gm,
46
+ 'react-icons/vsc': /"(Vsc[A-Z0-9]\w*)"/gm,
47
+ 'react-icons/wi': /"(Wi[A-Z0-9]\w*)"/gm
48
+ };
49
+ export default iconPackages;
@@ -12,33 +12,49 @@
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 recursiveBuild from './recursiveBuild.js';
15
+ */ import operators from '@lowdefy/operators-js/operators/build';
16
+ import { resolve, WalkContext } from './walker.js';
17
+ import getRefContent from './getRefContent.js';
16
18
  import makeRefDefinition from './makeRefDefinition.js';
17
- import evaluateBuildOperators from './evaluateBuildOperators.js';
18
19
  import evaluateStaticOperators from './evaluateStaticOperators.js';
19
- import collectTypeNames from '../collectTypeNames.js';
20
+ import collectDynamicIdentifiers from '../collectDynamicIdentifiers.js';
21
+ import validateOperatorsDynamic from '../validateOperatorsDynamic.js';
22
+ import isPageContentPath from '../jit/isPageContentPath.js';
23
+ // Validate and collect dynamic identifiers once at module load
24
+ validateOperatorsDynamic({
25
+ operators
26
+ });
27
+ const dynamicIdentifiers = collectDynamicIdentifiers({
28
+ operators
29
+ });
20
30
  async function buildRefs({ context, shallowOptions }) {
31
+ context.unresolvedRefVars = context.unresolvedRefVars ?? {};
21
32
  const refDef = makeRefDefinition('lowdefy.yaml', null, context.refMap);
22
- let components = await recursiveBuild({
23
- context,
24
- refDef,
25
- count: 0,
26
- shallowOptions
33
+ const ctx = new WalkContext({
34
+ buildContext: context,
35
+ refId: refDef.id,
36
+ sourceRefId: null,
37
+ vars: {},
38
+ path: '',
39
+ currentFile: refDef.path,
40
+ refChain: new Set(refDef.path ? [
41
+ refDef.path
42
+ ] : []),
43
+ operators,
44
+ env: process.env,
45
+ dynamicIdentifiers,
46
+ shouldStop: shallowOptions ? // JIT can re-resolve them from source files. Inline pages (defined
47
+ // directly in lowdefy.yaml) live in the root ref and have no separate
48
+ // source file — their content must be preserved for buildShallowPages.
49
+ (path, refId)=>isPageContentPath(path) && refId !== refDef.id : null
27
50
  });
28
- // First: evaluate _build.* operators (e.g., _build.env)
29
- // Pass typeNames so page objects act as type boundaries, preventing ~dyn markers
30
- // from ~shallow content (blocks, events) from bubbling up and blocking evaluation
31
- // of _build.array at the pages level.
32
- const typeNames = collectTypeNames({
33
- typesMap: context.typesMap
34
- });
35
- components = await evaluateBuildOperators({
51
+ const content = await getRefContent({
36
52
  context,
37
- input: components,
38
53
  refDef,
39
- typeNames
54
+ referencedFrom: null
40
55
  });
41
- // Second: evaluate static operators (_sum, _if, etc.) that don't depend on runtime data
56
+ let components = await resolve(content, ctx);
57
+ // Evaluate static operators (_sum, _if, etc.) that don't depend on runtime data
42
58
  components = evaluateStaticOperators({
43
59
  context,
44
60
  input: components,
@@ -12,13 +12,12 @@
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 { BuildParser } from '@lowdefy/operators';
15
+ */ import { evaluateOperators } from '@lowdefy/operators';
16
16
  import operators from '@lowdefy/operators-js/operators/build';
17
17
  import collectDynamicIdentifiers from '../collectDynamicIdentifiers.js';
18
18
  import collectTypeNames from '../collectTypeNames.js';
19
19
  import validateOperatorsDynamic from '../validateOperatorsDynamic.js';
20
20
  import collectExceptions from '../../utils/collectExceptions.js';
21
- // Validate and collect dynamic identifiers once at module load
22
21
  validateOperatorsDynamic({
23
22
  operators
24
23
  });
@@ -26,20 +25,17 @@ const dynamicIdentifiers = collectDynamicIdentifiers({
26
25
  operators
27
26
  });
28
27
  function evaluateStaticOperators({ context, input, refDef }) {
29
- // Collect type names from context.typesMap for type boundary detection
30
28
  const typeNames = collectTypeNames({
31
29
  typesMap: context.typesMap
32
30
  });
33
- const operatorsParser = new BuildParser({
34
- env: process.env,
31
+ const { output, errors } = evaluateOperators({
32
+ input,
35
33
  operators,
34
+ operatorPrefix: '_',
35
+ env: process.env,
36
36
  dynamicIdentifiers,
37
37
  typeNames
38
38
  });
39
- const { output, errors } = operatorsParser.parse({
40
- input,
41
- operatorPrefix: '_'
42
- });
43
39
  if (errors.length > 0) {
44
40
  errors.forEach((error)=>{
45
41
  // Resolve source file path for error location.
@@ -15,8 +15,10 @@
15
15
  */ import { get } from '@lowdefy/helpers';
16
16
  import getRefPath from './getRefPath.js';
17
17
  import makeId from '../../utils/makeId.js';
18
- function makeRefDefinition(refDefinition, parent, refMap, lineNumber) {
19
- const id = makeId.next();
18
+ function makeRefDefinition(refDefinition, parent, refMap, lineNumber, walkerPath) {
19
+ // Use walker tree path when available for deterministic IDs under parallel
20
+ // resolution. Falls back to counter for root ref and JIT-created refs.
21
+ const id = walkerPath != null ? walkerPath : makeId.next();
20
22
  const refDef = {
21
23
  parent,
22
24
  lineNumber
@@ -92,7 +92,8 @@ function parseRefContent({ content, refDef }) {
92
92
  const { path, vars } = refDef;
93
93
  if (type.isString(path)) {
94
94
  let ext = getFileExtension(path);
95
- if (ext === 'njk') {
95
+ const isNjk = ext === 'njk';
96
+ if (isNjk) {
96
97
  try {
97
98
  content = parseNunjucks(content, vars);
98
99
  } catch (error) {
@@ -107,6 +108,12 @@ function parseRefContent({ content, refDef }) {
107
108
  try {
108
109
  content = parseYamlWithLineNumbers(content);
109
110
  } catch (error) {
111
+ if (isNjk) {
112
+ throw new ConfigError(`Nunjucks template "${path}" produced invalid YAML.`, {
113
+ cause: error,
114
+ filePath: path
115
+ });
116
+ }
110
117
  const lineMatch = error.message.match(/at line (\d+)/);
111
118
  throw new ConfigError(`YAML parse error in "${path}".`, {
112
119
  cause: error,
@@ -0,0 +1,348 @@
1
+ /*
2
+ Copyright 2020-2026 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 { get, type } from '@lowdefy/helpers';
16
+ import { ConfigError } from '@lowdefy/errors';
17
+ import { evaluateOperators } from '@lowdefy/operators';
18
+ import makeRefDefinition from './makeRefDefinition.js';
19
+ import getRefContent from './getRefContent.js';
20
+ import runTransformer from './runTransformer.js';
21
+ import getKey from './getKey.js';
22
+ import setNonEnumerableProperty from '../../utils/setNonEnumerableProperty.js';
23
+ import collectExceptions from '../../utils/collectExceptions.js';
24
+ let WalkContext = class WalkContext {
25
+ child(segment) {
26
+ return new WalkContext({
27
+ buildContext: this.buildContext,
28
+ refId: this.refId,
29
+ sourceRefId: this.sourceRefId,
30
+ vars: this.vars,
31
+ path: this.path ? `${this.path}.${segment}` : segment,
32
+ currentFile: this.currentFile,
33
+ refChain: this.refChain,
34
+ operators: this.operators,
35
+ env: this.env,
36
+ dynamicIdentifiers: this.dynamicIdentifiers,
37
+ shouldStop: this.shouldStop
38
+ });
39
+ }
40
+ forRef({ refId, vars, filePath }) {
41
+ const newChain = new Set(this.refChain);
42
+ if (filePath) {
43
+ newChain.add(filePath);
44
+ }
45
+ return new WalkContext({
46
+ buildContext: this.buildContext,
47
+ refId,
48
+ sourceRefId: this.refId,
49
+ vars: vars ?? {},
50
+ path: this.path,
51
+ currentFile: filePath ?? this.currentFile,
52
+ refChain: newChain,
53
+ operators: this.operators,
54
+ env: this.env,
55
+ dynamicIdentifiers: this.dynamicIdentifiers,
56
+ shouldStop: this.shouldStop
57
+ });
58
+ }
59
+ collectError(error) {
60
+ collectExceptions(this.buildContext, error);
61
+ }
62
+ get refMap() {
63
+ return this.buildContext.refMap;
64
+ }
65
+ get unresolvedRefVars() {
66
+ return this.buildContext.unresolvedRefVars;
67
+ }
68
+ constructor({ buildContext, refId, sourceRefId, vars, path, currentFile, refChain, operators, env, dynamicIdentifiers, shouldStop }){
69
+ this.buildContext = buildContext;
70
+ this.refId = refId;
71
+ this.sourceRefId = sourceRefId;
72
+ this.vars = vars;
73
+ this.path = path;
74
+ this.currentFile = currentFile;
75
+ this.refChain = refChain;
76
+ this.operators = operators;
77
+ this.env = env;
78
+ this.dynamicIdentifiers = dynamicIdentifiers;
79
+ this.shouldStop = shouldStop;
80
+ }
81
+ };
82
+ // Detect _build.* operator objects: single non-tilde key starting with '_build.'
83
+ function isBuildOperator(node) {
84
+ const keys = Object.keys(node);
85
+ const nonTildeKeys = keys.filter((k)=>!k.startsWith('~'));
86
+ return nonTildeKeys.length === 1 && nonTildeKeys[0].startsWith('_build.');
87
+ }
88
+ // Set ~r as non-enumerable if not already present
89
+ function tagRef(node, refId) {
90
+ if (type.isObject(node) || type.isArray(node)) {
91
+ if (node['~r'] === undefined) {
92
+ setNonEnumerableProperty(node, '~r', refId);
93
+ }
94
+ }
95
+ }
96
+ // Recursively set ~r on all objects/arrays that don't already have it
97
+ function tagRefDeep(node, refId) {
98
+ if (!type.isObject(node) && !type.isArray(node)) return;
99
+ if (node['~r'] !== undefined) return;
100
+ setNonEnumerableProperty(node, '~r', refId);
101
+ if (type.isArray(node)) {
102
+ for(let i = 0; i < node.length; i++){
103
+ tagRefDeep(node[i], refId);
104
+ }
105
+ } else {
106
+ for (const key of Object.keys(node)){
107
+ tagRefDeep(node[key], refId);
108
+ }
109
+ }
110
+ }
111
+ // Deep clone preserving ~r, ~l, ~k non-enumerable markers.
112
+ // Used before resolving ref def path/vars to prevent mutation of stored originals.
113
+ function cloneForResolve(value) {
114
+ if (!type.isObject(value) && !type.isArray(value)) return value;
115
+ if (type.isArray(value)) {
116
+ const clone = value.map((item)=>cloneForResolve(item));
117
+ if (value['~r'] !== undefined) setNonEnumerableProperty(clone, '~r', value['~r']);
118
+ if (value['~l'] !== undefined) setNonEnumerableProperty(clone, '~l', value['~l']);
119
+ if (value['~k'] !== undefined) setNonEnumerableProperty(clone, '~k', value['~k']);
120
+ if (value['~arr'] !== undefined) setNonEnumerableProperty(clone, '~arr', value['~arr']);
121
+ return clone;
122
+ }
123
+ const clone = {};
124
+ for (const key of Object.keys(value)){
125
+ clone[key] = cloneForResolve(value[key]);
126
+ }
127
+ if (value['~r'] !== undefined) setNonEnumerableProperty(clone, '~r', value['~r']);
128
+ if (value['~l'] !== undefined) setNonEnumerableProperty(clone, '~l', value['~l']);
129
+ if (value['~k'] !== undefined) setNonEnumerableProperty(clone, '~k', value['~k']);
130
+ return clone;
131
+ }
132
+ // Deep clone a var value, preserving markers and setting ~r provenance.
133
+ // When sourceRefId is null, preserves the template's existing ~r markers.
134
+ function cloneVarValue(value, sourceRefId) {
135
+ if (!type.isObject(value) && !type.isArray(value)) return value;
136
+ return cloneDeepWithProvenance(value, sourceRefId);
137
+ }
138
+ function cloneDeepWithProvenance(node, sourceRefId) {
139
+ if (!type.isObject(node) && !type.isArray(node)) return node;
140
+ if (type.isArray(node)) {
141
+ const clone = node.map((item)=>cloneDeepWithProvenance(item, sourceRefId));
142
+ if (node['~r'] !== undefined) {
143
+ setNonEnumerableProperty(clone, '~r', node['~r']);
144
+ } else if (sourceRefId) {
145
+ setNonEnumerableProperty(clone, '~r', sourceRefId);
146
+ }
147
+ if (node['~l'] !== undefined) setNonEnumerableProperty(clone, '~l', node['~l']);
148
+ if (node['~k'] !== undefined) setNonEnumerableProperty(clone, '~k', node['~k']);
149
+ if (node['~arr'] !== undefined) setNonEnumerableProperty(clone, '~arr', node['~arr']);
150
+ return clone;
151
+ }
152
+ const clone = {};
153
+ for (const key of Object.keys(node)){
154
+ clone[key] = cloneDeepWithProvenance(node[key], sourceRefId);
155
+ }
156
+ if (node['~r'] !== undefined) {
157
+ setNonEnumerableProperty(clone, '~r', node['~r']);
158
+ } else if (sourceRefId) {
159
+ setNonEnumerableProperty(clone, '~r', sourceRefId);
160
+ }
161
+ if (node['~l'] !== undefined) setNonEnumerableProperty(clone, '~l', node['~l']);
162
+ if (node['~k'] !== undefined) setNonEnumerableProperty(clone, '~k', node['~k']);
163
+ return clone;
164
+ }
165
+ // Evaluate a _build.* operator using evaluateOperators
166
+ function evaluateBuildOperator(node, ctx) {
167
+ const { output, errors } = evaluateOperators({
168
+ input: node,
169
+ operators: ctx.operators,
170
+ operatorPrefix: '_build.',
171
+ env: ctx.env,
172
+ dynamicIdentifiers: ctx.dynamicIdentifiers
173
+ });
174
+ if (errors.length > 0) {
175
+ errors.forEach((error)=>{
176
+ error.filePath = error.refId ? ctx.refMap[error.refId]?.path : ctx.currentFile;
177
+ ctx.collectError(error);
178
+ });
179
+ }
180
+ return output;
181
+ }
182
+ // Resolve a _var node
183
+ function resolveVar(node, ctx) {
184
+ const varDef = node._var;
185
+ // String form: { _var: "key" }
186
+ if (type.isString(varDef)) {
187
+ const value = get(ctx.vars, varDef, {
188
+ default: null
189
+ });
190
+ return cloneVarValue(value, ctx.sourceRefId);
191
+ }
192
+ // Object form: { _var: { key, default } }
193
+ if (type.isObject(varDef) && type.isString(varDef.key)) {
194
+ const varFromParent = get(ctx.vars, varDef.key);
195
+ // Var provided (even if null) → use parent's sourceRefId for location
196
+ if (!type.isUndefined(varFromParent)) {
197
+ return cloneVarValue(varFromParent, ctx.sourceRefId);
198
+ }
199
+ // Not provided → use default, preserve template's ~r
200
+ const defaultValue = type.isNone(varDef.default) ? null : varDef.default;
201
+ return cloneVarValue(defaultValue, null);
202
+ }
203
+ throw new ConfigError('_var operator takes a string or object with "key" field as arguments.', {
204
+ filePath: ctx.currentFile
205
+ });
206
+ }
207
+ // Resolve a _ref node (12-step ref handling)
208
+ async function resolveRef(node, ctx) {
209
+ // 1. Create ref definition
210
+ const lineNumber = node['~l'];
211
+ const refDef = makeRefDefinition(node._ref, ctx.refId, ctx.refMap, lineNumber, ctx.path);
212
+ // 2. Store unresolved vars before resolution mutates them, and clone so
213
+ // resolution operates on a copy (preserving original.vars for resolver refs).
214
+ const varKeys = Object.keys(refDef.vars);
215
+ if (varKeys.length > 0) {
216
+ ctx.unresolvedRefVars[refDef.id] = refDef.vars;
217
+ refDef.vars = cloneForResolve(refDef.vars);
218
+ }
219
+ // 3. Resolve dynamic path/vars/key
220
+ if (type.isObject(refDef.path)) {
221
+ refDef.path = await resolve(cloneForResolve(refDef.path), ctx);
222
+ }
223
+ await Promise.all(varKeys.map(async (varKey)=>{
224
+ if (type.isObject(refDef.vars[varKey]) || type.isArray(refDef.vars[varKey])) {
225
+ refDef.vars[varKey] = await resolve(refDef.vars[varKey], ctx);
226
+ }
227
+ }));
228
+ if (type.isObject(refDef.key)) {
229
+ refDef.key = await resolve(cloneForResolve(refDef.key), ctx);
230
+ }
231
+ // 4. Update refMap with resolved path; store original for resolver refs
232
+ ctx.refMap[refDef.id].path = refDef.path;
233
+ if (!refDef.path) {
234
+ ctx.refMap[refDef.id].original = refDef.original;
235
+ }
236
+ // 5. Circular detection
237
+ if (refDef.path && ctx.refChain.has(refDef.path)) {
238
+ const chainDisplay = [
239
+ ...ctx.refChain,
240
+ refDef.path
241
+ ].join('\n -> ');
242
+ throw new ConfigError(`Circular reference detected. File "${refDef.path}" references itself through:\n -> ${chainDisplay}`, {
243
+ filePath: ctx.currentFile,
244
+ lineNumber: ctx.currentFile ? lineNumber : null
245
+ });
246
+ }
247
+ // Steps 6-12: File operations that can fail independently per ref.
248
+ // Errors are collected so the walker can continue processing sibling refs,
249
+ // allowing multiple errors to be reported at once.
250
+ try {
251
+ // 6. Load content
252
+ let content = await getRefContent({
253
+ context: ctx.buildContext,
254
+ refDef,
255
+ referencedFrom: ctx.currentFile
256
+ });
257
+ // 7. Create child context for the ref file
258
+ const childCtx = ctx.forRef({
259
+ refId: refDef.id,
260
+ vars: refDef.vars,
261
+ filePath: refDef.path
262
+ });
263
+ // 8. Walk the content
264
+ content = await resolve(content, childCtx);
265
+ // 9. Run transformer
266
+ content = await runTransformer({
267
+ context: ctx.buildContext,
268
+ input: content,
269
+ refDef
270
+ });
271
+ // 10. Extract key
272
+ content = getKey({
273
+ input: content,
274
+ refDef
275
+ });
276
+ // 11. Tag all nodes with ~r for provenance
277
+ tagRefDeep(content, refDef.id);
278
+ // 12. Propagate ~ignoreBuildChecks
279
+ if (refDef.ignoreBuildChecks !== undefined) {
280
+ if (type.isObject(content)) {
281
+ content['~ignoreBuildChecks'] = refDef.ignoreBuildChecks;
282
+ } else if (type.isArray(content)) {
283
+ content.forEach((item)=>{
284
+ if (type.isObject(item)) {
285
+ item['~ignoreBuildChecks'] = refDef.ignoreBuildChecks;
286
+ }
287
+ });
288
+ }
289
+ }
290
+ return content;
291
+ } catch (error) {
292
+ if (error instanceof ConfigError) {
293
+ ctx.collectError(error);
294
+ return null;
295
+ }
296
+ throw error;
297
+ }
298
+ }
299
+ // Core walk function — single-pass async tree walker
300
+ async function resolve(node, ctx) {
301
+ // 1. Primitives pass through
302
+ if (!type.isObject(node) && !type.isArray(node)) return node;
303
+ // 2. Object with _ref
304
+ if (type.isObject(node) && !type.isUndefined(node._ref)) {
305
+ return resolveRef(node, ctx);
306
+ }
307
+ // 4. Object with _var — resolve, then re-walk the result so any
308
+ // _ref or _build.* operators inside the default value get processed.
309
+ if (type.isObject(node) && !type.isUndefined(node._var)) {
310
+ try {
311
+ const varResult = resolveVar(node, ctx);
312
+ return await resolve(varResult, ctx);
313
+ } catch (error) {
314
+ if (error instanceof ConfigError) {
315
+ ctx.collectError(error);
316
+ return null;
317
+ }
318
+ throw error;
319
+ }
320
+ }
321
+ // 5. Array — walk children in parallel
322
+ if (type.isArray(node)) {
323
+ await Promise.all(node.map(async (item, i)=>{
324
+ node[i] = await resolve(item, ctx.child(String(i)));
325
+ }));
326
+ return node;
327
+ }
328
+ // 6. Object — walk children in parallel
329
+ const keys = Object.keys(node);
330
+ await Promise.all(keys.map(async (key)=>{
331
+ if (ctx.shouldStop) {
332
+ const childPath = ctx.path ? `${ctx.path}.${key}` : key;
333
+ if (ctx.shouldStop(childPath, ctx.refId)) {
334
+ delete node[key];
335
+ return;
336
+ }
337
+ }
338
+ node[key] = await resolve(node[key], ctx.child(key));
339
+ }));
340
+ // Check if this is a _build.* operator
341
+ if (isBuildOperator(node)) {
342
+ const result = evaluateBuildOperator(node, ctx);
343
+ tagRefDeep(result, ctx.refId);
344
+ return result;
345
+ }
346
+ return node;
347
+ }
348
+ export { resolve, WalkContext, cloneForResolve, tagRefDeep };