@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
|
@@ -14,10 +14,9 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import path from 'path';
|
|
16
16
|
import { serializer, type } from '@lowdefy/helpers';
|
|
17
|
-
import {
|
|
18
|
-
import collectPageContent from '../collectPageContent.js';
|
|
17
|
+
import { writeFileIfChanged } from '@lowdefy/node-utils';
|
|
19
18
|
import writeJs from '../buildJs/writeJs.js';
|
|
20
|
-
async function writePageJit({ page, context }) {
|
|
19
|
+
async function writePageJit({ page, context, tailwindContent }) {
|
|
21
20
|
// Write page JSON
|
|
22
21
|
await context.writeBuildArtifact(`pages/${page.pageId}.json`, serializer.serializeToString(page ?? {}));
|
|
23
22
|
// Write page request JSONs
|
|
@@ -43,10 +42,14 @@ async function writePageJit({ page, context }) {
|
|
|
43
42
|
await writeJs({
|
|
44
43
|
context
|
|
45
44
|
});
|
|
46
|
-
// Write per-page content file for Tailwind to scan class and property strings
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
// Write per-page content file for Tailwind to scan class and property strings.
|
|
46
|
+
// Content is collected by the caller before _js extraction so class
|
|
47
|
+
// candidates inside _js source are included. Skipping unchanged content lets
|
|
48
|
+
// the caller avoid touching the CSS recompilation trigger
|
|
49
|
+
// (tailwind-candidates.css) for builds that introduced no new class candidates.
|
|
50
|
+
const tailwindChanged = await writeFileIfChanged(path.join(context.directories.server, 'lowdefy-build', 'tailwind', `${encodeURIComponent(page.pageId)}.html`), '<!-- Generated by Lowdefy build -->\n' + (tailwindContent ?? ''));
|
|
51
|
+
return {
|
|
52
|
+
tailwindChanged
|
|
53
|
+
};
|
|
51
54
|
}
|
|
52
55
|
export default writePageJit;
|
|
@@ -14,15 +14,15 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import path from 'path';
|
|
16
16
|
import semver from 'semver';
|
|
17
|
-
import { type } from '@lowdefy/helpers';
|
|
17
|
+
import { ReservedKeyError, setKey, type } from '@lowdefy/helpers';
|
|
18
18
|
import { ConfigError } from '@lowdefy/errors';
|
|
19
19
|
import operators from '@lowdefy/operators-js/operators/build';
|
|
20
20
|
import { resolve, WalkContext } from './buildRefs/walker.js';
|
|
21
21
|
import getRefContent from './buildRefs/getRefContent.js';
|
|
22
22
|
import makeRefDefinition from './buildRefs/makeRefDefinition.js';
|
|
23
|
-
import evaluateStaticOperators from './buildRefs/evaluateStaticOperators.js';
|
|
24
23
|
import collectDynamicIdentifiers from './collectDynamicIdentifiers.js';
|
|
25
24
|
import validateOperatorsDynamic from './validateOperatorsDynamic.js';
|
|
25
|
+
import { makeShouldStop } from './buildRefs/deferredRegions.js';
|
|
26
26
|
validateOperatorsDynamic({
|
|
27
27
|
operators
|
|
28
28
|
});
|
|
@@ -48,11 +48,69 @@ function validateRequiredVars(varDefs, consumerVars, entryId, source, prefix = '
|
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
+
function getRuntimeOperatorKey(value) {
|
|
52
|
+
if (!type.isObject(value)) return null;
|
|
53
|
+
const nonTildeKeys = Object.keys(value).filter((k)=>!k.startsWith('~'));
|
|
54
|
+
if (nonTildeKeys.length === 1 && nonTildeKeys[0].startsWith('_')) return nonTildeKeys[0];
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function suggestBuildOperator(operatorKey) {
|
|
58
|
+
// '_string.concat' -> '_build.string.concat', '_sum' -> '_build.sum'
|
|
59
|
+
return `_build.${operatorKey.slice(1)}`;
|
|
60
|
+
}
|
|
61
|
+
// Component bodies are preserved by config path (components.<i>.component), so
|
|
62
|
+
// bodies under an operator object at `components` itself or at an array element
|
|
63
|
+
// escape preservation and resolve eagerly during the manifest walk. That is
|
|
64
|
+
// harmless for static bodies (operator-composed component lists are supported —
|
|
65
|
+
// pinned by fixture 81-cross-module-build-op-components), but a _var in such a
|
|
66
|
+
// body resolves against the manifest walk's scope instead of the consumer's
|
|
67
|
+
// per-ref vars — a silently wrong value. Error on that combination explicitly.
|
|
68
|
+
// A _ref at either position is fully safe: it resolves top-down, so paths
|
|
69
|
+
// inside the ref'd file still match the preserve regex. An operator at
|
|
70
|
+
// components.<i>.component itself is inside the preserved body and is fine.
|
|
71
|
+
function findVarNode(value) {
|
|
72
|
+
if (type.isArray(value)) {
|
|
73
|
+
for (const item of value){
|
|
74
|
+
const found = findVarNode(item);
|
|
75
|
+
if (found) return found;
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
if (!type.isObject(value)) return null;
|
|
80
|
+
if (getRuntimeOperatorKey(value) === '_var') return value;
|
|
81
|
+
for (const key of Object.keys(value)){
|
|
82
|
+
const found = findVarNode(value[key]);
|
|
83
|
+
if (found) return found;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function assertStaticComponentsList({ components, entryId, filePath }) {
|
|
88
|
+
if (!type.isObject(components) && !type.isArray(components)) return;
|
|
89
|
+
const check = (value, position)=>{
|
|
90
|
+
const operatorKey = getRuntimeOperatorKey(value);
|
|
91
|
+
if (operatorKey && operatorKey !== '_ref' && findVarNode(value)) {
|
|
92
|
+
throw new ConfigError(`Module "${entryId}": _var inside an operator-generated components section ` + `cannot resolve per consumer. Found "${operatorKey}" at ${position} with a _var ` + `in its content. Define components whose bodies use _var as static ` + `{ id, component } list items so the bodies are preserved.`, {
|
|
93
|
+
filePath
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
check(components, '"components"');
|
|
98
|
+
if (type.isArray(components)) {
|
|
99
|
+
components.forEach((item, i)=>check(item, `"components.${i}"`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
51
102
|
function validateVarTypes(varDefs, resolvedVarCache, entryId, source, prefix = '') {
|
|
52
103
|
for (const [varName, varDef] of Object.entries(varDefs)){
|
|
53
104
|
const fullName = prefix ? `${prefix}.${varName}` : varName;
|
|
54
105
|
const value = resolvedVarCache[fullName];
|
|
55
106
|
if (varDef.type && !type.isNone(value)) {
|
|
107
|
+
// A typed var must hold a concrete value — runtime operators are not allowed
|
|
108
|
+
// regardless of whether they are static-foldable or dynamic. Suggest the
|
|
109
|
+
// build-time equivalent so the user knows how to fix it.
|
|
110
|
+
const operatorKey = getRuntimeOperatorKey(value);
|
|
111
|
+
if (operatorKey) {
|
|
112
|
+
throw new ConfigError(`Module "${entryId}" (${source}) var "${fullName}" is typed "${varDef.type}" ` + `but received a runtime operator "${operatorKey}". ` + `Use the build-time equivalent "${suggestBuildOperator(operatorKey)}" instead.` + (varDef.description ? `\n - ${varDef.description}` : ''));
|
|
113
|
+
}
|
|
56
114
|
if (type.typeOf(value) !== varDef.type) {
|
|
57
115
|
throw new ConfigError(`Module "${entryId}" (${source}) var "${fullName}" must be type ` + `"${varDef.type}" but got "${type.typeOf(value)}".` + (varDef.description ? `\n - ${varDef.description}` : ''));
|
|
58
116
|
}
|
|
@@ -69,9 +127,6 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
69
127
|
if (entry.id.includes('/')) {
|
|
70
128
|
throw new ConfigError(`Module entry id "${entry.id}" must not contain '/'. ` + `Use a flat identifier like "team-users".`);
|
|
71
129
|
}
|
|
72
|
-
if (entry.id === '__proto__' || entry.id === 'constructor' || entry.id === 'prototype') {
|
|
73
|
-
throw new ConfigError(`Module entry id "${entry.id}" is a reserved name.`);
|
|
74
|
-
}
|
|
75
130
|
if (!entry.source || !type.isString(entry.source)) {
|
|
76
131
|
throw new ConfigError(`Module entry "${entry.id}": 'source' is required and must be a string.`);
|
|
77
132
|
}
|
|
@@ -88,7 +143,17 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
88
143
|
refDef,
|
|
89
144
|
referencedFrom: null
|
|
90
145
|
});
|
|
91
|
-
|
|
146
|
+
if (type.isObject(content)) {
|
|
147
|
+
assertStaticComponentsList({
|
|
148
|
+
components: content.components,
|
|
149
|
+
entryId: entry.id,
|
|
150
|
+
filePath: moduleYamlPath
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Phase A — header parse: walk only the static header keys (dependencies,
|
|
154
|
+
// plugins, secrets, vars definitions — defaults record-ified). Content
|
|
155
|
+
// sections stay raw parsed YAML; Phase C.5 record-ifies exportables and
|
|
156
|
+
// Phase D resolves the rest.
|
|
92
157
|
const ctx = new WalkContext({
|
|
93
158
|
buildContext: context,
|
|
94
159
|
refId: refDef.id,
|
|
@@ -96,6 +161,9 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
96
161
|
vars: {},
|
|
97
162
|
moduleRoot,
|
|
98
163
|
packageRoot,
|
|
164
|
+
// The entry object is created after this walk; deferred-record coordinates
|
|
165
|
+
// still need the owning entry id.
|
|
166
|
+
entryId: entry.id,
|
|
99
167
|
path: '',
|
|
100
168
|
currentFile: moduleYamlPath,
|
|
101
169
|
refChain: new Set(refDef.path ? [
|
|
@@ -103,16 +171,9 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
103
171
|
] : []),
|
|
104
172
|
operators,
|
|
105
173
|
env: process.env,
|
|
174
|
+
lowdefyApp: context.appMeta,
|
|
106
175
|
dynamicIdentifiers,
|
|
107
|
-
shouldStop: (
|
|
108
|
-
if (/^vars(\.[^.]+\.properties)*\.[^.]+\.default(\..*)?$/.test(childPath)) return 'preserve';
|
|
109
|
-
if (/^components\.\d+\.component$/.test(childPath)) return 'preserve';
|
|
110
|
-
if (/^pages(\..*)?$/.test(childPath)) return 'preserve';
|
|
111
|
-
if (/^api(\..*)?$/.test(childPath)) return 'preserve';
|
|
112
|
-
if (/^connections(\..*)?$/.test(childPath)) return 'preserve';
|
|
113
|
-
if (/^menus\.\d+\.links$/.test(childPath)) return 'preserve';
|
|
114
|
-
return false;
|
|
115
|
-
}
|
|
176
|
+
shouldStop: makeShouldStop('header')
|
|
116
177
|
});
|
|
117
178
|
const manifest = await resolve(content, ctx);
|
|
118
179
|
// Parse dependencies array from manifest
|
|
@@ -151,22 +212,74 @@ async function resolveLocalManifest({ entry, resolvedPaths, context }) {
|
|
|
151
212
|
throw new ConfigError(`Module "${entry.id}" requires plugin "${plugin.name}" version "${plugin.version}" ` + `but the app has version "${appVersion}" installed. ` + `Update the plugin to a compatible version.`);
|
|
152
213
|
}
|
|
153
214
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
215
|
+
try {
|
|
216
|
+
setKey(context.modules, entry.id, {
|
|
217
|
+
id: entry.id,
|
|
218
|
+
source: entry.source,
|
|
219
|
+
packageRoot,
|
|
220
|
+
moduleRoot,
|
|
221
|
+
isLocal,
|
|
222
|
+
consumerVars: entry.vars ?? {},
|
|
223
|
+
varDefs,
|
|
224
|
+
resolvedVarCache: {},
|
|
225
|
+
connections: entry.connections ?? {},
|
|
226
|
+
manifest,
|
|
227
|
+
dependencies,
|
|
228
|
+
moduleDependencies: entry.dependencies ?? {},
|
|
229
|
+
refDef
|
|
230
|
+
});
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (error instanceof ReservedKeyError) {
|
|
233
|
+
throw new ConfigError(`Module entry id "${entry.id}" is a reserved name.`, {
|
|
234
|
+
cause: error
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// Phase C.5 — record-ify exportables: walk only the components and menus
|
|
241
|
+
// sections in module-static scope (entry id known, but NO moduleEntry on the
|
|
242
|
+
// context — a _module.var at a structural position errors, because export ids
|
|
243
|
+
// must not vary per consumer). File refs are entered and static operators
|
|
244
|
+
// resolve; component bodies and menu links become records before any phase can
|
|
245
|
+
// consume them, so cross-entry consumption never races record creation.
|
|
246
|
+
async function recordifyExportables({ entryId, context }) {
|
|
247
|
+
const moduleEntry = context.modules[entryId];
|
|
248
|
+
const { manifest, packageRoot, moduleRoot, refDef } = moduleEntry;
|
|
249
|
+
const moduleYamlPath = path.join(moduleRoot, 'module.lowdefy.yaml');
|
|
250
|
+
if (type.isObject(manifest)) {
|
|
251
|
+
assertStaticComponentsList({
|
|
252
|
+
components: manifest.components,
|
|
253
|
+
entryId,
|
|
254
|
+
filePath: moduleYamlPath
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
const ctx = new WalkContext({
|
|
258
|
+
buildContext: context,
|
|
259
|
+
refId: refDef.id,
|
|
260
|
+
sourceRefId: null,
|
|
261
|
+
vars: {},
|
|
158
262
|
moduleRoot,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
263
|
+
packageRoot,
|
|
264
|
+
entryId,
|
|
265
|
+
path: '',
|
|
266
|
+
currentFile: moduleYamlPath,
|
|
267
|
+
refChain: new Set(refDef.path ? [
|
|
268
|
+
refDef.path
|
|
269
|
+
] : []),
|
|
270
|
+
operators,
|
|
271
|
+
env: process.env,
|
|
272
|
+
lowdefyApp: context.appMeta,
|
|
273
|
+
dynamicIdentifiers,
|
|
274
|
+
shouldStop: makeShouldStop('exportables')
|
|
275
|
+
});
|
|
276
|
+
moduleEntry.manifest = await resolve(manifest, ctx);
|
|
169
277
|
}
|
|
278
|
+
// Phase D — manifest resolve: ONE full walk with the module entry in context.
|
|
279
|
+
// Pages, api, and connections resolve completely; component/menuLinks
|
|
280
|
+
// placeholders pass through the kind-aware dispatch untouched; varDefault
|
|
281
|
+
// placeholders keep their rule so the dispatch cannot force demand-only
|
|
282
|
+
// records.
|
|
170
283
|
async function resolveFullManifest({ entryId, context }) {
|
|
171
284
|
const moduleEntry = context.modules[entryId];
|
|
172
285
|
const { manifest, packageRoot, moduleRoot, moduleDependencies, refDef } = moduleEntry;
|
|
@@ -187,24 +300,18 @@ async function resolveFullManifest({ entryId, context }) {
|
|
|
187
300
|
] : []),
|
|
188
301
|
operators,
|
|
189
302
|
env: process.env,
|
|
303
|
+
lowdefyApp: context.appMeta,
|
|
190
304
|
dynamicIdentifiers,
|
|
191
|
-
shouldStop: (
|
|
192
|
-
if (/^vars(\.[^.]+\.properties)*\.[^.]+\.default(\..*)?$/.test(childPath)) return 'preserve';
|
|
193
|
-
if (/^components\.\d+\.component$/.test(childPath)) return 'preserve';
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
let resolved = await resolve(manifest, ctx);
|
|
198
|
-
resolved = evaluateStaticOperators({
|
|
199
|
-
context,
|
|
200
|
-
input: resolved,
|
|
201
|
-
refDef
|
|
305
|
+
shouldStop: makeShouldStop('manifest')
|
|
202
306
|
});
|
|
307
|
+
const resolved = await resolve(manifest, ctx);
|
|
203
308
|
// Filter null entries produced by _ref resolution failures
|
|
204
309
|
for (const key of [
|
|
205
310
|
'pages',
|
|
206
311
|
'connections',
|
|
207
|
-
'api'
|
|
312
|
+
'api',
|
|
313
|
+
'agents',
|
|
314
|
+
'notifications'
|
|
208
315
|
]){
|
|
209
316
|
if (type.isArray(resolved[key])) {
|
|
210
317
|
resolved[key] = resolved[key].filter((item)=>!type.isNone(item));
|
|
@@ -217,4 +324,4 @@ async function resolveFullManifest({ entryId, context }) {
|
|
|
217
324
|
validateVarTypes(varDefs, moduleEntry.resolvedVarCache, entryId, moduleEntry.source);
|
|
218
325
|
}
|
|
219
326
|
}
|
|
220
|
-
export { resolveLocalManifest, resolveFullManifest, validateRequiredVars };
|
|
327
|
+
export { resolveLocalManifest, recordifyExportables, resolveFullManifest, validateRequiredVars };
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
17
|
+
function collectCallAgentSteps(routine, steps) {
|
|
18
|
+
if (type.isArray(routine)) {
|
|
19
|
+
routine.forEach((item)=>collectCallAgentSteps(item, steps));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (type.isObject(routine)) {
|
|
23
|
+
if (routine.type === 'CallAgent') {
|
|
24
|
+
steps.push(routine);
|
|
25
|
+
}
|
|
26
|
+
// Recurse into all values (handles control structures like :then, :else, :try, :catch)
|
|
27
|
+
Object.values(routine).forEach((value)=>collectCallAgentSteps(value, steps));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Runs after buildAgents (needs context.agentIds and normalized agent.tools).
|
|
31
|
+
// Validates that CallAgent steps with a static agentId reference an existing
|
|
32
|
+
// agent, and warns when that agent has confirm tools — headless runs have no
|
|
33
|
+
// client to resolve an approval, so those tools auto-execute. Operator
|
|
34
|
+
// (dynamic) agentIds are skipped; they resolve at runtime.
|
|
35
|
+
function validateCallAgentSteps({ components, context }) {
|
|
36
|
+
(components.api ?? []).forEach((endpoint)=>{
|
|
37
|
+
const steps = [];
|
|
38
|
+
collectCallAgentSteps(endpoint.routine, steps);
|
|
39
|
+
steps.forEach((step)=>{
|
|
40
|
+
const agentId = step.properties?.agentId;
|
|
41
|
+
if (!type.isString(agentId)) return;
|
|
42
|
+
if (!context.agentIds?.has(agentId)) {
|
|
43
|
+
throw new ConfigError(`CallAgent step "${step.stepId}" at endpoint "${endpoint.endpointId}" references agent "${agentId}" which does not exist.`, {
|
|
44
|
+
configKey: step['~k']
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const agent = (components.agents ?? []).find((a)=>a.agentId === agentId);
|
|
48
|
+
const hasConfirmTools = (agent?.tools ?? []).some((t)=>t.confirm);
|
|
49
|
+
if (hasConfirmTools) {
|
|
50
|
+
context.handleWarning(new ConfigWarning(`Agent "${agentId}" has tools with confirm: true, but tool approval is not supported when run from a CallAgent step. Tools will auto-execute.`, {
|
|
51
|
+
configKey: step['~k']
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
return components;
|
|
57
|
+
}
|
|
58
|
+
export default validateCallAgentSteps;
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import validateCronConfig from './validateCronConfig.js';
|
|
16
17
|
function validateConfig({ components }) {
|
|
17
18
|
if (type.isNone(components.config)) {
|
|
18
19
|
components.config = {};
|
|
@@ -25,6 +26,9 @@ function validateConfig({ components }) {
|
|
|
25
26
|
throw new Error('Base path must start with "/".');
|
|
26
27
|
}
|
|
27
28
|
}
|
|
29
|
+
validateCronConfig({
|
|
30
|
+
components
|
|
31
|
+
});
|
|
28
32
|
return components;
|
|
29
33
|
}
|
|
30
34
|
export default validateConfig;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
import getCronEnvironmentNames from '../utils/getCronEnvironmentNames.js';
|
|
18
|
+
// Environment names become a path segment (/api/cron-forward/<environment>/<endpointId>).
|
|
19
|
+
const environmentNamePattern = /^[A-Za-z0-9\-_]+$/;
|
|
20
|
+
function isAbsoluteHttpUrl(value) {
|
|
21
|
+
try {
|
|
22
|
+
const url = new URL(value);
|
|
23
|
+
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
24
|
+
} catch {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Returns true when the environment is forwarded to (has a url), false for the host environment.
|
|
29
|
+
function validateCronEnvironment({ name, environment, configKey }) {
|
|
30
|
+
if (name === 'default') {
|
|
31
|
+
throw new ConfigError('Cron environment name "default" is reserved: it names the schedules every environment inherits.', {
|
|
32
|
+
configKey
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
if (!environmentNamePattern.test(name)) {
|
|
36
|
+
throw new ConfigError(`Cron environment name "${name}" is invalid. Environment names must only contain A-Z, a-z, 0-9, "-" and "_".`, {
|
|
37
|
+
configKey
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!type.isObject(environment)) {
|
|
41
|
+
throw new ConfigError(`Cron environment "${name}" is not an object.`, {
|
|
42
|
+
received: environment,
|
|
43
|
+
configKey
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
if (!type.isUndefined(environment.enabled) && !type.isBoolean(environment.enabled)) {
|
|
47
|
+
throw new ConfigError(`Cron environment "${name}" enabled is not a boolean.`, {
|
|
48
|
+
received: environment.enabled,
|
|
49
|
+
configKey: environment['~k'] ?? configKey
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (type.isUndefined(environment.url)) {
|
|
53
|
+
if (!type.isUndefined(environment.secret)) {
|
|
54
|
+
throw new ConfigError(`Cron environment "${name}" has a secret but no url. The environment without a url is the deployment that runs the crons and needs no secret.`, {
|
|
55
|
+
configKey: environment['~k'] ?? configKey
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (!type.isString(environment.url) || !isAbsoluteHttpUrl(environment.url)) {
|
|
61
|
+
throw new ConfigError(`Cron environment "${name}" url is not an absolute http(s) URL, e.g. "https://staging.example.com".`, {
|
|
62
|
+
received: environment.url,
|
|
63
|
+
configKey: environment['~k'] ?? configKey
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (!type.isString(environment.secret) || environment.secret === '') {
|
|
67
|
+
throw new ConfigError(`Cron environment "${name}" has a url but no secret. Set secret to the Lowdefy secret name that holds the environment's CRON_SECRET.`, {
|
|
68
|
+
received: environment.secret,
|
|
69
|
+
configKey: environment['~k'] ?? configKey
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
// Validates config.cron: the deployment environments scheduled endpoints run in. Exactly one
|
|
75
|
+
// environment has no url — the deployment whose crons Vercel fires — and every other environment
|
|
76
|
+
// is forwarded to from there, so it needs a url and the secret that authorizes the forwarded call.
|
|
77
|
+
function validateCronConfig({ components }) {
|
|
78
|
+
const cron = components.config.cron;
|
|
79
|
+
if (type.isUndefined(cron)) return;
|
|
80
|
+
const configKey = cron['~k'] ?? components.config['~k'];
|
|
81
|
+
if (!type.isObject(cron)) {
|
|
82
|
+
throw new ConfigError('lowdefy.config.cron is not an object.', {
|
|
83
|
+
received: cron,
|
|
84
|
+
configKey
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (!type.isObject(cron.environments)) {
|
|
88
|
+
throw new ConfigError('lowdefy.config.cron.environments is not an object.', {
|
|
89
|
+
received: cron.environments,
|
|
90
|
+
configKey
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const names = getCronEnvironmentNames(cron.environments);
|
|
94
|
+
if (names.length === 0) {
|
|
95
|
+
throw new ConfigError('lowdefy.config.cron.environments declares no environments.', {
|
|
96
|
+
configKey
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
const hosts = names.filter((name)=>!validateCronEnvironment({
|
|
100
|
+
name,
|
|
101
|
+
environment: cron.environments[name],
|
|
102
|
+
configKey
|
|
103
|
+
}));
|
|
104
|
+
if (hosts.length !== 1) {
|
|
105
|
+
throw new ConfigError(`Exactly one environment in lowdefy.config.cron.environments must have no url (the deployment that runs the crons). Received ${JSON.stringify(hosts)}.`, {
|
|
106
|
+
configKey
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
export default validateCronConfig;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { type } from '@lowdefy/helpers';
|
|
16
|
+
import { ConfigError } from '@lowdefy/errors';
|
|
17
|
+
function collectRenderNotificationSteps(routine, steps) {
|
|
18
|
+
if (type.isArray(routine)) {
|
|
19
|
+
routine.forEach((item)=>collectRenderNotificationSteps(item, steps));
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (type.isObject(routine)) {
|
|
23
|
+
if (routine.type === 'RenderNotification') {
|
|
24
|
+
steps.push(routine);
|
|
25
|
+
}
|
|
26
|
+
// Recurse into all values (handles control structures like :then, :else, :try, :catch)
|
|
27
|
+
Object.values(routine).forEach((value)=>collectRenderNotificationSteps(value, steps));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
// Runs after buildNotifications (needs context.notificationIds). Validates that
|
|
31
|
+
// RenderNotification steps with a static notificationId reference an existing
|
|
32
|
+
// notification. Operator (dynamic) notificationIds are skipped; they resolve at
|
|
33
|
+
// runtime.
|
|
34
|
+
function validateRenderNotificationSteps({ components, context }) {
|
|
35
|
+
(components.api ?? []).forEach((endpoint)=>{
|
|
36
|
+
const steps = [];
|
|
37
|
+
collectRenderNotificationSteps(endpoint.routine, steps);
|
|
38
|
+
steps.forEach((step)=>{
|
|
39
|
+
const notificationId = step.properties?.notificationId;
|
|
40
|
+
if (!type.isString(notificationId)) return;
|
|
41
|
+
if (!context.notificationIds?.has(notificationId)) {
|
|
42
|
+
throw new ConfigError(`RenderNotification step "${step.stepId}" at endpoint "${endpoint.endpointId}" references notification "${notificationId}" which does not exist.`, {
|
|
43
|
+
configKey: step['~k']
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
return components;
|
|
49
|
+
}
|
|
50
|
+
export default validateRenderNotificationSteps;
|
package/dist/build/writeApi.js
CHANGED
|
@@ -13,9 +13,62 @@
|
|
|
13
13
|
See the License for the specific language governing permissions and
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/ import { type, serializer } from '@lowdefy/helpers';
|
|
16
|
+
import getCronEnvironmentNames from '../utils/getCronEnvironmentNames.js';
|
|
16
17
|
async function writeEndpoint({ endpoint, context }) {
|
|
17
18
|
await context.writeBuildArtifact(`api/${endpoint.endpointId}.json`, serializer.serializeToString(endpoint ?? {}));
|
|
18
19
|
}
|
|
20
|
+
// Flat manifest of every scheduled endpoint, consumed by the Vercel Build Output assembly to
|
|
21
|
+
// generate the `crons` array in config.json. The runtime reads schedules off the endpoint artifact
|
|
22
|
+
// directly, so it does not depend on this file.
|
|
23
|
+
//
|
|
24
|
+
// With config.cron.environments declared there is one entry per enabled environment and schedule:
|
|
25
|
+
// `environment` names it and `forward` is true for environments the production deployment forwards
|
|
26
|
+
// to (those with a url) — Vercel fires crons only on production, so every environment's schedules
|
|
27
|
+
// must be registered there.
|
|
28
|
+
function getSchedules({ endpoint, environment }) {
|
|
29
|
+
if (type.isArray(endpoint.schedules)) return endpoint.schedules;
|
|
30
|
+
return endpoint.schedules?.[environment] ?? [];
|
|
31
|
+
}
|
|
32
|
+
function pushSchedules({ endpoint, environment, forward }) {
|
|
33
|
+
return getSchedules({
|
|
34
|
+
endpoint,
|
|
35
|
+
environment
|
|
36
|
+
}).map((schedule)=>({
|
|
37
|
+
endpointId: endpoint.endpointId,
|
|
38
|
+
cron: schedule.cron,
|
|
39
|
+
payload: schedule.payload ?? {},
|
|
40
|
+
...type.isUndefined(environment) ? {} : {
|
|
41
|
+
environment,
|
|
42
|
+
forward
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
}
|
|
46
|
+
async function writeSchedulesManifest({ components, context }) {
|
|
47
|
+
const cronEnvironments = components.config?.cron?.environments;
|
|
48
|
+
const environmentNames = getCronEnvironmentNames(cronEnvironments);
|
|
49
|
+
const schedules = [];
|
|
50
|
+
(components.api ?? []).forEach((endpoint)=>{
|
|
51
|
+
if (environmentNames.length === 0) {
|
|
52
|
+
schedules.push(...pushSchedules({
|
|
53
|
+
endpoint
|
|
54
|
+
}));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
environmentNames.forEach((environment)=>{
|
|
58
|
+
const config = cronEnvironments[environment];
|
|
59
|
+
if (config.enabled === false) return;
|
|
60
|
+
schedules.push(...pushSchedules({
|
|
61
|
+
endpoint,
|
|
62
|
+
environment,
|
|
63
|
+
forward: !type.isUndefined(config.url)
|
|
64
|
+
}));
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// Only emit the manifest when something is scheduled; the Vercel assembly treats a missing
|
|
68
|
+
// schedules.json as "no crons".
|
|
69
|
+
if (schedules.length === 0) return;
|
|
70
|
+
await context.writeBuildArtifact('schedules.json', serializer.serializeToString(schedules));
|
|
71
|
+
}
|
|
19
72
|
async function writeApi({ components, context }) {
|
|
20
73
|
if (type.isNone(components.api)) return;
|
|
21
74
|
if (!type.isArray(components.api)) {
|
|
@@ -25,6 +78,10 @@ async function writeApi({ components, context }) {
|
|
|
25
78
|
endpoint,
|
|
26
79
|
context
|
|
27
80
|
}));
|
|
28
|
-
|
|
81
|
+
await Promise.all(writePromises);
|
|
82
|
+
await writeSchedulesManifest({
|
|
83
|
+
components,
|
|
84
|
+
context
|
|
85
|
+
});
|
|
29
86
|
}
|
|
30
87
|
export default writeApi;
|
package/dist/build/writeI18n.js
CHANGED
|
@@ -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 { ConfigError, ConfigWarning } from '@lowdefy/errors';
|
|
16
|
-
import { mergeObjects, serializer, type } from '@lowdefy/helpers';
|
|
16
|
+
import { isReserved, mergeObjects, serializer, type } from '@lowdefy/helpers';
|
|
17
17
|
function collectPluginMessages({ messagesMap, declaredCodes }) {
|
|
18
18
|
const collected = {};
|
|
19
19
|
Object.values(messagesMap ?? {}).forEach((pluginCatalog)=>{
|
|
@@ -57,6 +57,13 @@ async function writeI18n({ components, context }) {
|
|
|
57
57
|
configKey: locale?.['~k'] ?? i18n['~k']
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
|
+
// The code keys the collected plugin catalog and the merged messages
|
|
61
|
+
// artifact, both plain objects.
|
|
62
|
+
if (isReserved(locale.code)) {
|
|
63
|
+
throw new ConfigError(`Locale code "${locale.code}" in "config.i18n.locales" is a reserved name and cannot be used as a locale code.`, {
|
|
64
|
+
configKey: locale['~k']
|
|
65
|
+
});
|
|
66
|
+
}
|
|
60
67
|
if (codes.has(locale.code)) {
|
|
61
68
|
throw new ConfigError(`Duplicate locale code "${locale.code}" in "config.i18n.locales".`, {
|
|
62
69
|
configKey: locale['~k']
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/ import { serializer } from '@lowdefy/helpers';
|
|
16
|
+
// Always written, even unconfigured - the runtime reads mcp.configured and
|
|
17
|
+
// never needs an existence check on the artifact.
|
|
18
|
+
async function writeMcp({ components, context }) {
|
|
19
|
+
await context.writeBuildArtifact('mcp.json', serializer.serializeToString(components.mcp));
|
|
20
|
+
}
|
|
21
|
+
export default writeMcp;
|