@lowdefy/build 5.6.0 → 6.1.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/buildAgents.js +61 -16
- package/dist/build/buildApi/buildApi.js +4 -1
- package/dist/build/buildApi/buildEndpoint.js +8 -2
- package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
- package/dist/build/buildApi/buildRoutine/setStepId.js +2 -0
- package/dist/build/buildApi/buildRoutine/validateStep.js +61 -0
- package/dist/build/buildApi/resolveEndpointSchedules.js +40 -0
- package/dist/build/buildApi/validateEndpoint.js +96 -1
- package/dist/build/buildApp.js +13 -20
- package/dist/build/buildAppMeta.js +138 -0
- package/dist/build/buildAuth/buildAgentAuth.js +14 -1
- package/dist/build/buildAuth/buildAuth.js +8 -3
- package/dist/build/buildAuth/buildAuthPlugins.js +6 -0
- package/dist/build/buildAuth/buildAuthStrategies.js +117 -0
- package/dist/build/buildAuth/buildWebsocketAuth.js +55 -0
- package/dist/build/buildAuth/getProtectedWebsockets.js +29 -0
- package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
- package/dist/build/buildAuth/validateAuthConfig.js +15 -3
- package/dist/build/buildEnvironments.js +261 -0
- package/dist/build/buildImports/buildImportsDev.js +15 -0
- package/dist/build/buildImports/buildImportsProd.js +4 -1
- package/dist/build/buildJs/generateClientJsModule.js +27 -0
- package/dist/build/buildJs/writeJs.js +3 -5
- package/dist/build/buildLogger.js +22 -6
- package/dist/build/buildMcp.js +73 -0
- package/dist/build/buildModuleDefs.js +90 -48
- package/dist/build/buildModules.js +61 -14
- package/dist/build/buildNotifications.js +98 -0
- package/dist/build/buildPages/buildBlock/buildBlock.js +3 -1
- package/dist/build/buildPages/buildBlock/buildDynamicBlock.js +131 -0
- package/dist/build/buildPages/buildBlock/buildEvents.js +186 -21
- package/dist/build/buildPages/buildBlock/buildRequests.js +9 -0
- package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +2 -11
- package/dist/build/buildPages/buildBlock/setBlockId.js +5 -2
- package/dist/build/buildPages/buildBlock/validateBlock.js +7 -0
- package/dist/build/buildPages/buildPage.js +27 -3
- package/dist/build/buildPages/buildSubscriptions.js +145 -0
- package/dist/build/buildPages/validateDynamicBlockRefs.js +30 -0
- package/dist/build/buildPages/validateStateReferences.js +4 -2
- package/dist/build/buildPages/validateWebsocketRefs.js +30 -0
- package/dist/build/buildRefs/buildRefs.js +21 -12
- package/dist/build/buildRefs/cloneWithMarkers.js +57 -0
- package/dist/build/buildRefs/deferredRegions.js +80 -0
- package/dist/build/buildRefs/deferredRegistry.js +292 -0
- package/dist/build/buildRefs/getModuleRefContent.js +21 -6
- package/dist/build/buildRefs/getUserJavascriptFunction.js +1 -1
- package/dist/build/buildRefs/parseRefContent.js +24 -0
- package/dist/build/buildRefs/{evaluateStaticOperators.js → precomputeRuntimeOperators.js} +7 -6
- package/dist/build/buildRefs/walker.js +471 -207
- package/dist/build/buildTypes.js +22 -1
- package/dist/build/buildWebsockets.js +99 -0
- package/dist/build/collectPageContent.js +7 -1
- package/dist/build/copyAgentFileSystems.js +2 -2
- package/dist/build/full/buildPages.js +16 -0
- package/dist/build/full/updateServerPackageJson.js +21 -0
- package/dist/build/jit/buildPageJit.js +46 -6
- package/dist/build/jit/collectSkeletonSourceFiles.js +14 -0
- package/dist/build/jit/pageContentKeys.js +1 -0
- package/dist/build/jit/shallowBuild.js +84 -3
- package/dist/build/jit/writePageJit.js +11 -8
- package/dist/build/registerModules.js +125 -24
- package/dist/build/validateCallAgentSteps.js +58 -0
- package/dist/build/validateConfig.js +6 -1
- package/dist/build/validateEnvironmentGuards.js +69 -0
- package/dist/build/validateRenderNotificationSteps.js +50 -0
- package/dist/build/writeApi.js +98 -1
- package/dist/build/writeMcp.js +21 -0
- package/dist/build/writeNotifications.js +30 -0
- package/dist/build/writePluginImports/importPluginModule.js +39 -0
- package/dist/build/writePluginImports/writeActionSchemaMap.js +6 -7
- package/dist/build/writePluginImports/writeAuthImports.js +4 -0
- package/dist/build/writePluginImports/writeAvailableTypes.js +99 -0
- package/dist/build/writePluginImports/writeBlockSchemaMap.js +10 -9
- package/dist/build/writePluginImports/writeConnectionSchemaMap.js +101 -0
- package/dist/build/writePluginImports/writeGlobalsCss.js +20 -10
- package/dist/build/writePluginImports/writeNotificationImports.js +31 -0
- package/dist/build/writePluginImports/writeOperatorSchemaMap.js +6 -7
- package/dist/build/writePluginImports/writePluginImports.js +18 -4
- package/dist/build/writePluginImports/writeWebsocketImports.js +22 -0
- package/dist/build/writeWebsockets.js +30 -0
- package/dist/createContext.js +6 -1
- package/dist/defaultPackages.js +7 -1
- package/dist/defaultTypesMap.js +688 -388
- package/dist/dynamic/buildDynamicBlocks.js +120 -0
- package/dist/index.js +81 -10
- package/dist/indexDev.js +4 -0
- package/dist/lowdefySchema.js +969 -6
- package/dist/scripts/generateDefaultTypes.js +5 -2
- package/dist/test-utils/expectTerminates.js +33 -0
- package/dist/test-utils/runBuild.js +15 -1
- package/dist/test-utils/runBuildForSnapshots.js +37 -4
- package/dist/test-utils/testContext.js +8 -2
- package/dist/utils/countOperators.js +5 -17
- package/dist/utils/createPluginTypesMap.js +21 -0
- package/dist/utils/getEnvironmentNames.js +26 -0
- package/dist/utils/logCollectedErrors.js +15 -9
- package/dist/utils/serializeBuildException.js +29 -0
- package/dist/utils/validateCronExpression.js +131 -0
- package/dist/utils/writeBuildArtifact.js +5 -2
- package/package.json +67 -60
- package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
- /package/dist/build/{resolveModuleOperators.js → buildRefs/scopeMenuItemIds.js} +0 -0
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import path from 'path';
|
|
16
|
-
import { get, ReservedKeyError,
|
|
16
|
+
import { get, ReservedKeyError, type } from '@lowdefy/helpers';
|
|
17
17
|
import { ConfigError } from '@lowdefy/errors';
|
|
18
18
|
import { evaluateOperators } from '@lowdefy/operators';
|
|
19
19
|
import makeRefDefinition from './makeRefDefinition.js';
|
|
@@ -22,10 +22,12 @@ import getRefContent from './getRefContent.js';
|
|
|
22
22
|
import getModuleRefContent from './getModuleRefContent.js';
|
|
23
23
|
import runTransformer from './runTransformer.js';
|
|
24
24
|
import getKey from './getKey.js';
|
|
25
|
-
import { scopeMenuItemIds } from '
|
|
25
|
+
import { scopeMenuItemIds } from './scopeMenuItemIds.js';
|
|
26
26
|
import resolveDepTarget from '../resolveDepTarget.js';
|
|
27
27
|
import setNonEnumerableProperty from '../../utils/setNonEnumerableProperty.js';
|
|
28
|
+
import cloneWithMarkers from './cloneWithMarkers.js';
|
|
28
29
|
import collectExceptions from '../../utils/collectExceptions.js';
|
|
30
|
+
import { createRecord, getPlaceholderId, getRecord, makePlaceholder, makeRecordId, resolveDeferred } from './deferredRegistry.js';
|
|
29
31
|
let WalkContext = class WalkContext {
|
|
30
32
|
child(segment) {
|
|
31
33
|
return new WalkContext({
|
|
@@ -40,10 +42,15 @@ let WalkContext = class WalkContext {
|
|
|
40
42
|
path: this.path ? `${this.path}.${segment}` : segment,
|
|
41
43
|
currentFile: this.currentFile,
|
|
42
44
|
refChain: this.refChain,
|
|
45
|
+
deferModuleRefs: this.deferModuleRefs,
|
|
46
|
+
entrySection: this.entrySection,
|
|
43
47
|
operators: this.operators,
|
|
44
48
|
env: this.env,
|
|
49
|
+
lowdefyApp: this.lowdefyApp,
|
|
45
50
|
dynamicIdentifiers: this.dynamicIdentifiers,
|
|
46
|
-
shouldStop: this.shouldStop
|
|
51
|
+
shouldStop: this.shouldStop,
|
|
52
|
+
entryId: this.entryId,
|
|
53
|
+
activeRecord: this.activeRecord
|
|
47
54
|
});
|
|
48
55
|
}
|
|
49
56
|
forRef({ refId, vars, filePath, moduleRoot, packageRoot, moduleDependencies, moduleEntry, extraRefChainKeys }) {
|
|
@@ -68,10 +75,15 @@ let WalkContext = class WalkContext {
|
|
|
68
75
|
path: this.path,
|
|
69
76
|
currentFile: filePath ?? this.currentFile,
|
|
70
77
|
refChain: newChain,
|
|
78
|
+
deferModuleRefs: this.deferModuleRefs,
|
|
79
|
+
entrySection: this.entrySection,
|
|
71
80
|
operators: this.operators,
|
|
72
81
|
env: this.env,
|
|
82
|
+
lowdefyApp: this.lowdefyApp,
|
|
73
83
|
dynamicIdentifiers: this.dynamicIdentifiers,
|
|
74
|
-
shouldStop: this.shouldStop
|
|
84
|
+
shouldStop: this.shouldStop,
|
|
85
|
+
entryId: (moduleEntry ?? this.moduleEntry)?.id ?? this.entryId,
|
|
86
|
+
activeRecord: this.activeRecord
|
|
75
87
|
});
|
|
76
88
|
}
|
|
77
89
|
collectError(error) {
|
|
@@ -83,20 +95,34 @@ let WalkContext = class WalkContext {
|
|
|
83
95
|
get unresolvedRefVars() {
|
|
84
96
|
return this.buildContext.unresolvedRefVars;
|
|
85
97
|
}
|
|
86
|
-
constructor({ buildContext, refId, sourceRefId, vars, moduleDependencies, moduleEntry, moduleRoot, packageRoot, path, currentFile, refChain, operators, env, dynamicIdentifiers, shouldStop }){
|
|
98
|
+
constructor({ buildContext, refId, sourceRefId, vars, moduleDependencies, moduleEntry, moduleRoot, packageRoot, path, currentFile, refChain, deferModuleRefs, operators, env, lowdefyApp, dynamicIdentifiers, shouldStop, entryId, entrySection, activeRecord }){
|
|
87
99
|
this.buildContext = buildContext;
|
|
88
100
|
this.refId = refId;
|
|
89
101
|
this.sourceRefId = sourceRefId;
|
|
90
102
|
this.vars = vars;
|
|
91
103
|
this.moduleDependencies = moduleDependencies;
|
|
92
104
|
this.moduleEntry = moduleEntry ?? null;
|
|
105
|
+
// The owning module entry id for deferred-record coordinates. Set by the
|
|
106
|
+
// manifest resolvers even before the entry object exists (the local pass
|
|
107
|
+
// walks the manifest before registration completes).
|
|
108
|
+
this.entryId = entryId ?? this.moduleEntry?.id ?? null;
|
|
109
|
+
// The deferred record this walk resolves (null in ordinary walks). Threads
|
|
110
|
+
// through child() and forRef() unchanged, like refChain — every demand made
|
|
111
|
+
// anywhere inside a record's resolution attributes to that record in the
|
|
112
|
+
// wait-graph.
|
|
113
|
+
this.activeRecord = activeRecord ?? null;
|
|
93
114
|
this.moduleRoot = moduleRoot;
|
|
94
115
|
this.packageRoot = packageRoot;
|
|
95
116
|
this.path = path;
|
|
96
117
|
this.currentFile = currentFile;
|
|
97
118
|
this.refChain = refChain;
|
|
119
|
+
this.deferModuleRefs = deferModuleRefs ?? false;
|
|
120
|
+
// Which entry-config section a prepare walk serves ('consumerVars' or
|
|
121
|
+
// 'connections') — entryRef record coordinates and slots need it.
|
|
122
|
+
this.entrySection = entrySection ?? null;
|
|
98
123
|
this.operators = operators;
|
|
99
124
|
this.env = env;
|
|
125
|
+
this.lowdefyApp = lowdefyApp;
|
|
100
126
|
this.dynamicIdentifiers = dynamicIdentifiers;
|
|
101
127
|
this.shouldStop = shouldStop;
|
|
102
128
|
}
|
|
@@ -130,62 +156,6 @@ function tagRefDeep(node, refId) {
|
|
|
130
156
|
}
|
|
131
157
|
}
|
|
132
158
|
}
|
|
133
|
-
// Deep clone preserving non-enumerable build markers (~r, ~l, ~k, ~arr, ~deferredFrom).
|
|
134
|
-
// Used before resolving ref def path/vars to prevent mutation of stored originals.
|
|
135
|
-
function cloneForResolve(value) {
|
|
136
|
-
if (!type.isObject(value) && !type.isArray(value)) return value;
|
|
137
|
-
if (type.isArray(value)) {
|
|
138
|
-
const clone = value.map((item)=>cloneForResolve(item));
|
|
139
|
-
if (value['~r'] !== undefined) setNonEnumerableProperty(clone, '~r', value['~r']);
|
|
140
|
-
if (value['~l'] !== undefined) setNonEnumerableProperty(clone, '~l', value['~l']);
|
|
141
|
-
if (value['~k'] !== undefined) setNonEnumerableProperty(clone, '~k', value['~k']);
|
|
142
|
-
if (value['~arr'] !== undefined) setNonEnumerableProperty(clone, '~arr', value['~arr']);
|
|
143
|
-
if (value['~deferredFrom'] !== undefined) setNonEnumerableProperty(clone, '~deferredFrom', value['~deferredFrom']);
|
|
144
|
-
return clone;
|
|
145
|
-
}
|
|
146
|
-
const clone = {};
|
|
147
|
-
for (const key of Object.keys(value)){
|
|
148
|
-
clone[key] = cloneForResolve(value[key]);
|
|
149
|
-
}
|
|
150
|
-
if (value['~r'] !== undefined) setNonEnumerableProperty(clone, '~r', value['~r']);
|
|
151
|
-
if (value['~l'] !== undefined) setNonEnumerableProperty(clone, '~l', value['~l']);
|
|
152
|
-
if (value['~k'] !== undefined) setNonEnumerableProperty(clone, '~k', value['~k']);
|
|
153
|
-
if (value['~deferredFrom'] !== undefined) setNonEnumerableProperty(clone, '~deferredFrom', value['~deferredFrom']);
|
|
154
|
-
return clone;
|
|
155
|
-
}
|
|
156
|
-
// Deep clone a var value, preserving markers and setting ~r provenance.
|
|
157
|
-
// When sourceRefId is null, preserves the template's existing ~r markers.
|
|
158
|
-
function cloneVarValue(value, sourceRefId) {
|
|
159
|
-
if (!type.isObject(value) && !type.isArray(value)) return value;
|
|
160
|
-
return cloneDeepWithProvenance(value, sourceRefId);
|
|
161
|
-
}
|
|
162
|
-
function cloneDeepWithProvenance(node, sourceRefId) {
|
|
163
|
-
if (!type.isObject(node) && !type.isArray(node)) return node;
|
|
164
|
-
if (type.isArray(node)) {
|
|
165
|
-
const clone = node.map((item)=>cloneDeepWithProvenance(item, sourceRefId));
|
|
166
|
-
if (node['~r'] !== undefined) {
|
|
167
|
-
setNonEnumerableProperty(clone, '~r', node['~r']);
|
|
168
|
-
} else if (sourceRefId) {
|
|
169
|
-
setNonEnumerableProperty(clone, '~r', sourceRefId);
|
|
170
|
-
}
|
|
171
|
-
if (node['~l'] !== undefined) setNonEnumerableProperty(clone, '~l', node['~l']);
|
|
172
|
-
if (node['~k'] !== undefined) setNonEnumerableProperty(clone, '~k', node['~k']);
|
|
173
|
-
if (node['~arr'] !== undefined) setNonEnumerableProperty(clone, '~arr', node['~arr']);
|
|
174
|
-
return clone;
|
|
175
|
-
}
|
|
176
|
-
const clone = {};
|
|
177
|
-
for (const key of Object.keys(node)){
|
|
178
|
-
clone[key] = cloneDeepWithProvenance(node[key], sourceRefId);
|
|
179
|
-
}
|
|
180
|
-
if (node['~r'] !== undefined) {
|
|
181
|
-
setNonEnumerableProperty(clone, '~r', node['~r']);
|
|
182
|
-
} else if (sourceRefId) {
|
|
183
|
-
setNonEnumerableProperty(clone, '~r', sourceRefId);
|
|
184
|
-
}
|
|
185
|
-
if (node['~l'] !== undefined) setNonEnumerableProperty(clone, '~l', node['~l']);
|
|
186
|
-
if (node['~k'] !== undefined) setNonEnumerableProperty(clone, '~k', node['~k']);
|
|
187
|
-
return clone;
|
|
188
|
-
}
|
|
189
159
|
// Evaluate a _build.* operator using evaluateOperators
|
|
190
160
|
function evaluateBuildOperator(node, ctx) {
|
|
191
161
|
const { output, errors } = evaluateOperators({
|
|
@@ -193,6 +163,7 @@ function evaluateBuildOperator(node, ctx) {
|
|
|
193
163
|
operators: ctx.operators,
|
|
194
164
|
operatorPrefix: '_build.',
|
|
195
165
|
env: ctx.env,
|
|
166
|
+
lowdefyApp: ctx.lowdefyApp,
|
|
196
167
|
dynamicIdentifiers: ctx.dynamicIdentifiers
|
|
197
168
|
});
|
|
198
169
|
if (errors.length > 0) {
|
|
@@ -227,18 +198,22 @@ function resolveVar(node, ctx) {
|
|
|
227
198
|
const value = readVar(varDef, ctx, {
|
|
228
199
|
default: null
|
|
229
200
|
});
|
|
230
|
-
return
|
|
201
|
+
return cloneWithMarkers(value, {
|
|
202
|
+
assignRefId: ctx.sourceRefId
|
|
203
|
+
});
|
|
231
204
|
}
|
|
232
205
|
// Object form: { _var: { key, default } }
|
|
233
206
|
if (type.isObject(varDef) && type.isString(varDef.key)) {
|
|
234
207
|
const varFromParent = readVar(varDef.key, ctx);
|
|
235
208
|
// Var provided (even if null) → use parent's sourceRefId for location
|
|
236
209
|
if (!type.isUndefined(varFromParent)) {
|
|
237
|
-
return
|
|
210
|
+
return cloneWithMarkers(varFromParent, {
|
|
211
|
+
assignRefId: ctx.sourceRefId
|
|
212
|
+
});
|
|
238
213
|
}
|
|
239
214
|
// Not provided → use default, preserve template's ~r
|
|
240
215
|
const defaultValue = type.isNone(varDef.default) ? null : varDef.default;
|
|
241
|
-
return
|
|
216
|
+
return cloneWithMarkers(defaultValue);
|
|
242
217
|
}
|
|
243
218
|
throw new ConfigError('_var operator takes a string or object with "key" field as arguments.', {
|
|
244
219
|
filePath: ctx.currentFile
|
|
@@ -253,7 +228,9 @@ async function resolveModuleVar(node, ctx) {
|
|
|
253
228
|
});
|
|
254
229
|
}
|
|
255
230
|
const value = await resolveEffectiveVar(key, ctx.moduleEntry, ctx);
|
|
256
|
-
return
|
|
231
|
+
return cloneWithMarkers(value, {
|
|
232
|
+
assignRefId: ctx.sourceRefId
|
|
233
|
+
});
|
|
257
234
|
}
|
|
258
235
|
// Navigate the var definitions tree by dot-path key, following `properties` nesting.
|
|
259
236
|
function getVarDef(varDefs, key) {
|
|
@@ -274,27 +251,27 @@ function getVarDef(varDefs, key) {
|
|
|
274
251
|
// at the module manifest. The fresh context prevents false circular-ref detection
|
|
275
252
|
// from the consumer's refChain and ensures _ref paths resolve relative to the
|
|
276
253
|
// module root.
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
return
|
|
254
|
+
// Deep-force: demand every deferred-record placeholder in a value's subtree
|
|
255
|
+
// and splice the results in place, so cached var values are placeholder-free
|
|
256
|
+
// pure data — no consumer of the var cache needs to know records exist.
|
|
257
|
+
async function deepForcePlaceholders(value, ctx) {
|
|
258
|
+
const rootId = getPlaceholderId(value);
|
|
259
|
+
if (rootId !== undefined) {
|
|
260
|
+
return cloneWithMarkers(await resolveDeferred(ctx, rootId));
|
|
261
|
+
}
|
|
262
|
+
if (type.isArray(value)) {
|
|
263
|
+
for(let i = 0; i < value.length; i++){
|
|
264
|
+
value[i] = await deepForcePlaceholders(value[i], ctx);
|
|
265
|
+
}
|
|
266
|
+
return value;
|
|
267
|
+
}
|
|
268
|
+
if (type.isObject(value)) {
|
|
269
|
+
for (const key of Object.keys(value)){
|
|
270
|
+
value[key] = await deepForcePlaceholders(value[key], ctx);
|
|
271
|
+
}
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
return value;
|
|
298
275
|
}
|
|
299
276
|
// Build a merged object for namespace vars (vars with `properties`). Each
|
|
300
277
|
// declared property resolves through resolveEffectiveVar — consumer values
|
|
@@ -307,22 +284,53 @@ async function resolveNamespaceVar(prefix, varDef, moduleEntry, ctx) {
|
|
|
307
284
|
}
|
|
308
285
|
return result;
|
|
309
286
|
}
|
|
287
|
+
// Read a consumer value by dotted key, forcing any entryRef placeholder on the
|
|
288
|
+
// path: a placeholder at the key or any ancestor resolves through
|
|
289
|
+
// resolveDeferred and is spliced into consumerVars in place, then the read
|
|
290
|
+
// continues. There is no case split on entry state and no structural-snapshot
|
|
291
|
+
// read — a read forces exactly the records its value depends on.
|
|
292
|
+
async function readConsumerValue(moduleEntry, key, ctx) {
|
|
293
|
+
// Whole-blob deferral: the entire consumerVars value can be a placeholder
|
|
294
|
+
// (vars: { _ref: { module, component } }) — force it before walking keys.
|
|
295
|
+
const rootPlaceholderId = getPlaceholderId(moduleEntry.consumerVars);
|
|
296
|
+
if (rootPlaceholderId !== undefined) {
|
|
297
|
+
moduleEntry.consumerVars = cloneWithMarkers(await resolveDeferred(ctx, rootPlaceholderId)) ?? {};
|
|
298
|
+
}
|
|
299
|
+
const parts = key.split('.');
|
|
300
|
+
let parent = moduleEntry.consumerVars;
|
|
301
|
+
for(let i = 0; i < parts.length; i++){
|
|
302
|
+
if (!type.isObject(parent)) return undefined;
|
|
303
|
+
let node = parent[parts[i]];
|
|
304
|
+
const placeholderId = getPlaceholderId(node);
|
|
305
|
+
if (placeholderId !== undefined) {
|
|
306
|
+
node = cloneWithMarkers(await resolveDeferred(ctx, placeholderId));
|
|
307
|
+
parent[parts[i]] = node;
|
|
308
|
+
}
|
|
309
|
+
if (i === parts.length - 1) return node;
|
|
310
|
+
parent = node;
|
|
311
|
+
}
|
|
312
|
+
return undefined;
|
|
313
|
+
}
|
|
310
314
|
// Core lazy var resolution with caching on the module entry.
|
|
311
315
|
async function resolveEffectiveVar(key, moduleEntry, ctx) {
|
|
312
316
|
if (Object.hasOwn(moduleEntry.resolvedVarCache, key)) {
|
|
313
317
|
return moduleEntry.resolvedVarCache[key];
|
|
314
318
|
}
|
|
315
|
-
const consumerValue =
|
|
316
|
-
default: undefined
|
|
317
|
-
});
|
|
319
|
+
const consumerValue = await readConsumerValue(moduleEntry, key, ctx);
|
|
318
320
|
const varDef = getVarDef(moduleEntry.varDefs, key);
|
|
319
321
|
let result;
|
|
320
322
|
if (varDef?.properties) {
|
|
321
323
|
result = await resolveNamespaceVar(key, varDef, moduleEntry, ctx);
|
|
322
324
|
} else if (!type.isNone(consumerValue)) {
|
|
323
|
-
|
|
325
|
+
// Deep-force: a consumer value can carry deferred-record placeholders at
|
|
326
|
+
// any depth — demand each and splice, so the cache holds pure data.
|
|
327
|
+
result = await deepForcePlaceholders(consumerValue, ctx);
|
|
324
328
|
} else if (varDef && !type.isUndefined(varDef.default)) {
|
|
325
|
-
|
|
329
|
+
// Object/array defaults are varDefault records (demand-only — never swept,
|
|
330
|
+
// a broken default nobody reads must not fail the build); scalar defaults
|
|
331
|
+
// stay raw in varDefs and need no walk.
|
|
332
|
+
const defaultRecordId = getPlaceholderId(varDef.default);
|
|
333
|
+
result = defaultRecordId !== undefined ? await resolveDeferred(ctx, defaultRecordId) : varDef.default;
|
|
326
334
|
} else {
|
|
327
335
|
result = null;
|
|
328
336
|
}
|
|
@@ -334,6 +342,8 @@ const MODULE_ID_OPERATOR_KEYS = [
|
|
|
334
342
|
'_module.pageId',
|
|
335
343
|
'_module.connectionId',
|
|
336
344
|
'_module.endpointId',
|
|
345
|
+
'_module.agentId',
|
|
346
|
+
'_module.notificationId',
|
|
337
347
|
'_module.id'
|
|
338
348
|
];
|
|
339
349
|
function isModuleIdOperator(node) {
|
|
@@ -364,16 +374,28 @@ function resolveModulePageId(arg, moduleEntry, context, configKey) {
|
|
|
364
374
|
});
|
|
365
375
|
}
|
|
366
376
|
// Resolve _module.connectionId
|
|
367
|
-
function resolveModuleConnectionId(arg, moduleEntry,
|
|
377
|
+
async function resolveModuleConnectionId(arg, moduleEntry, ctx, configKey) {
|
|
378
|
+
const context = ctx.buildContext;
|
|
379
|
+
// A connections value can be an entryRef placeholder mid-build — demand it
|
|
380
|
+
// through the read path and splice, exactly like consumerVars reads. True
|
|
381
|
+
// remap cycles surface as named wait-graph errors from resolveDeferred.
|
|
382
|
+
const readRemapValue = async (entry, id)=>{
|
|
383
|
+
const remapping = entry.connections ?? {};
|
|
384
|
+
const placeholderId = getPlaceholderId(remapping[id]);
|
|
385
|
+
if (placeholderId !== undefined) {
|
|
386
|
+
remapping[id] = cloneWithMarkers(await resolveDeferred(ctx, placeholderId));
|
|
387
|
+
}
|
|
388
|
+
return remapping[id];
|
|
389
|
+
};
|
|
368
390
|
if (type.isString(arg)) {
|
|
369
391
|
if (!moduleEntry) {
|
|
370
392
|
throw new ConfigError('_module.connectionId string form is ambiguous at the app level — no module to scope against. Use { id, module } to specify the target module.', {
|
|
371
393
|
configKey
|
|
372
394
|
});
|
|
373
395
|
}
|
|
374
|
-
const
|
|
375
|
-
if (
|
|
376
|
-
return
|
|
396
|
+
const remapValue = await readRemapValue(moduleEntry, arg);
|
|
397
|
+
if (remapValue) {
|
|
398
|
+
return remapValue;
|
|
377
399
|
}
|
|
378
400
|
return `${moduleEntry.id}/${arg}`;
|
|
379
401
|
}
|
|
@@ -385,9 +407,9 @@ function resolveModuleConnectionId(arg, moduleEntry, context, configKey) {
|
|
|
385
407
|
configKey,
|
|
386
408
|
usage: `_module.connectionId { id: "${arg.id}", module: "${arg.module}" }`
|
|
387
409
|
});
|
|
388
|
-
const
|
|
389
|
-
if (
|
|
390
|
-
return
|
|
410
|
+
const remapValue = await readRemapValue(targetEntry, arg.id);
|
|
411
|
+
if (remapValue) {
|
|
412
|
+
return remapValue;
|
|
391
413
|
}
|
|
392
414
|
return `${targetEntry.id}/${arg.id}`;
|
|
393
415
|
}
|
|
@@ -419,6 +441,54 @@ function resolveModuleEndpointId(arg, moduleEntry, context, configKey) {
|
|
|
419
441
|
configKey
|
|
420
442
|
});
|
|
421
443
|
}
|
|
444
|
+
// Resolve _module.agentId
|
|
445
|
+
function resolveModuleAgentId(arg, moduleEntry, context, configKey) {
|
|
446
|
+
if (type.isString(arg)) {
|
|
447
|
+
if (!moduleEntry) {
|
|
448
|
+
throw new ConfigError('_module.agentId string form is ambiguous at the app level — no module to scope against. Use { id, module } to specify the target module.', {
|
|
449
|
+
configKey
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
return `${moduleEntry.id}/${arg}`;
|
|
453
|
+
}
|
|
454
|
+
if (type.isObject(arg) && type.isString(arg.id) && type.isString(arg.module)) {
|
|
455
|
+
const targetEntry = resolveDepTarget({
|
|
456
|
+
moduleEntry,
|
|
457
|
+
depName: arg.module,
|
|
458
|
+
context,
|
|
459
|
+
configKey,
|
|
460
|
+
usage: `_module.agentId { id: "${arg.id}", module: "${arg.module}" }`
|
|
461
|
+
});
|
|
462
|
+
return `${targetEntry.id}/${arg.id}`;
|
|
463
|
+
}
|
|
464
|
+
throw new ConfigError('_module.agentId requires a string or object { id, module }.', {
|
|
465
|
+
configKey
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
// Resolve _module.notificationId
|
|
469
|
+
function resolveModuleNotificationId(arg, moduleEntry, context, configKey) {
|
|
470
|
+
if (type.isString(arg)) {
|
|
471
|
+
if (!moduleEntry) {
|
|
472
|
+
throw new ConfigError('_module.notificationId string form is ambiguous at the app level — no module to scope against. Use { id, module } to specify the target module.', {
|
|
473
|
+
configKey
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
return `${moduleEntry.id}/${arg}`;
|
|
477
|
+
}
|
|
478
|
+
if (type.isObject(arg) && type.isString(arg.id) && type.isString(arg.module)) {
|
|
479
|
+
const targetEntry = resolveDepTarget({
|
|
480
|
+
moduleEntry,
|
|
481
|
+
depName: arg.module,
|
|
482
|
+
context,
|
|
483
|
+
configKey,
|
|
484
|
+
usage: `_module.notificationId { id: "${arg.id}", module: "${arg.module}" }`
|
|
485
|
+
});
|
|
486
|
+
return `${targetEntry.id}/${arg.id}`;
|
|
487
|
+
}
|
|
488
|
+
throw new ConfigError('_module.notificationId requires a string or object { id, module }.', {
|
|
489
|
+
configKey
|
|
490
|
+
});
|
|
491
|
+
}
|
|
422
492
|
// Resolve _module.id
|
|
423
493
|
function resolveModuleId(arg, moduleEntry, context, configKey) {
|
|
424
494
|
if (!type.isObject(arg)) {
|
|
@@ -444,7 +514,7 @@ function resolveModuleId(arg, moduleEntry, context, configKey) {
|
|
|
444
514
|
});
|
|
445
515
|
}
|
|
446
516
|
// Dispatch _module.*Id operators
|
|
447
|
-
function resolveModuleIdOperator(node, ctx) {
|
|
517
|
+
async function resolveModuleIdOperator(node, ctx) {
|
|
448
518
|
const { moduleEntry } = ctx;
|
|
449
519
|
const context = ctx.buildContext;
|
|
450
520
|
const configKey = node['~k'];
|
|
@@ -452,18 +522,61 @@ function resolveModuleIdOperator(node, ctx) {
|
|
|
452
522
|
return resolveModulePageId(node['_module.pageId'], moduleEntry, context, configKey);
|
|
453
523
|
}
|
|
454
524
|
if (!type.isUndefined(node['_module.connectionId'])) {
|
|
455
|
-
|
|
525
|
+
const connectionArg = node['_module.connectionId'];
|
|
526
|
+
// Prepare (deferModuleRefs) must not resolve the object form: it reads the
|
|
527
|
+
// TARGET entry's remap table, which may not be prepared yet — an eager read
|
|
528
|
+
// would see a raw ref instead of a demandable placeholder. It becomes a
|
|
529
|
+
// connRemap record; demand resolves it once every entry is prepared, so
|
|
530
|
+
// chained remaps force each other value-granularly and true remap cycles
|
|
531
|
+
// surface as named wait-graph errors.
|
|
532
|
+
if (ctx.deferModuleRefs && type.isObject(connectionArg)) {
|
|
533
|
+
const nested = ctx.path.split('.').includes('$refvars');
|
|
534
|
+
const section = ctx.entrySection ?? 'connections';
|
|
535
|
+
const recordId = makeRecordId({
|
|
536
|
+
entryId: ctx.entryId,
|
|
537
|
+
configPath: ctx.path ? `${section}.${ctx.path}` : section
|
|
538
|
+
});
|
|
539
|
+
createRecord(ctx.buildContext, {
|
|
540
|
+
id: recordId,
|
|
541
|
+
kind: 'connRemap',
|
|
542
|
+
body: node,
|
|
543
|
+
env: {
|
|
544
|
+
file: ctx.currentFile,
|
|
545
|
+
moduleRoot: null,
|
|
546
|
+
packageRoot: null,
|
|
547
|
+
entryId: null,
|
|
548
|
+
refId: ctx.refId,
|
|
549
|
+
configKey: configKey ?? null
|
|
550
|
+
},
|
|
551
|
+
slot: nested ? null : {
|
|
552
|
+
entryId: ctx.entryId,
|
|
553
|
+
section,
|
|
554
|
+
path: ctx.path
|
|
555
|
+
}
|
|
556
|
+
});
|
|
557
|
+
return makePlaceholder(recordId);
|
|
558
|
+
}
|
|
559
|
+
return resolveModuleConnectionId(connectionArg, moduleEntry, ctx, configKey);
|
|
456
560
|
}
|
|
457
561
|
if (!type.isUndefined(node['_module.endpointId'])) {
|
|
458
562
|
return resolveModuleEndpointId(node['_module.endpointId'], moduleEntry, context, configKey);
|
|
459
563
|
}
|
|
564
|
+
if (!type.isUndefined(node['_module.agentId'])) {
|
|
565
|
+
return resolveModuleAgentId(node['_module.agentId'], moduleEntry, context, configKey);
|
|
566
|
+
}
|
|
567
|
+
if (!type.isUndefined(node['_module.notificationId'])) {
|
|
568
|
+
return resolveModuleNotificationId(node['_module.notificationId'], moduleEntry, context, configKey);
|
|
569
|
+
}
|
|
460
570
|
if (!type.isUndefined(node['_module.id'])) {
|
|
461
571
|
return resolveModuleId(node['_module.id'], moduleEntry, context, configKey);
|
|
462
572
|
}
|
|
463
573
|
return node;
|
|
464
574
|
}
|
|
465
|
-
//
|
|
466
|
-
|
|
575
|
+
// Steps 1–7: scope-dependent prepare — resolves path/vars/key, normalises module
|
|
576
|
+
// paths, updates refMap, and checks for package-root escapes and circular refs.
|
|
577
|
+
// Returns the fully-resolved refDef; all side-effects (unresolvedRefVars, refMap)
|
|
578
|
+
// happen here. No file I/O is performed.
|
|
579
|
+
async function prepareRef(node, ctx) {
|
|
467
580
|
// 1. Create ref definition
|
|
468
581
|
const lineNumber = node['~l'];
|
|
469
582
|
const refDef = makeRefDefinition(node._ref, ctx.refId, ctx.refMap, lineNumber, ctx.path);
|
|
@@ -472,19 +585,32 @@ async function resolveRef(node, ctx) {
|
|
|
472
585
|
const varKeys = Object.keys(refDef.vars);
|
|
473
586
|
if (varKeys.length > 0) {
|
|
474
587
|
ctx.unresolvedRefVars[refDef.id] = refDef.vars;
|
|
475
|
-
refDef.vars =
|
|
588
|
+
refDef.vars = cloneWithMarkers(refDef.vars);
|
|
476
589
|
}
|
|
477
590
|
// 3. Resolve dynamic path/vars/key
|
|
478
591
|
if (type.isObject(refDef.path)) {
|
|
479
|
-
refDef.path = await resolve(
|
|
592
|
+
refDef.path = await resolve(cloneWithMarkers(refDef.path), ctx);
|
|
593
|
+
}
|
|
594
|
+
if (type.isObject(refDef.vars) && !type.isUndefined(refDef.vars._ref)) {
|
|
595
|
+
// A _ref at the root of vars resolves to the whole vars object
|
|
596
|
+
// (vars: { _ref: config.yaml }).
|
|
597
|
+
const varCtx = ctx.deferModuleRefs && refDef.module ? ctx.child('$refvars') : ctx;
|
|
598
|
+
refDef.vars = await resolve(refDef.vars, varCtx) ?? {};
|
|
599
|
+
} else {
|
|
600
|
+
await Promise.all(varKeys.map(async (varKey)=>{
|
|
601
|
+
if (type.isObject(refDef.vars[varKey]) || type.isArray(refDef.vars[varKey])) {
|
|
602
|
+
// Under prepare (deferModuleRefs), nested module refs inside this ref's
|
|
603
|
+
// vars become entryRef records too. Extend the path with a reserved
|
|
604
|
+
// segment so their coordinates are unique (distinct from this ref's own
|
|
605
|
+
// record) and recognizably nested (slot: null — their placeholder lives
|
|
606
|
+
// in the prepared body, not in consumerVars).
|
|
607
|
+
const varCtx = ctx.deferModuleRefs && refDef.module ? ctx.child('$refvars').child(varKey) : ctx;
|
|
608
|
+
refDef.vars[varKey] = await resolve(refDef.vars[varKey], varCtx);
|
|
609
|
+
}
|
|
610
|
+
}));
|
|
480
611
|
}
|
|
481
|
-
await Promise.all(varKeys.map(async (varKey)=>{
|
|
482
|
-
if (type.isObject(refDef.vars[varKey]) || type.isArray(refDef.vars[varKey])) {
|
|
483
|
-
refDef.vars[varKey] = await resolve(refDef.vars[varKey], ctx);
|
|
484
|
-
}
|
|
485
|
-
}));
|
|
486
612
|
if (type.isObject(refDef.key)) {
|
|
487
|
-
refDef.key = await resolve(
|
|
613
|
+
refDef.key = await resolve(cloneWithMarkers(refDef.key), ctx);
|
|
488
614
|
}
|
|
489
615
|
// 4. Module path resolution: resolve relative paths from the module root
|
|
490
616
|
rebaseModuleRefPaths({
|
|
@@ -520,114 +646,133 @@ async function resolveRef(node, ctx) {
|
|
|
520
646
|
lineNumber: ctx.currentFile ? lineNumber : null
|
|
521
647
|
});
|
|
522
648
|
}
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
649
|
+
return refDef;
|
|
650
|
+
}
|
|
651
|
+
// Steps 8–12, module flavor: content lookup, cross-module cycle key, module
|
|
652
|
+
// child context, consumer-var forwarding into file-ref bodies, walk under the
|
|
653
|
+
// module context, menu item scoping. Returns the walked content; loadAndWalkRef
|
|
654
|
+
// applies the shared tail (steps 13–16). Replayed deferred sentinels route here
|
|
655
|
+
// too — `fromFile` carries their original provenance.
|
|
656
|
+
async function resolveModuleExportRef(refDef, ctx, { configKey, fromFile }) {
|
|
657
|
+
// 8. Load content. Components and menus dereference a deferred record and
|
|
658
|
+
// clone its immutable body — the record env names the source file
|
|
659
|
+
// explicitly. Legacy live content (scalar bodies, sections produced inline
|
|
660
|
+
// by static operators) resolves against the manifest file.
|
|
661
|
+
const result = await getModuleRefContent({
|
|
662
|
+
context: ctx.buildContext,
|
|
663
|
+
refDef,
|
|
664
|
+
referencedFrom: fromFile,
|
|
665
|
+
walkCtx: ctx,
|
|
666
|
+
configKey
|
|
667
|
+
});
|
|
668
|
+
const resolvedEntryId = result.entryId;
|
|
669
|
+
const moduleEntry = ctx.buildContext.modules[resolvedEntryId];
|
|
670
|
+
let content;
|
|
671
|
+
let sourceFile;
|
|
672
|
+
if (result.recordId !== undefined) {
|
|
673
|
+
const record = getRecord(ctx.buildContext, result.recordId);
|
|
674
|
+
content = cloneWithMarkers(record.body);
|
|
675
|
+
sourceFile = record.env.file;
|
|
676
|
+
} else {
|
|
677
|
+
content = cloneWithMarkers(result.content);
|
|
678
|
+
sourceFile = path.join(moduleEntry.moduleRoot, 'module.lowdefy.yaml');
|
|
679
|
+
}
|
|
680
|
+
// 9. Circular detection for cross-module component/menu refs.
|
|
681
|
+
// File-based cycle detection (prepareRef step 7) misses these because each
|
|
682
|
+
// module has a different file path. Use a synthetic key with the resolved
|
|
683
|
+
// concrete entry ID: "module:<entryId>/<type>:<name>".
|
|
684
|
+
const exportType = refDef.component ? 'component' : 'menu';
|
|
685
|
+
const exportName = refDef.component ?? refDef.menu;
|
|
686
|
+
const cycleKey = `module:${resolvedEntryId}/${exportType}:${exportName}`;
|
|
687
|
+
if (ctx.refChain.has(cycleKey)) {
|
|
688
|
+
const chainDisplay = [
|
|
689
|
+
...ctx.refChain,
|
|
690
|
+
cycleKey
|
|
691
|
+
].join('\n -> ');
|
|
692
|
+
throw new ConfigError(`Circular module reference detected. Module "${resolvedEntryId}" ${exportType} "${exportName}" ` + `references itself through:\n -> ${chainDisplay}`, {
|
|
693
|
+
filePath: fromFile
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
// 10. Create module child context for the ref
|
|
697
|
+
const childCtx = ctx.forRef({
|
|
698
|
+
refId: refDef.id,
|
|
699
|
+
vars: refDef.vars,
|
|
700
|
+
filePath: sourceFile,
|
|
701
|
+
moduleRoot: moduleEntry.moduleRoot,
|
|
702
|
+
packageRoot: moduleEntry.packageRoot,
|
|
703
|
+
moduleDependencies: moduleEntry.moduleDependencies,
|
|
704
|
+
moduleEntry,
|
|
705
|
+
extraRefChainKeys: [
|
|
706
|
+
cycleKey
|
|
707
|
+
]
|
|
708
|
+
});
|
|
709
|
+
// When component/menu content is a file _ref, the inner ref would create
|
|
710
|
+
// a fresh var scope and lose the consumer's vars. Inject them into the clone.
|
|
711
|
+
if (type.isObject(content) && content._ref) {
|
|
712
|
+
if (type.isObject(content._ref)) {
|
|
713
|
+
content._ref.vars = {
|
|
714
|
+
...content._ref.vars ?? {},
|
|
715
|
+
...refDef.vars
|
|
716
|
+
};
|
|
717
|
+
} else if (type.isString(content._ref) && Object.keys(refDef.vars).length > 0) {
|
|
718
|
+
content._ref = {
|
|
719
|
+
path: content._ref,
|
|
720
|
+
vars: refDef.vars
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
// 11. Walk the content
|
|
725
|
+
content = await resolve(content, childCtx);
|
|
726
|
+
// 12. Scope menu item IDs (menu refs only)
|
|
727
|
+
if (refDef.menu) {
|
|
728
|
+
scopeMenuItemIds(content, moduleEntry.id);
|
|
729
|
+
}
|
|
730
|
+
return content;
|
|
731
|
+
}
|
|
732
|
+
// Steps 8–16: load content, build child context, walk, transform, tag.
|
|
733
|
+
// Module refs take steps 8–12 in resolveModuleExportRef; both paths share the
|
|
734
|
+
// tail (steps 13–16). `referencedFrom` overrides ctx.currentFile for provenance
|
|
735
|
+
// (used when replaying a deferred sentinel whose origin file differs from the
|
|
736
|
+
// stage-2 ctx.currentFile).
|
|
737
|
+
async function loadAndWalkRef(refDef, ctx, { configKey, referencedFrom } = {}) {
|
|
738
|
+
const fromFile = referencedFrom ?? ctx.currentFile;
|
|
739
|
+
// Errors here are collected (not thrown) so the walker can continue
|
|
740
|
+
// processing sibling refs and report multiple errors at once.
|
|
526
741
|
try {
|
|
527
|
-
// 8. Load content
|
|
528
742
|
let content;
|
|
529
|
-
let resolvedEntryId = null;
|
|
530
743
|
if (refDef.module) {
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
referencedFrom: ctx.currentFile,
|
|
535
|
-
walkCtx: ctx,
|
|
536
|
-
configKey: node['~k']
|
|
744
|
+
content = await resolveModuleExportRef(refDef, ctx, {
|
|
745
|
+
configKey,
|
|
746
|
+
fromFile
|
|
537
747
|
});
|
|
538
|
-
content = cloneForResolve(result.content);
|
|
539
|
-
resolvedEntryId = result.entryId;
|
|
540
748
|
} else {
|
|
749
|
+
// 8. Load content
|
|
541
750
|
content = await getRefContent({
|
|
542
751
|
context: ctx.buildContext,
|
|
543
752
|
refDef,
|
|
544
|
-
referencedFrom:
|
|
753
|
+
referencedFrom: fromFile
|
|
545
754
|
});
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
// File-based cycle detection (step 7) misses these because each module
|
|
549
|
-
// has a different file path. Use a synthetic key with the resolved
|
|
550
|
-
// concrete entry ID: "module:<entryId>/<type>:<name>".
|
|
551
|
-
if (resolvedEntryId && (refDef.component || refDef.menu)) {
|
|
552
|
-
const exportType = refDef.component ? 'component' : 'menu';
|
|
553
|
-
const exportName = refDef.component ?? refDef.menu;
|
|
554
|
-
const cycleKey = `module:${resolvedEntryId}/${exportType}:${exportName}`;
|
|
555
|
-
if (ctx.refChain.has(cycleKey)) {
|
|
556
|
-
const chainDisplay = [
|
|
557
|
-
...ctx.refChain,
|
|
558
|
-
cycleKey
|
|
559
|
-
].join('\n -> ');
|
|
560
|
-
throw new ConfigError(`Circular module reference detected. Module "${resolvedEntryId}" ${exportType} "${exportName}" ` + `references itself through:\n -> ${chainDisplay}`, {
|
|
561
|
-
filePath: ctx.currentFile
|
|
562
|
-
});
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
// 10. Create child context for the ref
|
|
566
|
-
let childCtx;
|
|
567
|
-
if (refDef.module && (refDef.component || refDef.menu)) {
|
|
568
|
-
const moduleEntry = ctx.buildContext.modules[resolvedEntryId];
|
|
569
|
-
const deferredFrom = content['~deferredFrom'];
|
|
570
|
-
const exportType = refDef.component ? 'component' : 'menu';
|
|
571
|
-
const exportName = refDef.component ?? refDef.menu;
|
|
572
|
-
const cycleKey = `module:${resolvedEntryId}/${exportType}:${exportName}`;
|
|
573
|
-
childCtx = ctx.forRef({
|
|
574
|
-
refId: refDef.id,
|
|
575
|
-
vars: refDef.vars,
|
|
576
|
-
filePath: deferredFrom ?? path.join(moduleEntry.moduleRoot, 'module.lowdefy.yaml'),
|
|
577
|
-
moduleRoot: moduleEntry.moduleRoot,
|
|
578
|
-
packageRoot: moduleEntry.packageRoot,
|
|
579
|
-
moduleDependencies: moduleEntry.moduleDependencies,
|
|
580
|
-
moduleEntry,
|
|
581
|
-
extraRefChainKeys: [
|
|
582
|
-
cycleKey
|
|
583
|
-
]
|
|
584
|
-
});
|
|
585
|
-
// Clone so each consumer gets an independent copy — getModuleRefContent
|
|
586
|
-
// returns a shared reference, and resolve() mutates in place.
|
|
587
|
-
// deferredFrom was read above before the clone (serializer.copy strips
|
|
588
|
-
// non-enumerable properties).
|
|
589
|
-
content = serializer.copy(content);
|
|
590
|
-
// When component/menu content is a file _ref, the inner ref would create
|
|
591
|
-
// a fresh var scope and lose the consumer's vars. Inject them into the clone.
|
|
592
|
-
if ((refDef.component || refDef.menu) && type.isObject(content) && content._ref) {
|
|
593
|
-
if (type.isObject(content._ref)) {
|
|
594
|
-
content._ref.vars = {
|
|
595
|
-
...content._ref.vars ?? {},
|
|
596
|
-
...refDef.vars
|
|
597
|
-
};
|
|
598
|
-
} else if (type.isString(content._ref) && Object.keys(refDef.vars).length > 0) {
|
|
599
|
-
content._ref = {
|
|
600
|
-
path: content._ref,
|
|
601
|
-
vars: refDef.vars
|
|
602
|
-
};
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
} else {
|
|
606
|
-
childCtx = ctx.forRef({
|
|
755
|
+
// 10. Create child context for the ref
|
|
756
|
+
const childCtx = ctx.forRef({
|
|
607
757
|
refId: refDef.id,
|
|
608
758
|
vars: refDef.vars,
|
|
609
759
|
filePath: refDef.path
|
|
610
760
|
});
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
content = await resolve(content, childCtx);
|
|
614
|
-
// 12. Scope menu item IDs (module menu refs only)
|
|
615
|
-
if (refDef.module && refDef.menu) {
|
|
616
|
-
const moduleEntry = ctx.buildContext.modules[resolvedEntryId];
|
|
617
|
-
scopeMenuItemIds(content, moduleEntry.id);
|
|
761
|
+
// 11. Walk the content
|
|
762
|
+
content = await resolve(content, childCtx);
|
|
618
763
|
}
|
|
619
764
|
// 13. Run transformer
|
|
620
765
|
content = await runTransformer({
|
|
621
766
|
context: ctx.buildContext,
|
|
622
767
|
input: content,
|
|
623
768
|
refDef,
|
|
624
|
-
referencedFrom:
|
|
769
|
+
referencedFrom: fromFile
|
|
625
770
|
});
|
|
626
771
|
// 14. Extract key
|
|
627
772
|
content = getKey({
|
|
628
773
|
input: content,
|
|
629
774
|
refDef,
|
|
630
|
-
filePath:
|
|
775
|
+
filePath: fromFile
|
|
631
776
|
});
|
|
632
777
|
// 15. Tag all nodes with ~r for provenance
|
|
633
778
|
tagRefDeep(content, refDef.id);
|
|
@@ -652,22 +797,81 @@ async function resolveRef(node, ctx) {
|
|
|
652
797
|
throw error;
|
|
653
798
|
}
|
|
654
799
|
}
|
|
800
|
+
// Resolve a _ref node: prepare (steps 1–7), then optionally defer module refs
|
|
801
|
+
// as sentinels (when ctx.deferModuleRefs is true), otherwise load-and-walk (steps 8–16).
|
|
802
|
+
async function resolveRef(node, ctx) {
|
|
803
|
+
const refDef = await prepareRef(node, ctx);
|
|
804
|
+
if (ctx.deferModuleRefs && refDef.module) {
|
|
805
|
+
// Prepare-time deferral: the refDef's dynamic parts are already resolved
|
|
806
|
+
// in the enclosing scope (steps 1-7); only the content pull defers. The
|
|
807
|
+
// prepared refDef becomes an entryRef record and the tree gets a
|
|
808
|
+
// placeholder. Records created inside another prepared ref's vars
|
|
809
|
+
// ($refvars segment) have no consumerVars slot — their placeholder lives
|
|
810
|
+
// in the parent body and is spliced by the walk that encounters it.
|
|
811
|
+
const nested = ctx.path.split('.').includes('$refvars');
|
|
812
|
+
const section = ctx.entrySection ?? 'consumerVars';
|
|
813
|
+
const id = makeRecordId({
|
|
814
|
+
entryId: ctx.entryId,
|
|
815
|
+
// ctx.path is '' when the whole section value is the ref (whole-blob).
|
|
816
|
+
configPath: ctx.path ? `${section}.${ctx.path}` : section
|
|
817
|
+
});
|
|
818
|
+
createRecord(ctx.buildContext, {
|
|
819
|
+
id,
|
|
820
|
+
kind: 'entryRef',
|
|
821
|
+
body: refDef,
|
|
822
|
+
env: {
|
|
823
|
+
// entryRef resolution runs at app level: no module scope; provenance
|
|
824
|
+
// is the file the ref was written in (replaces ~deferredFrom).
|
|
825
|
+
file: ctx.currentFile,
|
|
826
|
+
moduleRoot: null,
|
|
827
|
+
packageRoot: null,
|
|
828
|
+
entryId: null,
|
|
829
|
+
refId: ctx.refId,
|
|
830
|
+
configKey: node['~k'] ?? null
|
|
831
|
+
},
|
|
832
|
+
slot: nested ? null : {
|
|
833
|
+
entryId: ctx.entryId,
|
|
834
|
+
section,
|
|
835
|
+
path: ctx.path
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
return makePlaceholder(id);
|
|
839
|
+
}
|
|
840
|
+
return loadAndWalkRef(refDef, ctx, {
|
|
841
|
+
configKey: node['~k']
|
|
842
|
+
});
|
|
843
|
+
}
|
|
655
844
|
// Core walk function — single-pass async tree walker
|
|
656
845
|
async function resolve(node, ctx) {
|
|
657
846
|
// 1. Primitives pass through
|
|
658
847
|
if (!type.isObject(node) && !type.isArray(node)) return node;
|
|
659
|
-
// 2.
|
|
848
|
+
// 2. Deferred-record placeholder — kind-aware dispatch. Per-consumer kinds
|
|
849
|
+
// (component, menuLinks) pass through every generic walk untouched; only
|
|
850
|
+
// module-ref consumption dereferences them. Single-value kinds (entryRef,
|
|
851
|
+
// varDefault) force the record and splice a clone in place — the memoized
|
|
852
|
+
// value is shared across demanders, and the tree gets mutated downstream.
|
|
853
|
+
if (type.isObject(node)) {
|
|
854
|
+
const deferredId = getPlaceholderId(node);
|
|
855
|
+
if (deferredId !== undefined) {
|
|
856
|
+
const record = getRecord(ctx.buildContext, deferredId);
|
|
857
|
+
if (record.kind === 'component' || record.kind === 'menuLinks') {
|
|
858
|
+
return node;
|
|
859
|
+
}
|
|
860
|
+
return cloneWithMarkers(await resolveDeferred(ctx, deferredId));
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
// 3. _ref — top-down (only operator that needs it)
|
|
660
864
|
if (type.isObject(node) && !type.isUndefined(node._ref)) {
|
|
661
865
|
return resolveRef(node, ctx);
|
|
662
866
|
}
|
|
663
|
-
//
|
|
867
|
+
// 4. Array — walk children in parallel
|
|
664
868
|
if (type.isArray(node)) {
|
|
665
869
|
await Promise.all(node.map(async (item, i)=>{
|
|
666
870
|
node[i] = await resolve(item, ctx.child(String(i)));
|
|
667
871
|
}));
|
|
668
872
|
return node;
|
|
669
873
|
}
|
|
670
|
-
//
|
|
874
|
+
// 5. Object — walk children in parallel (with shouldStop)
|
|
671
875
|
const keys = Object.keys(node);
|
|
672
876
|
await Promise.all(keys.map(async (key)=>{
|
|
673
877
|
if (ctx.shouldStop) {
|
|
@@ -677,16 +881,49 @@ async function resolve(node, ctx) {
|
|
|
677
881
|
delete node[key];
|
|
678
882
|
return;
|
|
679
883
|
}
|
|
680
|
-
if (stopMode === 'preserve') {
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
884
|
+
if (stopMode === 'skip' || stopMode === 'preserve') {
|
|
885
|
+
// Leave raw — a later phase or consumer handles this region.
|
|
886
|
+
// 'preserve' is a legacy synonym: the ~deferredFrom marker it used
|
|
887
|
+
// to stamp is gone (records carry the source file in env.file).
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
if (type.isString(stopMode) && stopMode.startsWith('record:')) {
|
|
891
|
+
// Record-ify: move the raw body into the registry and splice in the
|
|
892
|
+
// placeholder. The body stays untagged, exactly like an in-place
|
|
893
|
+
// preserved body: manifest walks call resolve() directly (no step 15),
|
|
894
|
+
// so consumer provenance is applied at consumption time by
|
|
895
|
+
// loadAndWalkRef's tagRefDeep over the cloned body.
|
|
896
|
+
const body = node[key];
|
|
897
|
+
// Scalars stay in the tree (mirrors 'preserve', which cannot stamp
|
|
898
|
+
// them); a placeholder means the region was already record-ified by
|
|
899
|
+
// an earlier pass — do not create a duplicate.
|
|
900
|
+
if (!type.isObject(body) && !type.isArray(body)) return;
|
|
901
|
+
if (getPlaceholderId(body) !== undefined) return;
|
|
902
|
+
const kind = stopMode.slice('record:'.length);
|
|
903
|
+
const entryId = ctx.entryId;
|
|
904
|
+
const id = makeRecordId({
|
|
905
|
+
entryId,
|
|
906
|
+
configPath: childPath
|
|
907
|
+
});
|
|
908
|
+
createRecord(ctx.buildContext, {
|
|
909
|
+
id,
|
|
910
|
+
kind,
|
|
911
|
+
body,
|
|
912
|
+
env: {
|
|
913
|
+
file: ctx.currentFile,
|
|
914
|
+
moduleRoot: ctx.moduleRoot ?? null,
|
|
915
|
+
packageRoot: ctx.packageRoot ?? null,
|
|
916
|
+
entryId,
|
|
917
|
+
refId: ctx.refId
|
|
918
|
+
}
|
|
919
|
+
});
|
|
920
|
+
node[key] = makePlaceholder(id);
|
|
684
921
|
return;
|
|
685
922
|
}
|
|
686
923
|
}
|
|
687
924
|
node[key] = await resolve(node[key], ctx.child(key));
|
|
688
925
|
}));
|
|
689
|
-
//
|
|
926
|
+
// 6. _var — substitution (children already resolved)
|
|
690
927
|
if (!type.isUndefined(node._var)) {
|
|
691
928
|
try {
|
|
692
929
|
const varResult = resolveVar(node, ctx);
|
|
@@ -699,24 +936,51 @@ async function resolve(node, ctx) {
|
|
|
699
936
|
throw error;
|
|
700
937
|
}
|
|
701
938
|
}
|
|
702
|
-
//
|
|
939
|
+
// 7. _module.var — module variable substitution
|
|
703
940
|
if (!type.isUndefined(node['_module.var'])) {
|
|
704
941
|
if (!ctx.moduleEntry) {
|
|
705
|
-
if (ctx.moduleRoot)
|
|
942
|
+
if (ctx.moduleRoot) {
|
|
943
|
+
// Module scope without an entry = the header parse or the exportables
|
|
944
|
+
// pass. Those walk module-static structure only: a _module.var here
|
|
945
|
+
// would make manifest headers or export ids vary per consumer, which
|
|
946
|
+
// makes by-name lookup order-dependent.
|
|
947
|
+
throw new ConfigError('_module.var cannot be used in manifest headers or component/menu ids — ' + 'these are module-static. Move it inside a component body, page, or api section.', {
|
|
948
|
+
filePath: ctx.currentFile
|
|
949
|
+
});
|
|
950
|
+
}
|
|
706
951
|
throw new ConfigError('_module.var cannot be used at the app level.');
|
|
707
952
|
}
|
|
708
953
|
return resolve(await resolveModuleVar(node, ctx), ctx);
|
|
709
954
|
}
|
|
710
|
-
//
|
|
955
|
+
// 8. _module.*Id — resolve to scoped ID string
|
|
711
956
|
if (isModuleIdOperator(node)) {
|
|
712
957
|
return resolveModuleIdOperator(node, ctx);
|
|
713
958
|
}
|
|
714
|
-
//
|
|
959
|
+
// 9. _build.* operator
|
|
715
960
|
if (isBuildOperator(node)) {
|
|
961
|
+
// Under prepare (deferModuleRefs), an operand subtree can hold a deferred
|
|
962
|
+
// placeholder where a module ref used to be — the operator cannot fold
|
|
963
|
+
// over it. Leave the node unevaluated: the finalize/demand walk (deferral
|
|
964
|
+
// off) splices concrete values via the placeholder dispatch before this
|
|
965
|
+
// branch runs, and the fold happens then, in the same enclosing scope.
|
|
966
|
+
if (ctx.deferModuleRefs && findPlaceholderInSubtree(node)) {
|
|
967
|
+
return node;
|
|
968
|
+
}
|
|
716
969
|
const result = evaluateBuildOperator(node, ctx);
|
|
717
970
|
tagRefDeep(result, ctx.refId);
|
|
718
971
|
return result;
|
|
719
972
|
}
|
|
720
973
|
return node;
|
|
721
974
|
}
|
|
722
|
-
|
|
975
|
+
// Does any node in the subtree carry a deferred-record placeholder?
|
|
976
|
+
function findPlaceholderInSubtree(node) {
|
|
977
|
+
if (getPlaceholderId(node) !== undefined) return true;
|
|
978
|
+
if (type.isArray(node)) {
|
|
979
|
+
return node.some((item)=>findPlaceholderInSubtree(item));
|
|
980
|
+
}
|
|
981
|
+
if (type.isObject(node)) {
|
|
982
|
+
return Object.keys(node).some((key)=>findPlaceholderInSubtree(node[key]));
|
|
983
|
+
}
|
|
984
|
+
return false;
|
|
985
|
+
}
|
|
986
|
+
export { resolve, loadAndWalkRef, WalkContext, tagRefDeep };
|