@lowdefy/build 5.5.1 → 6.0.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 +70 -17
- 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 +37 -0
- package/dist/build/buildApi/validateEndpoint.js +96 -1
- package/dist/build/buildApp.js +13 -20
- package/dist/build/buildAppMeta.js +130 -0
- package/dist/build/buildAuth/buildAgentAuth.js +69 -0
- package/dist/build/buildAuth/buildAuth.js +14 -0
- 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/getAgentRoles.js +39 -0
- package/dist/build/buildAuth/getProtectedAgents.js +29 -0
- package/dist/build/buildAuth/getProtectedWebsockets.js +29 -0
- package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
- package/dist/build/buildAuth/rejectReservedEntityIds.js +52 -0
- package/dist/build/buildAuth/validateAuthConfig.js +15 -3
- 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/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 +197 -22
- 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 +19 -1
- 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/getKey.js +16 -5
- 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 +490 -209
- 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/fetchModules.js +18 -4
- 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 +81 -1
- package/dist/build/jit/writePageJit.js +11 -8
- package/dist/build/registerModules.js +149 -42
- package/dist/build/validateCallAgentSteps.js +58 -0
- package/dist/build/validateConfig.js +4 -0
- package/dist/build/validateCronConfig.js +110 -0
- package/dist/build/validateRenderNotificationSteps.js +50 -0
- package/dist/build/writeApi.js +58 -1
- package/dist/build/writeI18n.js +8 -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 -11
- 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 +689 -386
- package/dist/dynamic/buildDynamicBlocks.js +120 -0
- package/dist/index.js +78 -8
- package/dist/indexDev.js +4 -0
- package/dist/lowdefySchema.js +861 -6
- package/dist/scripts/generateDefaultTypes.js +5 -2
- package/dist/test-utils/expectTerminates.js +33 -0
- package/dist/test-utils/runBuild.js +20 -1
- package/dist/test-utils/runBuildForSnapshots.js +34 -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/getCronEnvironmentNames.js +25 -0
- package/dist/utils/logCollectedErrors.js +44 -10
- package/dist/utils/serializeBuildException.js +29 -0
- package/dist/utils/validateCronExpression.js +131 -0
- package/dist/utils/validateId.js +11 -0
- package/dist/utils/writeBuildArtifact.js +5 -2
- package/package.json +66 -59
- 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,
|
|
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) {
|
|
@@ -203,26 +174,46 @@ function evaluateBuildOperator(node, ctx) {
|
|
|
203
174
|
}
|
|
204
175
|
return output;
|
|
205
176
|
}
|
|
177
|
+
// _var keys are author-written YAML, so a reserved name (e.g. "constructor") is a config
|
|
178
|
+
// mistake, not a missing var. Wraps both call shapes for that check. The object form call
|
|
179
|
+
// site below passes no `options` argument at all, so a missing key comes back from `get` as
|
|
180
|
+
// `undefined` instead of a default — resolveVar relies on that `undefined` to tell "var not
|
|
181
|
+
// provided" apart from "var provided as null".
|
|
182
|
+
function readVar(key, ctx, options) {
|
|
183
|
+
try {
|
|
184
|
+
return get(ctx.vars, key, options);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (!(error instanceof ReservedKeyError)) throw error;
|
|
187
|
+
throw new ConfigError(`_var key "${key}" is a reserved name.`, {
|
|
188
|
+
cause: error,
|
|
189
|
+
filePath: ctx.currentFile
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
206
193
|
// Resolve a _var node
|
|
207
194
|
function resolveVar(node, ctx) {
|
|
208
195
|
const varDef = node._var;
|
|
209
196
|
// String form: { _var: "key" }
|
|
210
197
|
if (type.isString(varDef)) {
|
|
211
|
-
const value =
|
|
198
|
+
const value = readVar(varDef, ctx, {
|
|
212
199
|
default: null
|
|
213
200
|
});
|
|
214
|
-
return
|
|
201
|
+
return cloneWithMarkers(value, {
|
|
202
|
+
assignRefId: ctx.sourceRefId
|
|
203
|
+
});
|
|
215
204
|
}
|
|
216
205
|
// Object form: { _var: { key, default } }
|
|
217
206
|
if (type.isObject(varDef) && type.isString(varDef.key)) {
|
|
218
|
-
const varFromParent =
|
|
207
|
+
const varFromParent = readVar(varDef.key, ctx);
|
|
219
208
|
// Var provided (even if null) → use parent's sourceRefId for location
|
|
220
209
|
if (!type.isUndefined(varFromParent)) {
|
|
221
|
-
return
|
|
210
|
+
return cloneWithMarkers(varFromParent, {
|
|
211
|
+
assignRefId: ctx.sourceRefId
|
|
212
|
+
});
|
|
222
213
|
}
|
|
223
214
|
// Not provided → use default, preserve template's ~r
|
|
224
215
|
const defaultValue = type.isNone(varDef.default) ? null : varDef.default;
|
|
225
|
-
return
|
|
216
|
+
return cloneWithMarkers(defaultValue);
|
|
226
217
|
}
|
|
227
218
|
throw new ConfigError('_var operator takes a string or object with "key" field as arguments.', {
|
|
228
219
|
filePath: ctx.currentFile
|
|
@@ -237,7 +228,9 @@ async function resolveModuleVar(node, ctx) {
|
|
|
237
228
|
});
|
|
238
229
|
}
|
|
239
230
|
const value = await resolveEffectiveVar(key, ctx.moduleEntry, ctx);
|
|
240
|
-
return
|
|
231
|
+
return cloneWithMarkers(value, {
|
|
232
|
+
assignRefId: ctx.sourceRefId
|
|
233
|
+
});
|
|
241
234
|
}
|
|
242
235
|
// Navigate the var definitions tree by dot-path key, following `properties` nesting.
|
|
243
236
|
function getVarDef(varDefs, key) {
|
|
@@ -258,27 +251,27 @@ function getVarDef(varDefs, key) {
|
|
|
258
251
|
// at the module manifest. The fresh context prevents false circular-ref detection
|
|
259
252
|
// from the consumer's refChain and ensures _ref paths resolve relative to the
|
|
260
253
|
// module root.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
}
|
|
281
|
-
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;
|
|
282
275
|
}
|
|
283
276
|
// Build a merged object for namespace vars (vars with `properties`). Each
|
|
284
277
|
// declared property resolves through resolveEffectiveVar — consumer values
|
|
@@ -291,22 +284,53 @@ async function resolveNamespaceVar(prefix, varDef, moduleEntry, ctx) {
|
|
|
291
284
|
}
|
|
292
285
|
return result;
|
|
293
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
|
+
}
|
|
294
314
|
// Core lazy var resolution with caching on the module entry.
|
|
295
315
|
async function resolveEffectiveVar(key, moduleEntry, ctx) {
|
|
296
316
|
if (Object.hasOwn(moduleEntry.resolvedVarCache, key)) {
|
|
297
317
|
return moduleEntry.resolvedVarCache[key];
|
|
298
318
|
}
|
|
299
|
-
const consumerValue =
|
|
300
|
-
default: undefined
|
|
301
|
-
});
|
|
319
|
+
const consumerValue = await readConsumerValue(moduleEntry, key, ctx);
|
|
302
320
|
const varDef = getVarDef(moduleEntry.varDefs, key);
|
|
303
321
|
let result;
|
|
304
322
|
if (varDef?.properties) {
|
|
305
323
|
result = await resolveNamespaceVar(key, varDef, moduleEntry, ctx);
|
|
306
324
|
} else if (!type.isNone(consumerValue)) {
|
|
307
|
-
|
|
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);
|
|
308
328
|
} else if (varDef && !type.isUndefined(varDef.default)) {
|
|
309
|
-
|
|
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;
|
|
310
334
|
} else {
|
|
311
335
|
result = null;
|
|
312
336
|
}
|
|
@@ -318,6 +342,8 @@ const MODULE_ID_OPERATOR_KEYS = [
|
|
|
318
342
|
'_module.pageId',
|
|
319
343
|
'_module.connectionId',
|
|
320
344
|
'_module.endpointId',
|
|
345
|
+
'_module.agentId',
|
|
346
|
+
'_module.notificationId',
|
|
321
347
|
'_module.id'
|
|
322
348
|
];
|
|
323
349
|
function isModuleIdOperator(node) {
|
|
@@ -348,16 +374,28 @@ function resolveModulePageId(arg, moduleEntry, context, configKey) {
|
|
|
348
374
|
});
|
|
349
375
|
}
|
|
350
376
|
// Resolve _module.connectionId
|
|
351
|
-
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
|
+
};
|
|
352
390
|
if (type.isString(arg)) {
|
|
353
391
|
if (!moduleEntry) {
|
|
354
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.', {
|
|
355
393
|
configKey
|
|
356
394
|
});
|
|
357
395
|
}
|
|
358
|
-
const
|
|
359
|
-
if (
|
|
360
|
-
return
|
|
396
|
+
const remapValue = await readRemapValue(moduleEntry, arg);
|
|
397
|
+
if (remapValue) {
|
|
398
|
+
return remapValue;
|
|
361
399
|
}
|
|
362
400
|
return `${moduleEntry.id}/${arg}`;
|
|
363
401
|
}
|
|
@@ -369,9 +407,9 @@ function resolveModuleConnectionId(arg, moduleEntry, context, configKey) {
|
|
|
369
407
|
configKey,
|
|
370
408
|
usage: `_module.connectionId { id: "${arg.id}", module: "${arg.module}" }`
|
|
371
409
|
});
|
|
372
|
-
const
|
|
373
|
-
if (
|
|
374
|
-
return
|
|
410
|
+
const remapValue = await readRemapValue(targetEntry, arg.id);
|
|
411
|
+
if (remapValue) {
|
|
412
|
+
return remapValue;
|
|
375
413
|
}
|
|
376
414
|
return `${targetEntry.id}/${arg.id}`;
|
|
377
415
|
}
|
|
@@ -403,6 +441,54 @@ function resolveModuleEndpointId(arg, moduleEntry, context, configKey) {
|
|
|
403
441
|
configKey
|
|
404
442
|
});
|
|
405
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
|
+
}
|
|
406
492
|
// Resolve _module.id
|
|
407
493
|
function resolveModuleId(arg, moduleEntry, context, configKey) {
|
|
408
494
|
if (!type.isObject(arg)) {
|
|
@@ -428,7 +514,7 @@ function resolveModuleId(arg, moduleEntry, context, configKey) {
|
|
|
428
514
|
});
|
|
429
515
|
}
|
|
430
516
|
// Dispatch _module.*Id operators
|
|
431
|
-
function resolveModuleIdOperator(node, ctx) {
|
|
517
|
+
async function resolveModuleIdOperator(node, ctx) {
|
|
432
518
|
const { moduleEntry } = ctx;
|
|
433
519
|
const context = ctx.buildContext;
|
|
434
520
|
const configKey = node['~k'];
|
|
@@ -436,18 +522,61 @@ function resolveModuleIdOperator(node, ctx) {
|
|
|
436
522
|
return resolveModulePageId(node['_module.pageId'], moduleEntry, context, configKey);
|
|
437
523
|
}
|
|
438
524
|
if (!type.isUndefined(node['_module.connectionId'])) {
|
|
439
|
-
|
|
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);
|
|
440
560
|
}
|
|
441
561
|
if (!type.isUndefined(node['_module.endpointId'])) {
|
|
442
562
|
return resolveModuleEndpointId(node['_module.endpointId'], moduleEntry, context, configKey);
|
|
443
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
|
+
}
|
|
444
570
|
if (!type.isUndefined(node['_module.id'])) {
|
|
445
571
|
return resolveModuleId(node['_module.id'], moduleEntry, context, configKey);
|
|
446
572
|
}
|
|
447
573
|
return node;
|
|
448
574
|
}
|
|
449
|
-
//
|
|
450
|
-
|
|
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) {
|
|
451
580
|
// 1. Create ref definition
|
|
452
581
|
const lineNumber = node['~l'];
|
|
453
582
|
const refDef = makeRefDefinition(node._ref, ctx.refId, ctx.refMap, lineNumber, ctx.path);
|
|
@@ -456,19 +585,32 @@ async function resolveRef(node, ctx) {
|
|
|
456
585
|
const varKeys = Object.keys(refDef.vars);
|
|
457
586
|
if (varKeys.length > 0) {
|
|
458
587
|
ctx.unresolvedRefVars[refDef.id] = refDef.vars;
|
|
459
|
-
refDef.vars =
|
|
588
|
+
refDef.vars = cloneWithMarkers(refDef.vars);
|
|
460
589
|
}
|
|
461
590
|
// 3. Resolve dynamic path/vars/key
|
|
462
591
|
if (type.isObject(refDef.path)) {
|
|
463
|
-
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
|
+
}));
|
|
464
611
|
}
|
|
465
|
-
await Promise.all(varKeys.map(async (varKey)=>{
|
|
466
|
-
if (type.isObject(refDef.vars[varKey]) || type.isArray(refDef.vars[varKey])) {
|
|
467
|
-
refDef.vars[varKey] = await resolve(refDef.vars[varKey], ctx);
|
|
468
|
-
}
|
|
469
|
-
}));
|
|
470
612
|
if (type.isObject(refDef.key)) {
|
|
471
|
-
refDef.key = await resolve(
|
|
613
|
+
refDef.key = await resolve(cloneWithMarkers(refDef.key), ctx);
|
|
472
614
|
}
|
|
473
615
|
// 4. Module path resolution: resolve relative paths from the module root
|
|
474
616
|
rebaseModuleRefPaths({
|
|
@@ -504,113 +646,133 @@ async function resolveRef(node, ctx) {
|
|
|
504
646
|
lineNumber: ctx.currentFile ? lineNumber : null
|
|
505
647
|
});
|
|
506
648
|
}
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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.
|
|
510
741
|
try {
|
|
511
|
-
// 8. Load content
|
|
512
742
|
let content;
|
|
513
|
-
let resolvedEntryId = null;
|
|
514
743
|
if (refDef.module) {
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
referencedFrom: ctx.currentFile,
|
|
519
|
-
walkCtx: ctx,
|
|
520
|
-
configKey: node['~k']
|
|
744
|
+
content = await resolveModuleExportRef(refDef, ctx, {
|
|
745
|
+
configKey,
|
|
746
|
+
fromFile
|
|
521
747
|
});
|
|
522
|
-
content = cloneForResolve(result.content);
|
|
523
|
-
resolvedEntryId = result.entryId;
|
|
524
748
|
} else {
|
|
749
|
+
// 8. Load content
|
|
525
750
|
content = await getRefContent({
|
|
526
751
|
context: ctx.buildContext,
|
|
527
752
|
refDef,
|
|
528
|
-
referencedFrom:
|
|
753
|
+
referencedFrom: fromFile
|
|
529
754
|
});
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
// File-based cycle detection (step 7) misses these because each module
|
|
533
|
-
// has a different file path. Use a synthetic key with the resolved
|
|
534
|
-
// concrete entry ID: "module:<entryId>/<type>:<name>".
|
|
535
|
-
if (resolvedEntryId && (refDef.component || refDef.menu)) {
|
|
536
|
-
const exportType = refDef.component ? 'component' : 'menu';
|
|
537
|
-
const exportName = refDef.component ?? refDef.menu;
|
|
538
|
-
const cycleKey = `module:${resolvedEntryId}/${exportType}:${exportName}`;
|
|
539
|
-
if (ctx.refChain.has(cycleKey)) {
|
|
540
|
-
const chainDisplay = [
|
|
541
|
-
...ctx.refChain,
|
|
542
|
-
cycleKey
|
|
543
|
-
].join('\n -> ');
|
|
544
|
-
throw new ConfigError(`Circular module reference detected. Module "${resolvedEntryId}" ${exportType} "${exportName}" ` + `references itself through:\n -> ${chainDisplay}`, {
|
|
545
|
-
filePath: ctx.currentFile
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
// 10. Create child context for the ref
|
|
550
|
-
let childCtx;
|
|
551
|
-
if (refDef.module && (refDef.component || refDef.menu)) {
|
|
552
|
-
const moduleEntry = ctx.buildContext.modules[resolvedEntryId];
|
|
553
|
-
const deferredFrom = content['~deferredFrom'];
|
|
554
|
-
const exportType = refDef.component ? 'component' : 'menu';
|
|
555
|
-
const exportName = refDef.component ?? refDef.menu;
|
|
556
|
-
const cycleKey = `module:${resolvedEntryId}/${exportType}:${exportName}`;
|
|
557
|
-
childCtx = ctx.forRef({
|
|
558
|
-
refId: refDef.id,
|
|
559
|
-
vars: refDef.vars,
|
|
560
|
-
filePath: deferredFrom ?? path.join(moduleEntry.moduleRoot, 'module.lowdefy.yaml'),
|
|
561
|
-
moduleRoot: moduleEntry.moduleRoot,
|
|
562
|
-
packageRoot: moduleEntry.packageRoot,
|
|
563
|
-
moduleDependencies: moduleEntry.moduleDependencies,
|
|
564
|
-
moduleEntry,
|
|
565
|
-
extraRefChainKeys: [
|
|
566
|
-
cycleKey
|
|
567
|
-
]
|
|
568
|
-
});
|
|
569
|
-
// Clone so each consumer gets an independent copy — getModuleRefContent
|
|
570
|
-
// returns a shared reference, and resolve() mutates in place.
|
|
571
|
-
// deferredFrom was read above before the clone (serializer.copy strips
|
|
572
|
-
// non-enumerable properties).
|
|
573
|
-
content = serializer.copy(content);
|
|
574
|
-
// When component/menu content is a file _ref, the inner ref would create
|
|
575
|
-
// a fresh var scope and lose the consumer's vars. Inject them into the clone.
|
|
576
|
-
if ((refDef.component || refDef.menu) && type.isObject(content) && content._ref) {
|
|
577
|
-
if (type.isObject(content._ref)) {
|
|
578
|
-
content._ref.vars = {
|
|
579
|
-
...content._ref.vars ?? {},
|
|
580
|
-
...refDef.vars
|
|
581
|
-
};
|
|
582
|
-
} else if (type.isString(content._ref) && Object.keys(refDef.vars).length > 0) {
|
|
583
|
-
content._ref = {
|
|
584
|
-
path: content._ref,
|
|
585
|
-
vars: refDef.vars
|
|
586
|
-
};
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
} else {
|
|
590
|
-
childCtx = ctx.forRef({
|
|
755
|
+
// 10. Create child context for the ref
|
|
756
|
+
const childCtx = ctx.forRef({
|
|
591
757
|
refId: refDef.id,
|
|
592
758
|
vars: refDef.vars,
|
|
593
759
|
filePath: refDef.path
|
|
594
760
|
});
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
content = await resolve(content, childCtx);
|
|
598
|
-
// 12. Scope menu item IDs (module menu refs only)
|
|
599
|
-
if (refDef.module && refDef.menu) {
|
|
600
|
-
const moduleEntry = ctx.buildContext.modules[resolvedEntryId];
|
|
601
|
-
scopeMenuItemIds(content, moduleEntry.id);
|
|
761
|
+
// 11. Walk the content
|
|
762
|
+
content = await resolve(content, childCtx);
|
|
602
763
|
}
|
|
603
764
|
// 13. Run transformer
|
|
604
765
|
content = await runTransformer({
|
|
605
766
|
context: ctx.buildContext,
|
|
606
767
|
input: content,
|
|
607
768
|
refDef,
|
|
608
|
-
referencedFrom:
|
|
769
|
+
referencedFrom: fromFile
|
|
609
770
|
});
|
|
610
771
|
// 14. Extract key
|
|
611
772
|
content = getKey({
|
|
612
773
|
input: content,
|
|
613
|
-
refDef
|
|
774
|
+
refDef,
|
|
775
|
+
filePath: fromFile
|
|
614
776
|
});
|
|
615
777
|
// 15. Tag all nodes with ~r for provenance
|
|
616
778
|
tagRefDeep(content, refDef.id);
|
|
@@ -635,22 +797,81 @@ async function resolveRef(node, ctx) {
|
|
|
635
797
|
throw error;
|
|
636
798
|
}
|
|
637
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
|
+
}
|
|
638
844
|
// Core walk function — single-pass async tree walker
|
|
639
845
|
async function resolve(node, ctx) {
|
|
640
846
|
// 1. Primitives pass through
|
|
641
847
|
if (!type.isObject(node) && !type.isArray(node)) return node;
|
|
642
|
-
// 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)
|
|
643
864
|
if (type.isObject(node) && !type.isUndefined(node._ref)) {
|
|
644
865
|
return resolveRef(node, ctx);
|
|
645
866
|
}
|
|
646
|
-
//
|
|
867
|
+
// 4. Array — walk children in parallel
|
|
647
868
|
if (type.isArray(node)) {
|
|
648
869
|
await Promise.all(node.map(async (item, i)=>{
|
|
649
870
|
node[i] = await resolve(item, ctx.child(String(i)));
|
|
650
871
|
}));
|
|
651
872
|
return node;
|
|
652
873
|
}
|
|
653
|
-
//
|
|
874
|
+
// 5. Object — walk children in parallel (with shouldStop)
|
|
654
875
|
const keys = Object.keys(node);
|
|
655
876
|
await Promise.all(keys.map(async (key)=>{
|
|
656
877
|
if (ctx.shouldStop) {
|
|
@@ -660,16 +881,49 @@ async function resolve(node, ctx) {
|
|
|
660
881
|
delete node[key];
|
|
661
882
|
return;
|
|
662
883
|
}
|
|
663
|
-
if (stopMode === 'preserve') {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
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);
|
|
667
921
|
return;
|
|
668
922
|
}
|
|
669
923
|
}
|
|
670
924
|
node[key] = await resolve(node[key], ctx.child(key));
|
|
671
925
|
}));
|
|
672
|
-
//
|
|
926
|
+
// 6. _var — substitution (children already resolved)
|
|
673
927
|
if (!type.isUndefined(node._var)) {
|
|
674
928
|
try {
|
|
675
929
|
const varResult = resolveVar(node, ctx);
|
|
@@ -682,24 +936,51 @@ async function resolve(node, ctx) {
|
|
|
682
936
|
throw error;
|
|
683
937
|
}
|
|
684
938
|
}
|
|
685
|
-
//
|
|
939
|
+
// 7. _module.var — module variable substitution
|
|
686
940
|
if (!type.isUndefined(node['_module.var'])) {
|
|
687
941
|
if (!ctx.moduleEntry) {
|
|
688
|
-
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
|
+
}
|
|
689
951
|
throw new ConfigError('_module.var cannot be used at the app level.');
|
|
690
952
|
}
|
|
691
953
|
return resolve(await resolveModuleVar(node, ctx), ctx);
|
|
692
954
|
}
|
|
693
|
-
//
|
|
955
|
+
// 8. _module.*Id — resolve to scoped ID string
|
|
694
956
|
if (isModuleIdOperator(node)) {
|
|
695
957
|
return resolveModuleIdOperator(node, ctx);
|
|
696
958
|
}
|
|
697
|
-
//
|
|
959
|
+
// 9. _build.* operator
|
|
698
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
|
+
}
|
|
699
969
|
const result = evaluateBuildOperator(node, ctx);
|
|
700
970
|
tagRefDeep(result, ctx.refId);
|
|
701
971
|
return result;
|
|
702
972
|
}
|
|
703
973
|
return node;
|
|
704
974
|
}
|
|
705
|
-
|
|
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 };
|