@lowdefy/build 5.1.0 → 5.2.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/buildApi/buildRoutine/countStepTypes.js +3 -0
- package/dist/build/buildApi/buildRoutine/setStepId.js +3 -2
- package/dist/build/buildApi/buildRoutine/validateStep.js +19 -0
- package/dist/build/buildApi/validateEndpoint.js +10 -0
- package/dist/build/buildApi/validateStepReferences.js +4 -4
- package/dist/build/buildAuth/buildApiAuth.js +2 -1
- package/dist/build/buildAuth/buildPageAuth.js +2 -1
- package/dist/build/buildAuth/getApiRoles.js +12 -6
- package/dist/build/buildAuth/getPageRoles.js +12 -6
- package/dist/build/buildAuth/getProtectedApi.js +3 -2
- package/dist/build/buildAuth/getProtectedPages.js +3 -2
- package/dist/build/buildAuth/matchPattern.js +22 -0
- package/dist/build/buildConnections.js +42 -4
- package/dist/build/buildJs/jsMapParser.js +25 -12
- package/dist/build/buildJs/writeJs.js +2 -2
- package/dist/build/buildMenu.js +41 -0
- package/dist/build/buildModuleDefs.js +97 -0
- package/dist/build/buildModules.js +96 -0
- package/dist/build/buildPages/buildBlock/buildBlock.js +2 -2
- package/dist/build/buildPages/buildBlock/buildEvents.js +16 -1
- package/dist/build/buildPages/buildBlock/buildSubBlocks.js +2 -1
- package/dist/build/buildPages/buildBlock/validateBlock.js +3 -3
- package/dist/build/buildPages/buildPage.js +1 -0
- package/dist/build/buildPages/validateCallApiRefs.js +31 -0
- package/dist/build/buildRefs/getModuleRefContent.js +81 -0
- package/dist/build/buildRefs/makeRefDefinition.js +6 -0
- package/dist/build/buildRefs/walker.js +424 -44
- package/dist/build/fetchGitHubModule.js +94 -0
- package/dist/build/fetchModules.js +60 -0
- package/dist/build/full/buildPages.js +10 -1
- package/dist/build/full/writePages.js +1 -1
- package/dist/build/jit/buildPageJit.js +34 -4
- package/dist/build/jit/collectSkeletonSourceFiles.js +8 -0
- package/dist/build/jit/createPageRegistry.js +10 -1
- package/dist/build/jit/shallowBuild.js +22 -11
- package/dist/build/jit/writePageJit.js +2 -2
- package/dist/build/jit/writeSourcelessPages.js +1 -1
- package/dist/build/parseModuleSource.js +48 -0
- package/dist/build/registerModules.js +242 -0
- package/dist/build/resolveDepTarget.js +43 -0
- package/dist/build/resolveModuleDependencies.js +60 -0
- package/dist/build/resolveModuleOperators.js +27 -0
- package/dist/build/testSchema.js +22 -11
- package/dist/build/writePluginImports/writeGlobalsCss.js +1 -1
- package/dist/createContext.js +4 -0
- package/dist/defaultPackages.js +51 -0
- package/dist/defaultTypesMap.js +399 -357
- package/dist/index.js +16 -1
- package/dist/indexDev.js +3 -1
- package/dist/lowdefySchema.js +58 -0
- package/dist/scripts/generateDefaultTypes.js +1 -35
- package/package.json +46 -42
- package/dist/build/jit/stripPageContent.js +0 -29
|
@@ -12,13 +12,17 @@
|
|
|
12
12
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
|
-
*/ import
|
|
15
|
+
*/ import path from 'path';
|
|
16
|
+
import { get, serializer, type } from '@lowdefy/helpers';
|
|
16
17
|
import { ConfigError } from '@lowdefy/errors';
|
|
17
18
|
import { evaluateOperators } from '@lowdefy/operators';
|
|
18
19
|
import makeRefDefinition from './makeRefDefinition.js';
|
|
19
20
|
import getRefContent from './getRefContent.js';
|
|
21
|
+
import getModuleRefContent from './getModuleRefContent.js';
|
|
20
22
|
import runTransformer from './runTransformer.js';
|
|
21
23
|
import getKey from './getKey.js';
|
|
24
|
+
import { scopeMenuItemIds } from '../resolveModuleOperators.js';
|
|
25
|
+
import resolveDepTarget from '../resolveDepTarget.js';
|
|
22
26
|
import setNonEnumerableProperty from '../../utils/setNonEnumerableProperty.js';
|
|
23
27
|
import collectExceptions from '../../utils/collectExceptions.js';
|
|
24
28
|
let WalkContext = class WalkContext {
|
|
@@ -28,6 +32,10 @@ let WalkContext = class WalkContext {
|
|
|
28
32
|
refId: this.refId,
|
|
29
33
|
sourceRefId: this.sourceRefId,
|
|
30
34
|
vars: this.vars,
|
|
35
|
+
moduleDependencies: this.moduleDependencies,
|
|
36
|
+
moduleEntry: this.moduleEntry,
|
|
37
|
+
moduleRoot: this.moduleRoot,
|
|
38
|
+
packageRoot: this.packageRoot,
|
|
31
39
|
path: this.path ? `${this.path}.${segment}` : segment,
|
|
32
40
|
currentFile: this.currentFile,
|
|
33
41
|
refChain: this.refChain,
|
|
@@ -37,16 +45,25 @@ let WalkContext = class WalkContext {
|
|
|
37
45
|
shouldStop: this.shouldStop
|
|
38
46
|
});
|
|
39
47
|
}
|
|
40
|
-
forRef({ refId, vars, filePath }) {
|
|
48
|
+
forRef({ refId, vars, filePath, moduleRoot, packageRoot, moduleDependencies, moduleEntry, extraRefChainKeys }) {
|
|
41
49
|
const newChain = new Set(this.refChain);
|
|
42
50
|
if (filePath) {
|
|
43
51
|
newChain.add(filePath);
|
|
44
52
|
}
|
|
53
|
+
if (extraRefChainKeys) {
|
|
54
|
+
for (const key of extraRefChainKeys){
|
|
55
|
+
newChain.add(key);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
45
58
|
return new WalkContext({
|
|
46
59
|
buildContext: this.buildContext,
|
|
47
60
|
refId,
|
|
48
61
|
sourceRefId: this.refId,
|
|
49
62
|
vars: vars ?? {},
|
|
63
|
+
moduleDependencies: moduleDependencies ?? this.moduleDependencies,
|
|
64
|
+
moduleEntry: moduleEntry ?? this.moduleEntry,
|
|
65
|
+
moduleRoot: moduleRoot ?? this.moduleRoot,
|
|
66
|
+
packageRoot: packageRoot ?? this.packageRoot,
|
|
50
67
|
path: this.path,
|
|
51
68
|
currentFile: filePath ?? this.currentFile,
|
|
52
69
|
refChain: newChain,
|
|
@@ -65,11 +82,15 @@ let WalkContext = class WalkContext {
|
|
|
65
82
|
get unresolvedRefVars() {
|
|
66
83
|
return this.buildContext.unresolvedRefVars;
|
|
67
84
|
}
|
|
68
|
-
constructor({ buildContext, refId, sourceRefId, vars, path, currentFile, refChain, operators, env, dynamicIdentifiers, shouldStop }){
|
|
85
|
+
constructor({ buildContext, refId, sourceRefId, vars, moduleDependencies, moduleEntry, moduleRoot, packageRoot, path, currentFile, refChain, operators, env, dynamicIdentifiers, shouldStop }){
|
|
69
86
|
this.buildContext = buildContext;
|
|
70
87
|
this.refId = refId;
|
|
71
88
|
this.sourceRefId = sourceRefId;
|
|
72
89
|
this.vars = vars;
|
|
90
|
+
this.moduleDependencies = moduleDependencies;
|
|
91
|
+
this.moduleEntry = moduleEntry ?? null;
|
|
92
|
+
this.moduleRoot = moduleRoot;
|
|
93
|
+
this.packageRoot = packageRoot;
|
|
73
94
|
this.path = path;
|
|
74
95
|
this.currentFile = currentFile;
|
|
75
96
|
this.refChain = refChain;
|
|
@@ -108,7 +129,7 @@ function tagRefDeep(node, refId) {
|
|
|
108
129
|
}
|
|
109
130
|
}
|
|
110
131
|
}
|
|
111
|
-
// Deep clone preserving ~r, ~l, ~k
|
|
132
|
+
// Deep clone preserving non-enumerable build markers (~r, ~l, ~k, ~arr, ~deferredFrom).
|
|
112
133
|
// Used before resolving ref def path/vars to prevent mutation of stored originals.
|
|
113
134
|
function cloneForResolve(value) {
|
|
114
135
|
if (!type.isObject(value) && !type.isArray(value)) return value;
|
|
@@ -118,6 +139,7 @@ function cloneForResolve(value) {
|
|
|
118
139
|
if (value['~l'] !== undefined) setNonEnumerableProperty(clone, '~l', value['~l']);
|
|
119
140
|
if (value['~k'] !== undefined) setNonEnumerableProperty(clone, '~k', value['~k']);
|
|
120
141
|
if (value['~arr'] !== undefined) setNonEnumerableProperty(clone, '~arr', value['~arr']);
|
|
142
|
+
if (value['~deferredFrom'] !== undefined) setNonEnumerableProperty(clone, '~deferredFrom', value['~deferredFrom']);
|
|
121
143
|
return clone;
|
|
122
144
|
}
|
|
123
145
|
const clone = {};
|
|
@@ -127,6 +149,7 @@ function cloneForResolve(value) {
|
|
|
127
149
|
if (value['~r'] !== undefined) setNonEnumerableProperty(clone, '~r', value['~r']);
|
|
128
150
|
if (value['~l'] !== undefined) setNonEnumerableProperty(clone, '~l', value['~l']);
|
|
129
151
|
if (value['~k'] !== undefined) setNonEnumerableProperty(clone, '~k', value['~k']);
|
|
152
|
+
if (value['~deferredFrom'] !== undefined) setNonEnumerableProperty(clone, '~deferredFrom', value['~deferredFrom']);
|
|
130
153
|
return clone;
|
|
131
154
|
}
|
|
132
155
|
// Deep clone a var value, preserving markers and setting ~r provenance.
|
|
@@ -204,7 +227,258 @@ function resolveVar(node, ctx) {
|
|
|
204
227
|
filePath: ctx.currentFile
|
|
205
228
|
});
|
|
206
229
|
}
|
|
207
|
-
// Resolve a
|
|
230
|
+
// Resolve a _module.var node via lazy resolution against the module entry.
|
|
231
|
+
async function resolveModuleVar(node, ctx) {
|
|
232
|
+
const key = node['_module.var'];
|
|
233
|
+
if (!type.isString(key)) {
|
|
234
|
+
throw new ConfigError('_module.var operator takes a string argument.', {
|
|
235
|
+
filePath: ctx.currentFile
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const value = await resolveEffectiveVar(key, ctx.moduleEntry, ctx);
|
|
239
|
+
return cloneVarValue(value, ctx.sourceRefId);
|
|
240
|
+
}
|
|
241
|
+
// Navigate the var definitions tree by dot-path key, following `properties` nesting.
|
|
242
|
+
function getVarDef(varDefs, key) {
|
|
243
|
+
const parts = key.split('.');
|
|
244
|
+
let current = varDefs;
|
|
245
|
+
for(let i = 0; i < parts.length; i++){
|
|
246
|
+
const part = parts[i];
|
|
247
|
+
if (!current?.[part]) return undefined;
|
|
248
|
+
if (current[part].properties && i < parts.length - 1) {
|
|
249
|
+
current = current[part].properties;
|
|
250
|
+
} else {
|
|
251
|
+
return current[part];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return undefined;
|
|
255
|
+
}
|
|
256
|
+
// Resolve a raw var default through the walker using a fresh WalkContext rooted
|
|
257
|
+
// at the module manifest. The fresh context prevents false circular-ref detection
|
|
258
|
+
// from the consumer's refChain and ensures _ref paths resolve relative to the
|
|
259
|
+
// module root.
|
|
260
|
+
async function resolveVarDefault(rawDefault, moduleEntry, ctx) {
|
|
261
|
+
const moduleYamlPath = path.join(moduleEntry.moduleRoot, 'module.lowdefy.yaml');
|
|
262
|
+
const defaultCtx = new WalkContext({
|
|
263
|
+
buildContext: ctx.buildContext,
|
|
264
|
+
refId: moduleEntry.refDef.id,
|
|
265
|
+
sourceRefId: null,
|
|
266
|
+
vars: {},
|
|
267
|
+
moduleDependencies: moduleEntry.moduleDependencies,
|
|
268
|
+
moduleEntry,
|
|
269
|
+
moduleRoot: moduleEntry.moduleRoot,
|
|
270
|
+
packageRoot: moduleEntry.packageRoot,
|
|
271
|
+
path: '',
|
|
272
|
+
currentFile: moduleYamlPath,
|
|
273
|
+
refChain: new Set(moduleEntry.refDef.path ? [
|
|
274
|
+
moduleEntry.refDef.path
|
|
275
|
+
] : []),
|
|
276
|
+
operators: ctx.operators,
|
|
277
|
+
env: ctx.env,
|
|
278
|
+
dynamicIdentifiers: ctx.dynamicIdentifiers
|
|
279
|
+
});
|
|
280
|
+
return await resolve(rawDefault, defaultCtx);
|
|
281
|
+
}
|
|
282
|
+
// Build a merged object for namespace vars (vars with `properties`). Each
|
|
283
|
+
// declared property resolves through resolveEffectiveVar — consumer values
|
|
284
|
+
// take precedence per-leaf; missing leaves fall back to defaults.
|
|
285
|
+
async function resolveNamespaceVar(prefix, varDef, moduleEntry, ctx) {
|
|
286
|
+
const result = {};
|
|
287
|
+
for (const propName of Object.keys(varDef.properties)){
|
|
288
|
+
const fullKey = `${prefix}.${propName}`;
|
|
289
|
+
result[propName] = await resolveEffectiveVar(fullKey, moduleEntry, ctx);
|
|
290
|
+
}
|
|
291
|
+
return result;
|
|
292
|
+
}
|
|
293
|
+
// Core lazy var resolution with caching on the module entry.
|
|
294
|
+
async function resolveEffectiveVar(key, moduleEntry, ctx) {
|
|
295
|
+
if (Object.hasOwn(moduleEntry.resolvedVarCache, key)) {
|
|
296
|
+
return moduleEntry.resolvedVarCache[key];
|
|
297
|
+
}
|
|
298
|
+
const consumerValue = get(moduleEntry.consumerVars, key, {
|
|
299
|
+
default: undefined
|
|
300
|
+
});
|
|
301
|
+
const varDef = getVarDef(moduleEntry.varDefs, key);
|
|
302
|
+
let result;
|
|
303
|
+
if (varDef?.properties) {
|
|
304
|
+
result = await resolveNamespaceVar(key, varDef, moduleEntry, ctx);
|
|
305
|
+
} else if (!type.isNone(consumerValue)) {
|
|
306
|
+
result = consumerValue;
|
|
307
|
+
} else if (varDef && !type.isUndefined(varDef.default)) {
|
|
308
|
+
result = await resolveVarDefault(varDef.default, moduleEntry, ctx);
|
|
309
|
+
} else {
|
|
310
|
+
result = null;
|
|
311
|
+
}
|
|
312
|
+
moduleEntry.resolvedVarCache[key] = result;
|
|
313
|
+
return result;
|
|
314
|
+
}
|
|
315
|
+
// Detect _module.*Id operators
|
|
316
|
+
const MODULE_ID_OPERATOR_KEYS = [
|
|
317
|
+
'_module.pageId',
|
|
318
|
+
'_module.connectionId',
|
|
319
|
+
'_module.endpointId',
|
|
320
|
+
'_module.id'
|
|
321
|
+
];
|
|
322
|
+
function isModuleIdOperator(node) {
|
|
323
|
+
return MODULE_ID_OPERATOR_KEYS.some((key)=>!type.isUndefined(node[key]));
|
|
324
|
+
}
|
|
325
|
+
// Resolve _module.pageId
|
|
326
|
+
function resolveModulePageId(arg, moduleEntry, context, configKey) {
|
|
327
|
+
if (type.isString(arg)) {
|
|
328
|
+
if (!moduleEntry) {
|
|
329
|
+
throw new ConfigError('_module.pageId string form is ambiguous at the app level — no module to scope against. Use { id, module } to specify the target module.', {
|
|
330
|
+
configKey
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
if (!(moduleEntry.exports?.pages ?? []).some((p)=>p.id === arg)) {
|
|
334
|
+
throw new ConfigError(`Module "${moduleEntry.id}" does not export page "${arg}".`, {
|
|
335
|
+
configKey
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
return `${moduleEntry.id}/${arg}`;
|
|
339
|
+
}
|
|
340
|
+
if (type.isObject(arg) && type.isString(arg.id) && type.isString(arg.module)) {
|
|
341
|
+
const targetEntry = resolveDepTarget({
|
|
342
|
+
moduleEntry,
|
|
343
|
+
depName: arg.module,
|
|
344
|
+
context,
|
|
345
|
+
configKey,
|
|
346
|
+
usage: `_module.pageId { id: "${arg.id}", module: "${arg.module}" }`
|
|
347
|
+
});
|
|
348
|
+
if (!(targetEntry.exports?.pages ?? []).some((p)=>p.id === arg.id)) {
|
|
349
|
+
const caller = moduleEntry ? `Module "${moduleEntry.id}"` : 'App config';
|
|
350
|
+
throw new ConfigError(`${caller} references page "${arg.id}" ` + `from "${arg.module}" (entry "${targetEntry.id}"), ` + `but that module does not export page "${arg.id}".`, {
|
|
351
|
+
configKey
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
return `${targetEntry.id}/${arg.id}`;
|
|
355
|
+
}
|
|
356
|
+
throw new ConfigError('_module.pageId requires a string or object { id, module }.', {
|
|
357
|
+
configKey
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
// Resolve _module.connectionId
|
|
361
|
+
function resolveModuleConnectionId(arg, moduleEntry, context, configKey) {
|
|
362
|
+
if (type.isString(arg)) {
|
|
363
|
+
if (!moduleEntry) {
|
|
364
|
+
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.', {
|
|
365
|
+
configKey
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
if (!(moduleEntry.exports?.connections ?? []).some((c)=>c.id === arg)) {
|
|
369
|
+
throw new ConfigError(`Module "${moduleEntry.id}" does not export connection "${arg}".`, {
|
|
370
|
+
configKey
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
const remapping = moduleEntry.connections ?? {};
|
|
374
|
+
if (remapping[arg]) {
|
|
375
|
+
return remapping[arg];
|
|
376
|
+
}
|
|
377
|
+
return `${moduleEntry.id}/${arg}`;
|
|
378
|
+
}
|
|
379
|
+
if (type.isObject(arg) && type.isString(arg.id) && type.isString(arg.module)) {
|
|
380
|
+
const targetEntry = resolveDepTarget({
|
|
381
|
+
moduleEntry,
|
|
382
|
+
depName: arg.module,
|
|
383
|
+
context,
|
|
384
|
+
configKey,
|
|
385
|
+
usage: `_module.connectionId { id: "${arg.id}", module: "${arg.module}" }`
|
|
386
|
+
});
|
|
387
|
+
if (!(targetEntry.exports?.connections ?? []).some((c)=>c.id === arg.id)) {
|
|
388
|
+
const caller = moduleEntry ? `Module "${moduleEntry.id}"` : 'App config';
|
|
389
|
+
throw new ConfigError(`${caller} references connection "${arg.id}" ` + `from "${arg.module}" (entry "${targetEntry.id}"), ` + `but that module does not export connection "${arg.id}".`, {
|
|
390
|
+
configKey
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
const targetRemapping = targetEntry.connections ?? {};
|
|
394
|
+
if (targetRemapping[arg.id]) {
|
|
395
|
+
return targetRemapping[arg.id];
|
|
396
|
+
}
|
|
397
|
+
return `${targetEntry.id}/${arg.id}`;
|
|
398
|
+
}
|
|
399
|
+
throw new ConfigError('_module.connectionId requires a string or object { id, module }.', {
|
|
400
|
+
configKey
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
// Resolve _module.endpointId
|
|
404
|
+
function resolveModuleEndpointId(arg, moduleEntry, context, configKey) {
|
|
405
|
+
if (type.isString(arg)) {
|
|
406
|
+
if (!moduleEntry) {
|
|
407
|
+
throw new ConfigError('_module.endpointId string form is ambiguous at the app level — no module to scope against. Use { id, module } to specify the target module.', {
|
|
408
|
+
configKey
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
if (!(moduleEntry.exports?.api ?? []).some((e)=>e.id === arg)) {
|
|
412
|
+
throw new ConfigError(`Module "${moduleEntry.id}" does not export endpoint "${arg}".`, {
|
|
413
|
+
configKey
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return `${moduleEntry.id}/${arg}`;
|
|
417
|
+
}
|
|
418
|
+
if (type.isObject(arg) && type.isString(arg.id) && type.isString(arg.module)) {
|
|
419
|
+
const targetEntry = resolveDepTarget({
|
|
420
|
+
moduleEntry,
|
|
421
|
+
depName: arg.module,
|
|
422
|
+
context,
|
|
423
|
+
configKey,
|
|
424
|
+
usage: `_module.endpointId { id: "${arg.id}", module: "${arg.module}" }`
|
|
425
|
+
});
|
|
426
|
+
if (!(targetEntry.exports?.api ?? []).some((e)=>e.id === arg.id)) {
|
|
427
|
+
const caller = moduleEntry ? `Module "${moduleEntry.id}"` : 'App config';
|
|
428
|
+
throw new ConfigError(`${caller} references endpoint "${arg.id}" ` + `from "${arg.module}" (entry "${targetEntry.id}"), ` + `but that module does not export endpoint "${arg.id}".`, {
|
|
429
|
+
configKey
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return `${targetEntry.id}/${arg.id}`;
|
|
433
|
+
}
|
|
434
|
+
throw new ConfigError('_module.endpointId requires a string or object { id, module }.', {
|
|
435
|
+
configKey
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
// Resolve _module.id
|
|
439
|
+
function resolveModuleId(arg, moduleEntry, context, configKey) {
|
|
440
|
+
if (!type.isObject(arg)) {
|
|
441
|
+
if (!moduleEntry) {
|
|
442
|
+
throw new ConfigError('_module.id is ambiguous at the app level — no module to scope against. Use { module } to specify the target module.', {
|
|
443
|
+
configKey
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
return moduleEntry.id;
|
|
447
|
+
}
|
|
448
|
+
if (type.isString(arg.module)) {
|
|
449
|
+
const targetEntry = resolveDepTarget({
|
|
450
|
+
moduleEntry,
|
|
451
|
+
depName: arg.module,
|
|
452
|
+
context,
|
|
453
|
+
configKey,
|
|
454
|
+
usage: `_module.id { module: "${arg.module}" }`
|
|
455
|
+
});
|
|
456
|
+
return targetEntry.id;
|
|
457
|
+
}
|
|
458
|
+
throw new ConfigError('_module.id requires a truthy value or object { module }.', {
|
|
459
|
+
configKey
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
// Dispatch _module.*Id operators
|
|
463
|
+
function resolveModuleIdOperator(node, ctx) {
|
|
464
|
+
const { moduleEntry } = ctx;
|
|
465
|
+
const context = ctx.buildContext;
|
|
466
|
+
const configKey = node['~k'];
|
|
467
|
+
if (!type.isUndefined(node['_module.pageId'])) {
|
|
468
|
+
return resolveModulePageId(node['_module.pageId'], moduleEntry, context, configKey);
|
|
469
|
+
}
|
|
470
|
+
if (!type.isUndefined(node['_module.connectionId'])) {
|
|
471
|
+
return resolveModuleConnectionId(node['_module.connectionId'], moduleEntry, context, configKey);
|
|
472
|
+
}
|
|
473
|
+
if (!type.isUndefined(node['_module.endpointId'])) {
|
|
474
|
+
return resolveModuleEndpointId(node['_module.endpointId'], moduleEntry, context, configKey);
|
|
475
|
+
}
|
|
476
|
+
if (!type.isUndefined(node['_module.id'])) {
|
|
477
|
+
return resolveModuleId(node['_module.id'], moduleEntry, context, configKey);
|
|
478
|
+
}
|
|
479
|
+
return node;
|
|
480
|
+
}
|
|
481
|
+
// Resolve a _ref node (16-step ref handling)
|
|
208
482
|
async function resolveRef(node, ctx) {
|
|
209
483
|
// 1. Create ref definition
|
|
210
484
|
const lineNumber = node['~l'];
|
|
@@ -228,12 +502,22 @@ async function resolveRef(node, ctx) {
|
|
|
228
502
|
if (type.isObject(refDef.key)) {
|
|
229
503
|
refDef.key = await resolve(cloneForResolve(refDef.key), ctx);
|
|
230
504
|
}
|
|
231
|
-
// 4.
|
|
505
|
+
// 4. Module path resolution: resolve relative paths from the module root
|
|
506
|
+
if (ctx.moduleRoot && type.isString(refDef.path) && !path.isAbsolute(refDef.path)) {
|
|
507
|
+
refDef.path = path.resolve(ctx.moduleRoot, refDef.path);
|
|
508
|
+
}
|
|
509
|
+
// 5. Update refMap with resolved path; store original for resolver refs
|
|
232
510
|
ctx.refMap[refDef.id].path = refDef.path;
|
|
233
511
|
if (!refDef.path) {
|
|
234
512
|
ctx.refMap[refDef.id].original = refDef.original;
|
|
235
513
|
}
|
|
236
|
-
//
|
|
514
|
+
// 6. Path escape constraint: module refs cannot escape the package root
|
|
515
|
+
if (ctx.packageRoot && refDef.path) {
|
|
516
|
+
if (!refDef.path.startsWith(ctx.packageRoot + '/') && refDef.path !== ctx.packageRoot) {
|
|
517
|
+
throw new ConfigError(`Module ref path "${refDef.path}" escapes the package root.`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// 7. Circular detection
|
|
237
521
|
if (refDef.path && ctx.refChain.has(refDef.path)) {
|
|
238
522
|
const chainDisplay = [
|
|
239
523
|
...ctx.refChain,
|
|
@@ -244,38 +528,116 @@ async function resolveRef(node, ctx) {
|
|
|
244
528
|
lineNumber: ctx.currentFile ? lineNumber : null
|
|
245
529
|
});
|
|
246
530
|
}
|
|
247
|
-
// Steps
|
|
531
|
+
// Steps 8-16: File operations that can fail independently per ref.
|
|
248
532
|
// Errors are collected so the walker can continue processing sibling refs,
|
|
249
533
|
// allowing multiple errors to be reported at once.
|
|
250
534
|
try {
|
|
251
|
-
//
|
|
252
|
-
let content
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
535
|
+
// 8. Load content
|
|
536
|
+
let content;
|
|
537
|
+
let resolvedEntryId = null;
|
|
538
|
+
if (refDef.module) {
|
|
539
|
+
const result = await getModuleRefContent({
|
|
540
|
+
context: ctx.buildContext,
|
|
541
|
+
refDef,
|
|
542
|
+
referencedFrom: ctx.currentFile,
|
|
543
|
+
walkCtx: ctx,
|
|
544
|
+
configKey: node['~k']
|
|
545
|
+
});
|
|
546
|
+
content = cloneForResolve(result.content);
|
|
547
|
+
resolvedEntryId = result.entryId;
|
|
548
|
+
} else {
|
|
549
|
+
content = await getRefContent({
|
|
550
|
+
context: ctx.buildContext,
|
|
551
|
+
refDef,
|
|
552
|
+
referencedFrom: ctx.currentFile
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
// 9. Circular detection for cross-module component/menu refs.
|
|
556
|
+
// File-based cycle detection (step 7) misses these because each module
|
|
557
|
+
// has a different file path. Use a synthetic key with the resolved
|
|
558
|
+
// concrete entry ID: "module:<entryId>/<type>:<name>".
|
|
559
|
+
if (resolvedEntryId && (refDef.component || refDef.menu)) {
|
|
560
|
+
const exportType = refDef.component ? 'component' : 'menu';
|
|
561
|
+
const exportName = refDef.component ?? refDef.menu;
|
|
562
|
+
const cycleKey = `module:${resolvedEntryId}/${exportType}:${exportName}`;
|
|
563
|
+
if (ctx.refChain.has(cycleKey)) {
|
|
564
|
+
const chainDisplay = [
|
|
565
|
+
...ctx.refChain,
|
|
566
|
+
cycleKey
|
|
567
|
+
].join('\n -> ');
|
|
568
|
+
throw new ConfigError(`Circular module reference detected. Module "${resolvedEntryId}" ${exportType} "${exportName}" ` + `references itself through:\n -> ${chainDisplay}`, {
|
|
569
|
+
filePath: ctx.currentFile
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
// 10. Create child context for the ref
|
|
574
|
+
let childCtx;
|
|
575
|
+
if (refDef.module && (refDef.component || refDef.menu)) {
|
|
576
|
+
const moduleEntry = ctx.buildContext.modules[resolvedEntryId];
|
|
577
|
+
const deferredFrom = content['~deferredFrom'];
|
|
578
|
+
const exportType = refDef.component ? 'component' : 'menu';
|
|
579
|
+
const exportName = refDef.component ?? refDef.menu;
|
|
580
|
+
const cycleKey = `module:${resolvedEntryId}/${exportType}:${exportName}`;
|
|
581
|
+
childCtx = ctx.forRef({
|
|
582
|
+
refId: refDef.id,
|
|
583
|
+
vars: refDef.vars,
|
|
584
|
+
filePath: deferredFrom ?? path.join(moduleEntry.moduleRoot, 'module.lowdefy.yaml'),
|
|
585
|
+
moduleRoot: moduleEntry.moduleRoot,
|
|
586
|
+
packageRoot: moduleEntry.packageRoot,
|
|
587
|
+
moduleDependencies: moduleEntry.moduleDependencies,
|
|
588
|
+
moduleEntry,
|
|
589
|
+
extraRefChainKeys: [
|
|
590
|
+
cycleKey
|
|
591
|
+
]
|
|
592
|
+
});
|
|
593
|
+
// Clone so each consumer gets an independent copy — getModuleRefContent
|
|
594
|
+
// returns a shared reference, and resolve() mutates in place.
|
|
595
|
+
// deferredFrom was read above before the clone (serializer.copy strips
|
|
596
|
+
// non-enumerable properties).
|
|
597
|
+
content = serializer.copy(content);
|
|
598
|
+
// When component/menu content is a file _ref, the inner ref would create
|
|
599
|
+
// a fresh var scope and lose the consumer's vars. Inject them into the clone.
|
|
600
|
+
if ((refDef.component || refDef.menu) && type.isObject(content) && content._ref) {
|
|
601
|
+
if (type.isObject(content._ref)) {
|
|
602
|
+
content._ref.vars = {
|
|
603
|
+
...content._ref.vars ?? {},
|
|
604
|
+
...refDef.vars
|
|
605
|
+
};
|
|
606
|
+
} else if (type.isString(content._ref) && Object.keys(refDef.vars).length > 0) {
|
|
607
|
+
content._ref = {
|
|
608
|
+
path: content._ref,
|
|
609
|
+
vars: refDef.vars
|
|
610
|
+
};
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
} else {
|
|
614
|
+
childCtx = ctx.forRef({
|
|
615
|
+
refId: refDef.id,
|
|
616
|
+
vars: refDef.vars,
|
|
617
|
+
filePath: refDef.path
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
// 11. Walk the content
|
|
264
621
|
content = await resolve(content, childCtx);
|
|
265
|
-
//
|
|
622
|
+
// 12. Scope menu item IDs (module menu refs only)
|
|
623
|
+
if (refDef.module && refDef.menu) {
|
|
624
|
+
const moduleEntry = ctx.buildContext.modules[resolvedEntryId];
|
|
625
|
+
scopeMenuItemIds(content, moduleEntry.id);
|
|
626
|
+
}
|
|
627
|
+
// 13. Run transformer
|
|
266
628
|
content = await runTransformer({
|
|
267
629
|
context: ctx.buildContext,
|
|
268
630
|
input: content,
|
|
269
631
|
refDef
|
|
270
632
|
});
|
|
271
|
-
//
|
|
633
|
+
// 14. Extract key
|
|
272
634
|
content = getKey({
|
|
273
635
|
input: content,
|
|
274
636
|
refDef
|
|
275
637
|
});
|
|
276
|
-
//
|
|
638
|
+
// 15. Tag all nodes with ~r for provenance
|
|
277
639
|
tagRefDeep(content, refDef.id);
|
|
278
|
-
//
|
|
640
|
+
// 16. Propagate ~ignoreBuildChecks
|
|
279
641
|
if (refDef.ignoreBuildChecks !== undefined) {
|
|
280
642
|
if (type.isObject(content)) {
|
|
281
643
|
content['~ignoreBuildChecks'] = refDef.ignoreBuildChecks;
|
|
@@ -300,44 +662,62 @@ async function resolveRef(node, ctx) {
|
|
|
300
662
|
async function resolve(node, ctx) {
|
|
301
663
|
// 1. Primitives pass through
|
|
302
664
|
if (!type.isObject(node) && !type.isArray(node)) return node;
|
|
303
|
-
// 2.
|
|
665
|
+
// 2. _ref — top-down (only operator that needs it)
|
|
304
666
|
if (type.isObject(node) && !type.isUndefined(node._ref)) {
|
|
305
667
|
return resolveRef(node, ctx);
|
|
306
668
|
}
|
|
307
|
-
//
|
|
308
|
-
// _ref or _build.* operators inside the default value get processed.
|
|
309
|
-
if (type.isObject(node) && !type.isUndefined(node._var)) {
|
|
310
|
-
try {
|
|
311
|
-
const varResult = resolveVar(node, ctx);
|
|
312
|
-
return await resolve(varResult, ctx);
|
|
313
|
-
} catch (error) {
|
|
314
|
-
if (error instanceof ConfigError) {
|
|
315
|
-
ctx.collectError(error);
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
throw error;
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
// 5. Array — walk children in parallel
|
|
669
|
+
// 3. Array — walk children in parallel
|
|
322
670
|
if (type.isArray(node)) {
|
|
323
671
|
await Promise.all(node.map(async (item, i)=>{
|
|
324
672
|
node[i] = await resolve(item, ctx.child(String(i)));
|
|
325
673
|
}));
|
|
326
674
|
return node;
|
|
327
675
|
}
|
|
328
|
-
//
|
|
676
|
+
// 4. Object — walk children in parallel (with shouldStop)
|
|
329
677
|
const keys = Object.keys(node);
|
|
330
678
|
await Promise.all(keys.map(async (key)=>{
|
|
331
679
|
if (ctx.shouldStop) {
|
|
332
680
|
const childPath = ctx.path ? `${ctx.path}.${key}` : key;
|
|
333
|
-
|
|
681
|
+
const stopMode = ctx.shouldStop(childPath, ctx.refId);
|
|
682
|
+
if (stopMode === 'delete' || stopMode === true) {
|
|
334
683
|
delete node[key];
|
|
335
684
|
return;
|
|
336
685
|
}
|
|
686
|
+
if (stopMode === 'preserve') {
|
|
687
|
+
if (type.isObject(node[key]) || type.isArray(node[key])) {
|
|
688
|
+
setNonEnumerableProperty(node[key], '~deferredFrom', ctx.currentFile);
|
|
689
|
+
}
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
337
692
|
}
|
|
338
693
|
node[key] = await resolve(node[key], ctx.child(key));
|
|
339
694
|
}));
|
|
340
|
-
//
|
|
695
|
+
// 5. _var — substitution (children already resolved)
|
|
696
|
+
if (!type.isUndefined(node._var)) {
|
|
697
|
+
try {
|
|
698
|
+
const varResult = resolveVar(node, ctx);
|
|
699
|
+
return await resolve(varResult, ctx);
|
|
700
|
+
} catch (error) {
|
|
701
|
+
if (error instanceof ConfigError) {
|
|
702
|
+
ctx.collectError(error);
|
|
703
|
+
return null;
|
|
704
|
+
}
|
|
705
|
+
throw error;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
// 6. _module.var — module variable substitution
|
|
709
|
+
if (!type.isUndefined(node['_module.var'])) {
|
|
710
|
+
if (!ctx.moduleEntry) {
|
|
711
|
+
if (ctx.moduleRoot) return node;
|
|
712
|
+
throw new ConfigError('_module.var cannot be used at the app level.');
|
|
713
|
+
}
|
|
714
|
+
return resolve(await resolveModuleVar(node, ctx), ctx);
|
|
715
|
+
}
|
|
716
|
+
// 7. _module.*Id — resolve to scoped ID string
|
|
717
|
+
if (isModuleIdOperator(node)) {
|
|
718
|
+
return resolveModuleIdOperator(node, ctx);
|
|
719
|
+
}
|
|
720
|
+
// 8. _build.* operator
|
|
341
721
|
if (isBuildOperator(node)) {
|
|
342
722
|
const result = evaluateBuildOperator(node, ctx);
|
|
343
723
|
tagRefDeep(result, ctx.refId);
|