@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.
Files changed (53) hide show
  1. package/dist/build/buildApi/buildRoutine/countStepTypes.js +3 -0
  2. package/dist/build/buildApi/buildRoutine/setStepId.js +3 -2
  3. package/dist/build/buildApi/buildRoutine/validateStep.js +19 -0
  4. package/dist/build/buildApi/validateEndpoint.js +10 -0
  5. package/dist/build/buildApi/validateStepReferences.js +4 -4
  6. package/dist/build/buildAuth/buildApiAuth.js +2 -1
  7. package/dist/build/buildAuth/buildPageAuth.js +2 -1
  8. package/dist/build/buildAuth/getApiRoles.js +12 -6
  9. package/dist/build/buildAuth/getPageRoles.js +12 -6
  10. package/dist/build/buildAuth/getProtectedApi.js +3 -2
  11. package/dist/build/buildAuth/getProtectedPages.js +3 -2
  12. package/dist/build/buildAuth/matchPattern.js +22 -0
  13. package/dist/build/buildConnections.js +42 -4
  14. package/dist/build/buildJs/jsMapParser.js +25 -12
  15. package/dist/build/buildJs/writeJs.js +2 -2
  16. package/dist/build/buildMenu.js +41 -0
  17. package/dist/build/buildModuleDefs.js +97 -0
  18. package/dist/build/buildModules.js +96 -0
  19. package/dist/build/buildPages/buildBlock/buildBlock.js +2 -2
  20. package/dist/build/buildPages/buildBlock/buildEvents.js +16 -1
  21. package/dist/build/buildPages/buildBlock/buildSubBlocks.js +2 -1
  22. package/dist/build/buildPages/buildBlock/validateBlock.js +3 -3
  23. package/dist/build/buildPages/buildPage.js +1 -0
  24. package/dist/build/buildPages/validateCallApiRefs.js +31 -0
  25. package/dist/build/buildRefs/getModuleRefContent.js +81 -0
  26. package/dist/build/buildRefs/makeRefDefinition.js +6 -0
  27. package/dist/build/buildRefs/walker.js +424 -44
  28. package/dist/build/fetchGitHubModule.js +94 -0
  29. package/dist/build/fetchModules.js +60 -0
  30. package/dist/build/full/buildPages.js +10 -1
  31. package/dist/build/full/writePages.js +1 -1
  32. package/dist/build/jit/buildPageJit.js +34 -4
  33. package/dist/build/jit/collectSkeletonSourceFiles.js +8 -0
  34. package/dist/build/jit/createPageRegistry.js +10 -1
  35. package/dist/build/jit/shallowBuild.js +22 -11
  36. package/dist/build/jit/writePageJit.js +2 -2
  37. package/dist/build/jit/writeSourcelessPages.js +1 -1
  38. package/dist/build/parseModuleSource.js +48 -0
  39. package/dist/build/registerModules.js +242 -0
  40. package/dist/build/resolveDepTarget.js +43 -0
  41. package/dist/build/resolveModuleDependencies.js +60 -0
  42. package/dist/build/resolveModuleOperators.js +27 -0
  43. package/dist/build/testSchema.js +22 -11
  44. package/dist/build/writePluginImports/writeGlobalsCss.js +1 -1
  45. package/dist/createContext.js +4 -0
  46. package/dist/defaultPackages.js +51 -0
  47. package/dist/defaultTypesMap.js +399 -357
  48. package/dist/index.js +16 -1
  49. package/dist/indexDev.js +3 -1
  50. package/dist/lowdefySchema.js +58 -0
  51. package/dist/scripts/generateDefaultTypes.js +1 -35
  52. package/package.json +46 -42
  53. package/dist/build/jit/stripPageContent.js +0 -29
@@ -13,6 +13,9 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ function countStepTypes(step, { typeCounters }) {
16
+ if (step.type === 'CallApi') {
17
+ return;
18
+ }
16
19
  typeCounters.requests.increment(step.type, step['~k']);
17
20
  }
18
21
  export default countStepTypes;
@@ -13,8 +13,9 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ function setStepId(step, { endpointId }) {
16
- step.requestId = step.id;
16
+ step.stepId = step.id;
17
17
  step.endpointId = endpointId;
18
- step.id = `request:${endpointId}:${step.requestId}`;
18
+ const prefix = step.type === 'CallApi' ? 'endpoint' : 'request';
19
+ step.id = `${prefix}:${endpointId}:${step.stepId}`;
19
20
  }
20
21
  export default setStepId;
@@ -50,6 +50,25 @@ function validateStep(step, { endpointId }) {
50
50
  configKey
51
51
  });
52
52
  }
53
+ if (step.type === 'CallApi') {
54
+ if (type.isNone(step.properties?.endpointId)) {
55
+ throw new ConfigError(`Endpoint step "${step.id}" at endpoint "${endpointId}" requires properties.endpointId.`, {
56
+ configKey
57
+ });
58
+ }
59
+ if (!type.isString(step.properties.endpointId) && !type.isObject(step.properties.endpointId)) {
60
+ throw new ConfigError(`Endpoint step "${step.id}" at endpoint "${endpointId}" properties.endpointId is not a string.`, {
61
+ received: step.properties.endpointId,
62
+ configKey
63
+ });
64
+ }
65
+ if (!type.isNone(step.connectionId)) {
66
+ throw new ConfigError(`Endpoint step "${step.id}" at endpoint "${endpointId}" should not have a connectionId.`, {
67
+ configKey
68
+ });
69
+ }
70
+ return;
71
+ }
53
72
  if (type.isUndefined(step.connectionId)) {
54
73
  throw new ConfigError(`Step connectionId missing at endpoint "${endpointId}".`, {
55
74
  configKey
@@ -44,6 +44,16 @@ function validateEndpoint({ endpoint, index, checkDuplicateEndpointId }) {
44
44
  configKey
45
45
  });
46
46
  }
47
+ const validEndpointTypes = [
48
+ 'Api',
49
+ 'InternalApi'
50
+ ];
51
+ if (!validEndpointTypes.includes(endpoint.type)) {
52
+ throw new ConfigError(`Endpoint type "${endpoint.type}" is not valid at "${endpoint.id}". Must be one of: ${validEndpointTypes.join(', ')}.`, {
53
+ received: endpoint.type,
54
+ configKey
55
+ });
56
+ }
47
57
  checkDuplicateEndpointId({
48
58
  id: endpoint.id,
49
59
  configKey
@@ -17,16 +17,16 @@ import { type } from '@lowdefy/helpers';
17
17
  import extractOperatorKey from '../../utils/extractOperatorKey.js';
18
18
  import traverseConfig from '../../utils/traverseConfig.js';
19
19
  // Collect all step IDs from a routine (including nested control structures)
20
- // Note: After buildRoutine, steps have requestId (original id) and id is modified
20
+ // Note: After buildRoutine, steps have stepId (original id) and id is modified
21
21
  function collectStepIds(routine, stepIds) {
22
22
  if (type.isArray(routine)) {
23
23
  routine.forEach((item)=>collectStepIds(item, stepIds));
24
24
  return;
25
25
  }
26
26
  if (type.isObject(routine)) {
27
- // Check if this is a step (has requestId after build, or id before build)
28
- if (routine.requestId) {
29
- stepIds.add(routine.requestId);
27
+ // Check if this is a step (has stepId after build, or id before build)
28
+ if (routine.stepId) {
29
+ stepIds.add(routine.stepId);
30
30
  }
31
31
  // Recurse into all values (handles control structures like :then, :else, :try, :catch)
32
32
  Object.values(routine).forEach((value)=>collectStepIds(value, stepIds));
@@ -16,6 +16,7 @@
16
16
  import { ConfigError } from '@lowdefy/errors';
17
17
  import getApiRoles from './getApiRoles.js';
18
18
  import getProtectedApi from './getProtectedApi.js';
19
+ import { isInPatternList } from './matchPattern.js';
19
20
  function buildApiAuth({ components, context }) {
20
21
  const protectedApiEndpoints = getProtectedApi({
21
22
  components
@@ -29,7 +30,7 @@ function buildApiAuth({ components, context }) {
29
30
  }
30
31
  (components.api || []).forEach((endpoint)=>{
31
32
  if (apiRoles[endpoint.id]) {
32
- if (configPublicApi.includes(endpoint.id)) {
33
+ if (isInPatternList(endpoint.id, configPublicApi)) {
33
34
  throw new ConfigError(`Endpoint "${endpoint.id}" is both protected by roles and public.`, {
34
35
  received: apiRoles[endpoint.id],
35
36
  configKey: endpoint['~k']
@@ -16,6 +16,7 @@
16
16
  import { ConfigError } from '@lowdefy/errors';
17
17
  import getPageRoles from './getPageRoles.js';
18
18
  import getProtectedPages from './getProtectedPages.js';
19
+ import { isInPatternList } from './matchPattern.js';
19
20
  function buildPageAuth({ components, context }) {
20
21
  const protectedPages = getProtectedPages({
21
22
  components
@@ -36,7 +37,7 @@ function buildPageAuth({ components, context }) {
36
37
  return;
37
38
  }
38
39
  if (pageRoles[page.id]) {
39
- if (configPublicPages.includes(page.id)) {
40
+ if (isInPatternList(page.id, configPublicPages)) {
40
41
  throw new ConfigError(`Page "${page.id}" is both protected by roles and public.`, {
41
42
  received: pageRoles[page.id],
42
43
  configKey: page['~k']
@@ -12,15 +12,21 @@
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
- */ function getApiRoles({ components }) {
15
+ */ import { matchesPattern } from './matchPattern.js';
16
+ function getApiRoles({ components }) {
16
17
  const roles = components.auth.api.roles;
18
+ const endpointIds = (components.api ?? []).map((e)=>e.id);
17
19
  const apiRoles = {};
18
20
  Object.keys(roles).forEach((roleName)=>{
19
- roles[roleName].forEach((endpointId)=>{
20
- if (!apiRoles[endpointId]) {
21
- apiRoles[endpointId] = new Set();
22
- }
23
- apiRoles[endpointId].add(roleName);
21
+ roles[roleName].forEach((pattern)=>{
22
+ endpointIds.forEach((endpointId)=>{
23
+ if (matchesPattern(endpointId, pattern)) {
24
+ if (!apiRoles[endpointId]) {
25
+ apiRoles[endpointId] = new Set();
26
+ }
27
+ apiRoles[endpointId].add(roleName);
28
+ }
29
+ });
24
30
  });
25
31
  });
26
32
  Object.keys(apiRoles).forEach((endpointId)=>{
@@ -12,15 +12,21 @@
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
- */ function getPageRoles({ components }) {
15
+ */ import { matchesPattern } from './matchPattern.js';
16
+ function getPageRoles({ components }) {
16
17
  const roles = components.auth.pages.roles;
18
+ const pageIds = (components.pages ?? []).map((p)=>p.id);
17
19
  const pageRoles = {};
18
20
  Object.keys(roles).forEach((roleName)=>{
19
- roles[roleName].forEach((pageId)=>{
20
- if (!pageRoles[pageId]) {
21
- pageRoles[pageId] = new Set();
22
- }
23
- pageRoles[pageId].add(roleName);
21
+ roles[roleName].forEach((pattern)=>{
22
+ pageIds.forEach((pageId)=>{
23
+ if (matchesPattern(pageId, pattern)) {
24
+ if (!pageRoles[pageId]) {
25
+ pageRoles[pageId] = new Set();
26
+ }
27
+ pageRoles[pageId].add(roleName);
28
+ }
29
+ });
24
30
  });
25
31
  });
26
32
  Object.keys(pageRoles).forEach((pageId)=>{
@@ -13,15 +13,16 @@
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 { isInPatternList } from './matchPattern.js';
16
17
  function getProtectedApi({ components }) {
17
18
  const endpointIds = (components.api || []).map((endpoint)=>endpoint.id);
18
19
  let protectedApi = [];
19
20
  if (type.isArray(components.auth.api.public)) {
20
- protectedApi = endpointIds.filter((endpointId)=>!components.auth.api.public.includes(endpointId));
21
+ protectedApi = endpointIds.filter((endpointId)=>!isInPatternList(endpointId, components.auth.api.public));
21
22
  } else if (components.auth.api.protected === true) {
22
23
  protectedApi = endpointIds;
23
24
  } else if (type.isArray(components.auth.api.protected)) {
24
- protectedApi = components.auth.api.protected;
25
+ protectedApi = endpointIds.filter((endpointId)=>isInPatternList(endpointId, components.auth.api.protected));
25
26
  }
26
27
  return protectedApi;
27
28
  }
@@ -13,15 +13,16 @@
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 { isInPatternList } from './matchPattern.js';
16
17
  function getProtectedPages({ components }) {
17
18
  const pageIds = (components.pages || []).map((page)=>page.id);
18
19
  let protectedPages = [];
19
20
  if (type.isArray(components.auth.pages.public)) {
20
- protectedPages = pageIds.filter((pageId)=>!components.auth.pages.public.includes(pageId));
21
+ protectedPages = pageIds.filter((pageId)=>!isInPatternList(pageId, components.auth.pages.public));
21
22
  } else if (components.auth.pages.protected === true) {
22
23
  protectedPages = pageIds;
23
24
  } else if (type.isArray(components.auth.pages.protected)) {
24
- protectedPages = components.auth.pages.protected;
25
+ protectedPages = pageIds.filter((pageId)=>isInPatternList(pageId, components.auth.pages.protected));
25
26
  }
26
27
  return protectedPages;
27
28
  }
@@ -0,0 +1,22 @@
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 picomatch from 'picomatch';
16
+ function matchesPattern(id, pattern) {
17
+ return picomatch.isMatch(id, pattern);
18
+ }
19
+ function isInPatternList(id, patternList) {
20
+ return patternList.some((pattern)=>matchesPattern(id, pattern));
21
+ }
22
+ export { matchesPattern, isInPatternList };
@@ -12,20 +12,58 @@
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 countOperators from '../utils/countOperators.js';
15
+ */ import { type } from '@lowdefy/helpers';
16
+ import { ConfigError } from '@lowdefy/errors';
17
+ import collectExceptions from '../utils/collectExceptions.js';
18
+ import countOperators from '../utils/countOperators.js';
16
19
  import createCheckDuplicateId from '../utils/createCheckDuplicateId.js';
17
20
  import validateId from '../utils/validateId.js';
21
+ function validateConnection(connection, context) {
22
+ const configKey = connection?.['~k'];
23
+ if (!type.isObject(connection)) {
24
+ collectExceptions(context, new ConfigError('Connection should be an object.', {
25
+ received: connection,
26
+ configKey
27
+ }));
28
+ return false;
29
+ }
30
+ if (type.isUndefined(connection.id)) {
31
+ collectExceptions(context, new ConfigError('Connection id missing.', {
32
+ configKey
33
+ }));
34
+ return false;
35
+ }
36
+ if (!type.isString(connection.id)) {
37
+ collectExceptions(context, new ConfigError('Connection id is not a string.', {
38
+ received: connection.id,
39
+ configKey
40
+ }));
41
+ return false;
42
+ }
43
+ if (type.isNone(connection.type)) {
44
+ collectExceptions(context, new ConfigError(`Connection type is not defined at connection "${connection.id}".`, {
45
+ configKey
46
+ }));
47
+ return false;
48
+ }
49
+ if (!type.isString(connection.type)) {
50
+ collectExceptions(context, new ConfigError(`Connection type is not a string at connection "${connection.id}".`, {
51
+ received: connection.type,
52
+ configKey
53
+ }));
54
+ return false;
55
+ }
56
+ return true;
57
+ }
18
58
  function buildConnections({ components, context }) {
19
59
  // Store connection IDs for validation in buildRequests
20
60
  context.connectionIds = new Set();
21
- // Schema validates: id required, id is string, type is string
22
- // Only check for duplicates here (schema can't do that)
23
61
  const checkDuplicateConnectionId = createCheckDuplicateId({
24
62
  message: 'Duplicate connectionId "{{ id }}".'
25
63
  });
26
64
  (components.connections ?? []).forEach((connection)=>{
65
+ if (!validateConnection(connection, context)) return;
27
66
  const configKey = connection['~k'];
28
- // Check duplicates (schema can't validate this)
29
67
  checkDuplicateConnectionId({
30
68
  id: connection.id,
31
69
  configKey
@@ -15,12 +15,10 @@
15
15
  */ import { ConfigError } from '@lowdefy/errors';
16
16
  import { serializer, type } from '@lowdefy/helpers';
17
17
  import crypto from 'crypto';
18
- function makeHash({ jsMap, env, value }) {
18
+ function hashFn({ jsMap, env, value }) {
19
19
  const hash = crypto.createHash('sha1').update(value).digest('base64');
20
20
  jsMap[env][hash] = value;
21
- return {
22
- _js: hash
23
- };
21
+ return hash;
24
22
  }
25
23
  function JsMapParser({ input, jsMap, env }) {
26
24
  if (!jsMap[env]) {
@@ -31,15 +29,30 @@ function JsMapParser({ input, jsMap, env }) {
31
29
  if (Object.keys(value).length !== 1) return value;
32
30
  const key = Object.keys(value)[0];
33
31
  if (key !== '_js') return value;
34
- if (!type.isString(value[key])) {
35
- throw new ConfigError(`_js operator expects the JavaScript definition as a string. Received ${JSON.stringify(value[key])}.`, {
36
- configKey: value['~k']
37
- });
32
+ const inner = value[key];
33
+ if (type.isString(inner)) {
34
+ return {
35
+ _js: hashFn({
36
+ jsMap,
37
+ env,
38
+ value: inner
39
+ })
40
+ };
41
+ }
42
+ if (type.isObject(inner) && type.isString(inner.fn)) {
43
+ return {
44
+ _js: {
45
+ fn: hashFn({
46
+ jsMap,
47
+ env,
48
+ value: inner.fn
49
+ }),
50
+ args: inner.args
51
+ }
52
+ };
38
53
  }
39
- return makeHash({
40
- jsMap,
41
- env,
42
- value: value[key]
54
+ throw new ConfigError(`_js operator expects a JavaScript string or { fn: string, args?: object }. Received ${JSON.stringify(inner)}.`, {
55
+ configKey: value['~k']
43
56
  });
44
57
  };
45
58
  return serializer.copy(input, {
@@ -16,11 +16,11 @@
16
16
  async function writeJs({ context }) {
17
17
  await context.writeBuildArtifact('plugins/operators/clientJsMap.js', generateJsFile({
18
18
  map: context.jsMap.client,
19
- functionPrototype: `{ actions, event, input, location, lowdefyGlobal, request, state, urlQuery, user }`
19
+ functionPrototype: `{ actions, args, event, input, location, lowdefyGlobal, request, state, urlQuery, user }`
20
20
  }));
21
21
  await context.writeBuildArtifact('plugins/operators/serverJsMap.js', generateJsFile({
22
22
  map: context.jsMap.server,
23
- functionPrototype: `{ item, payload, secrets, state, step, user }`
23
+ functionPrototype: `{ args, item, payload, secrets, state, step, user }`
24
24
  }));
25
25
  }
26
26
  export default writeJs;
@@ -32,9 +32,50 @@ function buildDefaultMenu({ components, context }) {
32
32
  ];
33
33
  return menus;
34
34
  }
35
+ function validateMenuItem(menuItem, menuId, context) {
36
+ const configKey = menuItem?.['~k'];
37
+ if (!type.isObject(menuItem)) {
38
+ collectExceptions(context, new ConfigError(`Menu item should be an object on menu "${menuId}".`, {
39
+ received: menuItem,
40
+ configKey
41
+ }));
42
+ return false;
43
+ }
44
+ if (type.isUndefined(menuItem.id)) {
45
+ collectExceptions(context, new ConfigError(`Menu item id missing on menu "${menuId}".`, {
46
+ configKey
47
+ }));
48
+ return false;
49
+ }
50
+ if (!type.isString(menuItem.id)) {
51
+ collectExceptions(context, new ConfigError(`Menu item id is not a string on menu "${menuId}".`, {
52
+ received: menuItem.id,
53
+ configKey
54
+ }));
55
+ return false;
56
+ }
57
+ if (type.isNone(menuItem.type)) {
58
+ collectExceptions(context, new ConfigError(`Menu item type is not defined at "${menuItem.id}" on menu "${menuId}".`, {
59
+ configKey
60
+ }));
61
+ return false;
62
+ }
63
+ if (!type.isString(menuItem.type)) {
64
+ collectExceptions(context, new ConfigError(`Menu item type is not a string at "${menuItem.id}" on menu "${menuId}".`, {
65
+ received: menuItem.type,
66
+ configKey
67
+ }));
68
+ return false;
69
+ }
70
+ return true;
71
+ }
35
72
  function loopItems({ parent, menuId, pages, missingPageWarnings, checkDuplicateMenuItemId, context }) {
36
73
  if (type.isArray(parent.links)) {
37
74
  parent.links.forEach((menuItem)=>{
75
+ if (!validateMenuItem(menuItem, menuId, context)) {
76
+ menuItem.remove = true;
77
+ return;
78
+ }
38
79
  const configKey = menuItem['~k'];
39
80
  if (menuItem.type === 'MenuLink') {
40
81
  if (type.isString(menuItem.pageId)) {
@@ -0,0 +1,97 @@
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 operators from '@lowdefy/operators-js/operators/build';
16
+ import { resolve, WalkContext } from './buildRefs/walker.js';
17
+ import getRefContent from './buildRefs/getRefContent.js';
18
+ import makeRefDefinition from './buildRefs/makeRefDefinition.js';
19
+ import evaluateStaticOperators from './buildRefs/evaluateStaticOperators.js';
20
+ import collectDynamicIdentifiers from './collectDynamicIdentifiers.js';
21
+ import validateOperatorsDynamic from './validateOperatorsDynamic.js';
22
+ import fetchModules from './fetchModules.js';
23
+ import { resolveLocalManifest, resolveFullManifest } from './registerModules.js';
24
+ import resolveModuleDependencies from './resolveModuleDependencies.js';
25
+ validateOperatorsDynamic({
26
+ operators
27
+ });
28
+ const dynamicIdentifiers = collectDynamicIdentifiers({
29
+ operators
30
+ });
31
+ async function parseLowdefyYaml({ context }) {
32
+ const refDef = makeRefDefinition('lowdefy.yaml', null, context.refMap);
33
+ const content = await getRefContent({
34
+ context,
35
+ refDef,
36
+ referencedFrom: null
37
+ });
38
+ const ctx = new WalkContext({
39
+ buildContext: context,
40
+ refId: refDef.id,
41
+ sourceRefId: null,
42
+ vars: {},
43
+ path: '',
44
+ currentFile: refDef.path,
45
+ refChain: new Set(refDef.path ? [
46
+ refDef.path
47
+ ] : []),
48
+ operators,
49
+ env: process.env,
50
+ dynamicIdentifiers,
51
+ shouldStop: (path)=>{
52
+ if (path.startsWith('modules')) return false;
53
+ return 'preserve';
54
+ }
55
+ });
56
+ let config = await resolve(content, ctx);
57
+ config = evaluateStaticOperators({
58
+ context,
59
+ input: config,
60
+ refDef
61
+ });
62
+ return config ?? {};
63
+ }
64
+ async function buildModuleDefs({ context }) {
65
+ const lowdefyConfig = await parseLowdefyYaml({
66
+ context
67
+ });
68
+ context.plugins = lowdefyConfig.plugins ?? [];
69
+ const moduleEntries = lowdefyConfig.modules ?? [];
70
+ if (moduleEntries.length === 0) {
71
+ return;
72
+ }
73
+ const resolvedPaths = await fetchModules({
74
+ moduleEntries,
75
+ context
76
+ });
77
+ // Step 1: Local resolve — concrete arrays, preserved content, exports/deps extracted
78
+ for (const entry of moduleEntries){
79
+ await resolveLocalManifest({
80
+ entry,
81
+ resolvedPaths: resolvedPaths[entry.id],
82
+ context
83
+ });
84
+ }
85
+ // Step 2: Auto-wire and validate dependency wiring
86
+ resolveModuleDependencies({
87
+ context
88
+ });
89
+ // Step 3: Full resolve — cross-module refs, preserved content
90
+ for (const entryId of Object.keys(context.modules)){
91
+ await resolveFullManifest({
92
+ entryId,
93
+ context
94
+ });
95
+ }
96
+ }
97
+ export default buildModuleDefs;
@@ -0,0 +1,96 @@
1
+ /* eslint-disable no-param-reassign */ /*
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 { ConfigError } from '@lowdefy/errors';
16
+ import { serializer, type } from '@lowdefy/helpers';
17
+ function validateModuleSecrets({ content, manifest, entryId }) {
18
+ const declaredSecrets = new Set((manifest.secrets ?? []).map((s)=>s.name));
19
+ serializer.copy(content, {
20
+ reviver: (_, value)=>{
21
+ if (!type.isObject(value)) return value;
22
+ const keys = Object.keys(value).filter((k)=>!k.startsWith('~'));
23
+ if (keys.length !== 1) return value;
24
+ if (!type.isUndefined(value['_secret'])) {
25
+ const secretName = value['_secret'];
26
+ if (type.isString(secretName) && !declaredSecrets.has(secretName)) {
27
+ throw new ConfigError(`Module "${entryId}" references secret "${secretName}" ` + `but does not declare it in module.lowdefy.yaml secrets. ` + `Add it to the module's secrets list or remove the reference.`);
28
+ }
29
+ }
30
+ return value;
31
+ }
32
+ });
33
+ }
34
+ function buildModules({ components, context }) {
35
+ const moduleEntries = components.modules ?? [];
36
+ delete components.modules;
37
+ for (const entry of moduleEntries){
38
+ const moduleEntry = context.modules[entry.id];
39
+ if (!moduleEntry) {
40
+ throw new ConfigError(`Module entry "${entry.id}" not registered. ` + `Check that buildModuleDefs ran successfully.`);
41
+ }
42
+ const manifest = moduleEntry.manifest;
43
+ // Validate connection remapping keys
44
+ const remapping = moduleEntry.connections ?? {};
45
+ const moduleConnIds = new Set((manifest.connections ?? []).map((c)=>c.id));
46
+ for (const remapKey of Object.keys(remapping)){
47
+ if (!moduleConnIds.has(remapKey)) {
48
+ throw new ConfigError(`Module "${entry.id}" connection remapping references "${remapKey}", ` + `but the module does not export a connection with that id.`);
49
+ }
50
+ }
51
+ // Validate secret whitelist on non-remapped content
52
+ for (const page of manifest.pages ?? []){
53
+ validateModuleSecrets({
54
+ content: page,
55
+ manifest,
56
+ entryId: entry.id
57
+ });
58
+ }
59
+ for (const conn of manifest.connections ?? []){
60
+ if (remapping[conn.id]) continue;
61
+ validateModuleSecrets({
62
+ content: conn,
63
+ manifest,
64
+ entryId: entry.id
65
+ });
66
+ }
67
+ for (const endpoint of manifest.api ?? []){
68
+ validateModuleSecrets({
69
+ content: endpoint,
70
+ manifest,
71
+ entryId: entry.id
72
+ });
73
+ }
74
+ // Process pages
75
+ for (const page of manifest.pages ?? []){
76
+ page.id = `${entry.id}/${page.id}`;
77
+ components.pages = components.pages ?? [];
78
+ components.pages.push(page);
79
+ }
80
+ // Process connections (skip remapped -- app provides those)
81
+ for (const conn of manifest.connections ?? []){
82
+ if (remapping[conn.id]) continue;
83
+ conn.id = `${entry.id}/${conn.id}`;
84
+ components.connections = components.connections ?? [];
85
+ components.connections.push(conn);
86
+ }
87
+ // Process API endpoints
88
+ for (const endpoint of manifest.api ?? []){
89
+ endpoint.id = `${entry.id}/${endpoint.id}`;
90
+ components.api = components.api ?? [];
91
+ components.api.push(endpoint);
92
+ }
93
+ }
94
+ return components;
95
+ }
96
+ export default buildModules;
@@ -25,8 +25,8 @@ import normalizeLayout from './normalizeLayout.js';
25
25
  import setBlockId from './setBlockId.js';
26
26
  import validateBlock from './validateBlock.js';
27
27
  import validateSlots from './validateSlots.js';
28
- function buildBlock(block, pageContext) {
29
- validateBlock(block, pageContext);
28
+ function buildBlock(block, pageContext, parentConfigKey) {
29
+ validateBlock(block, pageContext, parentConfigKey);
30
30
  setBlockId(block, pageContext);
31
31
  normalizeLayout(block, pageContext);
32
32
  moveAreasToSlots(block, pageContext);