@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.
- package/dist/build/buildRefs/buildRefs.js +35 -19
- package/dist/build/buildRefs/evaluateStaticOperators.js +5 -9
- package/dist/build/buildRefs/parseRefContent.js +8 -1
- package/dist/build/buildRefs/walker.js +340 -0
- package/dist/build/jit/buildPageJit.js +48 -29
- package/dist/build/jit/createPageRegistry.js +6 -1
- package/dist/build/jit/shallowBuild.js +1 -4
- package/dist/defaultTypesMap.js +338 -338
- package/dist/index.js +8 -6
- package/dist/indexDev.js +1 -0
- package/dist/lowdefySchema.js +0 -1
- package/dist/utils/makeId.js +3 -0
- package/package.json +42 -42
- package/dist/build/buildRefs/createRefReviver.js +0 -28
- package/dist/build/buildRefs/evaluateBuildOperators.js +0 -53
- package/dist/build/buildRefs/getRefsFromFile.js +0 -42
- package/dist/build/buildRefs/populateRefs.js +0 -105
- package/dist/build/buildRefs/recursiveBuild.js +0 -133
- package/dist/build/jit/getRefPositions.js +0 -38
- package/dist/build/jit/stripPageContent.js +0 -23
|
@@ -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
|
|
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
|
|
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
|
-
|
|
23
|
-
context,
|
|
24
|
-
refDef,
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
54
|
+
referencedFrom: null
|
|
40
55
|
});
|
|
41
|
-
|
|
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 {
|
|
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
|
|
34
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
|
|
119
|
+
const pageContent = await getRefContent({
|
|
109
120
|
context: buildContext,
|
|
110
121
|
refDef,
|
|
111
|
-
|
|
122
|
+
referencedFrom: null
|
|
112
123
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
//
|
|
133
|
-
//
|
|
134
|
-
|
|
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
|