@lowdefy/build 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.
@@ -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,340 @@
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);
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
+ for (const varKey of varKeys){
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
+ const varResult = resolveVar(node, ctx);
311
+ return resolve(varResult, ctx);
312
+ }
313
+ // 5. Array — walk children in-place
314
+ if (type.isArray(node)) {
315
+ for(let i = 0; i < node.length; i++){
316
+ node[i] = await resolve(node[i], ctx.child(String(i)));
317
+ }
318
+ return node;
319
+ }
320
+ // 6. Object — walk children in-place
321
+ const keys = Object.keys(node);
322
+ for (const key of keys){
323
+ if (ctx.shouldStop) {
324
+ const childPath = ctx.path ? `${ctx.path}.${key}` : key;
325
+ if (ctx.shouldStop(childPath, ctx.refId)) {
326
+ delete node[key];
327
+ continue;
328
+ }
329
+ }
330
+ node[key] = await resolve(node[key], ctx.child(key));
331
+ }
332
+ // Check if this is a _build.* operator
333
+ if (isBuildOperator(node)) {
334
+ const result = evaluateBuildOperator(node, ctx);
335
+ tagRefDeep(result, ctx.refId);
336
+ return result;
337
+ }
338
+ return node;
339
+ }
340
+ export { resolve, WalkContext, cloneForResolve, tagRefDeep };
@@ -16,24 +16,33 @@
16
16
  import path from 'path';
17
17
  import { serializer, type } from '@lowdefy/helpers';
18
18
  import { ConfigError, LowdefyInternalError } from '@lowdefy/errors';
19
+ import operators from '@lowdefy/operators-js/operators/build';
19
20
  import addKeys from '../addKeys.js';
20
21
  import buildPage from '../buildPages/buildPage.js';
21
22
  import validateLinkReferences from '../buildPages/validateLinkReferences.js';
22
23
  import validatePayloadReferences from '../buildPages/validatePayloadReferences.js';
23
24
  import validateServerStateReferences from '../buildPages/validateServerStateReferences.js';
24
25
  import validateStateReferences from '../buildPages/validateStateReferences.js';
26
+ import collectDynamicIdentifiers from '../collectDynamicIdentifiers.js';
25
27
  import createCheckDuplicateId from '../../utils/createCheckDuplicateId.js';
26
28
  import createContext from '../../createContext.js';
27
- import createRefReviver from '../buildRefs/createRefReviver.js';
28
- import evaluateBuildOperators from '../buildRefs/evaluateBuildOperators.js';
29
29
  import evaluateStaticOperators from '../buildRefs/evaluateStaticOperators.js';
30
+ import getRefContent from '../buildRefs/getRefContent.js';
30
31
  import jsMapParser from '../buildJs/jsMapParser.js';
31
32
  import makeRefDefinition from '../buildRefs/makeRefDefinition.js';
32
- import recursiveBuild from '../buildRefs/recursiveBuild.js';
33
+ import { resolve, WalkContext, cloneForResolve, tagRefDeep } from '../buildRefs/walker.js';
34
+ import validateOperatorsDynamic from '../validateOperatorsDynamic.js';
35
+ import writeMaps from '../writeMaps.js';
33
36
  import detectMissingPluginPackages from './detectMissingPluginPackages.js';
34
37
  import updateServerPackageJsonJit from './updateServerPackageJsonJit.js';
35
38
  import validatePageTypes from './validatePageTypes.js';
36
39
  import writePageJit from './writePageJit.js';
40
+ validateOperatorsDynamic({
41
+ operators
42
+ });
43
+ const dynamicIdentifiers = collectDynamicIdentifiers({
44
+ operators
45
+ });
37
46
  async function buildPageJit({ pageId, pageRegistry, context, directories, logger }) {
38
47
  // Use provided context or create a minimal one for JIT builds
39
48
  const buildContext = context ?? createContext({
@@ -76,18 +85,20 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
76
85
  let resolvedVars = null;
77
86
  if (unresolvedVars) {
78
87
  const varRefDef = makeRefDefinition({}, null, buildContext.refMap);
79
- resolvedVars = await recursiveBuild({
80
- context: buildContext,
81
- refDef: varRefDef,
82
- count: 0,
83
- content: unresolvedVars,
84
- referencedFrom: pageEntry.refPath ?? pageEntry.resolverOriginal?.resolver
85
- });
86
- resolvedVars = await evaluateBuildOperators({
87
- context: buildContext,
88
- input: resolvedVars,
89
- refDef: varRefDef
88
+ const varCtx = new WalkContext({
89
+ buildContext,
90
+ refId: varRefDef.id,
91
+ sourceRefId: null,
92
+ vars: {},
93
+ path: '',
94
+ currentFile: pageEntry.refPath ?? pageEntry.resolverOriginal?.resolver ?? '',
95
+ refChain: new Set(),
96
+ operators,
97
+ env: process.env,
98
+ dynamicIdentifiers,
99
+ shouldStop: null
90
100
  });
101
+ resolvedVars = await resolve(cloneForResolve(unresolvedVars), varCtx);
91
102
  }
92
103
  let refDef;
93
104
  if (pageEntry.resolverOriginal) {
@@ -105,17 +116,25 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
105
116
  refDef = makeRefDefinition(refDefinition, null, buildContext.refMap);
106
117
  buildContext.refMap[refDef.id].path = refDef.path;
107
118
  }
108
- let processed = await recursiveBuild({
119
+ const pageContent = await getRefContent({
109
120
  context: buildContext,
110
121
  refDef,
111
- count: 0
122
+ referencedFrom: null
112
123
  });
113
- // Top-level operator evaluation (same as buildRefs does after recursiveBuild)
114
- processed = await evaluateBuildOperators({
115
- context: buildContext,
116
- input: processed,
117
- refDef
124
+ const pageCtx = new WalkContext({
125
+ buildContext,
126
+ refId: refDef.id,
127
+ sourceRefId: null,
128
+ vars: refDef.vars ?? {},
129
+ path: '',
130
+ currentFile: refDef.path ?? '',
131
+ refChain: new Set(),
132
+ operators,
133
+ env: process.env,
134
+ dynamicIdentifiers,
135
+ shouldStop: null
118
136
  });
137
+ let processed = await resolve(pageContent, pageCtx);
119
138
  processed = evaluateStaticOperators({
120
139
  context: buildContext,
121
140
  input: processed,
@@ -129,14 +148,9 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
129
148
  throw new ConfigError(`Page "${pageId}" not found in resolved page source file.`);
130
149
  }
131
150
  }
132
- // Stamp root-level content with ~r for correct error file tracing.
133
- // recursiveBuild stamps child _ref content via createRefReviver, but the
134
- // root file's own objects have no parent to do this. Without ~r, addKeys
135
- // can't link objects to their source file and errors fall back to lowdefy.yaml.
136
- const reviver = createRefReviver(refDef.id);
137
- processed = serializer.copy(processed, {
138
- reviver
139
- });
151
+ // Tag all objects with ~r for ref provenance (normally done inside _ref
152
+ // resolution by the walker; JIT resolves the page file directly).
153
+ tagRefDeep(processed, refDef.id);
140
154
  // Apply skeleton-computed auth (buildAuth ran during skeleton build)
141
155
  processed.auth = pageEntry.auth;
142
156
  // Add keys to the resolved page
@@ -144,6 +158,11 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
144
158
  components: processed,
145
159
  context: buildContext
146
160
  });
161
+ // Write keyMap/refMap so the error handler reads JIT entries from disk.
162
+ // JIT addKeys assigns fresh ~k values that aren't in the skeleton keyMap.
163
+ await writeMaps({
164
+ context: buildContext
165
+ });
147
166
  // Initialize linkActionRefs for buildPage (normally done by buildPages)
148
167
  if (!buildContext.linkActionRefs) {
149
168
  buildContext.linkActionRefs = [];
@@ -66,10 +66,15 @@ function createPageRegistry({ components, context }) {
66
66
  const keyMapEntry = context.keyMap[page['~k']];
67
67
  const refId = keyMapEntry?.['~r'] ?? null;
68
68
  const sourceRef = !type.isNone(refId) ? findPageSourceRef(refId, context.refMap, unresolvedRefVars) : null;
69
+ // Inline pages (defined directly in lowdefy.yaml) have a refId pointing to
70
+ // the root ref but findPageSourceRef returns null because there is no
71
+ // separate source file. Set refId to null so buildPageJit serves them from
72
+ // the pre-built artifact written by buildShallowPages.
73
+ const isInline = !type.isNone(refId) && sourceRef === null && !type.isNone(context.refMap[refId]) && type.isNone(context.refMap[refId].parent);
69
74
  registry.set(page.id, {
70
75
  pageId: page.id,
71
76
  auth: page.auth,
72
- refId,
77
+ refId: isInline ? null : refId,
73
78
  refPath: sourceRef?.path ?? null,
74
79
  unresolvedVars: sourceRef?.unresolvedVars ?? null,
75
80
  resolverOriginal: sourceRef?.original ?? null
@@ -48,7 +48,6 @@ import writePluginImports from '../writePluginImports/writePluginImports.js';
48
48
  import addInstalledTypes from './addInstalledTypes.js';
49
49
  import buildJsShallow from './buildJsShallow.js';
50
50
  import buildShallowPages from './buildShallowPages.js';
51
- import stripPageContent from './stripPageContent.js';
52
51
  import writeSourcelessPages from './writeSourcelessPages.js';
53
52
  async function shallowBuild(options) {
54
53
  makeId.reset();
@@ -73,9 +72,6 @@ async function shallowBuild(options) {
73
72
  components,
74
73
  context
75
74
  });
76
- stripPageContent({
77
- components
78
- });
79
75
  tryBuildStep(testSchema, 'testSchema', {
80
76
  components,
81
77
  context
@@ -201,6 +197,7 @@ async function shallowBuild(options) {
201
197
  context
202
198
  });
203
199
  await context.writeBuildArtifact('jsMap.json', JSON.stringify(context.jsMap));
200
+ await context.writeBuildArtifact('idCounter.json', JSON.stringify(makeId.counter));
204
201
  await context.writeBuildArtifact('customTypesMap.json', JSON.stringify(options.customTypesMap ?? {}));
205
202
  // Persist snapshot of installed packages for JIT missing-package detection.
206
203
  // Written as a build artifact so JIT builds compare against the skeleton