@lowdefy/build 0.0.0-experimental-20251203202233 → 0.0.0-experimental-20260112140412

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 (52) hide show
  1. package/dist/build/addKeys.js +2 -0
  2. package/dist/build/buildApi/buildEndpoint.js +6 -0
  3. package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
  4. package/dist/build/buildApi/validateStepReferences.js +65 -0
  5. package/dist/build/buildAuth/buildAuthPlugins.js +42 -12
  6. package/dist/build/buildAuth/validateAuthConfig.js +10 -2
  7. package/dist/build/buildAuth/validateMutualExclusivity.js +18 -4
  8. package/dist/build/buildConnections.js +25 -6
  9. package/dist/build/buildImports/buildIconImports.js +19 -36
  10. package/dist/build/buildLogger.js +41 -0
  11. package/dist/build/buildMenu.js +40 -13
  12. package/dist/build/buildPages/buildBlock/buildEvents.js +90 -9
  13. package/dist/build/buildPages/buildBlock/buildRequests.js +47 -7
  14. package/dist/build/buildPages/buildBlock/countBlockTypes.js +1 -1
  15. package/dist/build/buildPages/buildBlock/validateBlock.js +33 -7
  16. package/dist/build/buildPages/buildPage.js +28 -4
  17. package/dist/build/buildPages/buildPages.js +26 -1
  18. package/dist/build/buildPages/buildTestPage.js +9 -1
  19. package/dist/build/buildPages/validateLinkReferences.js +33 -0
  20. package/dist/build/buildPages/validatePayloadReferences.js +52 -0
  21. package/dist/build/buildPages/validateRequestReferences.js +33 -0
  22. package/dist/build/buildPages/validateStateReferences.js +52 -0
  23. package/dist/build/buildRefs/buildRefs.js +11 -30
  24. package/dist/build/buildRefs/evaluateBuildOperators.js +8 -2
  25. package/dist/build/buildRefs/getConfigFile.js +2 -1
  26. package/dist/build/buildRefs/getRefContent.js +17 -31
  27. package/dist/build/buildRefs/getRefsFromFile.js +8 -3
  28. package/dist/build/buildRefs/makeRefDefinition.js +9 -8
  29. package/dist/build/buildRefs/parseRefContent.js +61 -2
  30. package/dist/build/buildRefs/populateRefs.js +14 -11
  31. package/dist/build/buildRefs/recursiveBuild.js +64 -63
  32. package/dist/build/buildTypes.js +19 -2
  33. package/dist/build/formatBuildError.js +34 -0
  34. package/dist/build/writeLogger.js +19 -0
  35. package/dist/createContext.js +2 -15
  36. package/dist/defaultTypesMap.js +494 -487
  37. package/dist/index.js +130 -125
  38. package/dist/lowdefySchema.js +75 -0
  39. package/dist/test/testContext.js +2 -1
  40. package/dist/utils/countOperators.js +24 -11
  41. package/dist/utils/createCheckDuplicateId.js +30 -9
  42. package/dist/utils/createCounter.js +15 -2
  43. package/dist/utils/extractOperatorKey.js +36 -0
  44. package/dist/utils/findSimilarString.js +53 -0
  45. package/dist/utils/formatConfigError.js +24 -0
  46. package/dist/utils/formatConfigMessage.js +33 -0
  47. package/dist/utils/formatConfigWarning.js +24 -0
  48. package/dist/utils/traverseConfig.js +43 -0
  49. package/package.json +39 -39
  50. package/dist/utils/createBuildProfiler.js +0 -125
  51. package/dist/utils/invalidateChangedFiles.js +0 -74
  52. package/dist/utils/makeRefHash.js +0 -15
@@ -49,6 +49,7 @@ function recAddKeys({ object, key, keyMap, parentKeyMapId }) {
49
49
  keyMap[keyMapId] = {
50
50
  key,
51
51
  '~r': object['~r'],
52
+ '~l': object['~l'],
52
53
  '~k_parent': parentKeyMapId
53
54
  };
54
55
  Object.defineProperty(object, '~k', {
@@ -58,6 +59,7 @@ function recAddKeys({ object, key, keyMap, parentKeyMapId }) {
58
59
  configurable: true
59
60
  });
60
61
  delete object['~r'];
62
+ delete object['~l'];
61
63
  Object.keys(object).forEach((nextKey)=>{
62
64
  if (type.isObject(object[nextKey])) {
63
65
  recAddKeys({
@@ -14,6 +14,7 @@
14
14
  limitations under the License.
15
15
  */ import buildRoutine from './buildRoutine/buildRoutine.js';
16
16
  import validateEndpoint from './validateEndpoint.js';
17
+ import validateStepReferences from './validateStepReferences.js';
17
18
  function buildEndpoint({ endpoint, index, context, checkDuplicateEndpointId }) {
18
19
  validateEndpoint({
19
20
  endpoint,
@@ -25,6 +26,11 @@ function buildEndpoint({ endpoint, index, context, checkDuplicateEndpointId }) {
25
26
  endpointId: endpoint.endpointId,
26
27
  typeCounters: context.typeCounters
27
28
  });
29
+ // Validate that _step references point to defined step IDs
30
+ validateStepReferences({
31
+ endpoint,
32
+ context
33
+ });
28
34
  endpoint.id = `endpoint:${endpoint.endpointId}`;
29
35
  }
30
36
  export default buildEndpoint;
@@ -13,6 +13,6 @@
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
- typeCounters.requests.increment(step.type);
16
+ typeCounters.requests.increment(step.type, step['~k']);
17
17
  }
18
18
  export default countStepTypes;
@@ -0,0 +1,65 @@
1
+ /*
2
+ Copyright 2020-2024 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 extractOperatorKey from '../../utils/extractOperatorKey.js';
17
+ import formatConfigWarning from '../../utils/formatConfigWarning.js';
18
+ import traverseConfig from '../../utils/traverseConfig.js';
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
21
+ function collectStepIds(routine, stepIds) {
22
+ if (type.isArray(routine)) {
23
+ routine.forEach((item)=>collectStepIds(item, stepIds));
24
+ return;
25
+ }
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);
30
+ }
31
+ // Recurse into all values (handles control structures like :then, :else, :try, :catch)
32
+ Object.values(routine).forEach((value)=>collectStepIds(value, stepIds));
33
+ }
34
+ }
35
+ function validateStepReferences({ endpoint, context }) {
36
+ // Collect all step IDs defined in the routine
37
+ const stepIds = new Set();
38
+ collectStepIds(endpoint.routine, stepIds);
39
+ // Find _step references in the routine
40
+ const stepRefs = new Map(); // topLevelKey -> configKey (first occurrence)
41
+ traverseConfig({
42
+ config: endpoint.routine,
43
+ visitor: (obj)=>{
44
+ if (obj._step !== undefined) {
45
+ const stepId = extractOperatorKey({
46
+ operatorValue: obj._step
47
+ });
48
+ if (stepId && !stepRefs.has(stepId)) {
49
+ stepRefs.set(stepId, obj['~k']);
50
+ }
51
+ }
52
+ }
53
+ });
54
+ // Warn for undefined step references
55
+ stepRefs.forEach((configKey, stepId)=>{
56
+ if (stepIds.has(stepId)) return;
57
+ const message = `_step references "${stepId}" in endpoint "${endpoint.endpointId}", ` + `but no step with id "${stepId}" exists in the routine. ` + `Step IDs are defined by the "id" property of each step. ` + `Check for typos or add a step with this id.`;
58
+ context.logger.warn(formatConfigWarning({
59
+ message,
60
+ configKey,
61
+ context
62
+ }));
63
+ });
64
+ }
65
+ export default validateStepReferences;
@@ -13,19 +13,33 @@
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
- function buildAuthPlugin({ counter, pluginConfig, typeClass }) {
16
+ import formatConfigError from '../../utils/formatConfigError.js';
17
+ function buildAuthPlugin({ counter, pluginConfig, typeClass, context }) {
17
18
  if (type.isArray(pluginConfig)) {
18
19
  pluginConfig.forEach((plugin)=>{
20
+ const configKey = plugin['~k'];
19
21
  if (type.isUndefined(plugin.id)) {
20
- throw new Error(`Auth ${typeClass} id missing.`);
22
+ throw new Error(formatConfigError({
23
+ message: `Auth ${typeClass} id missing.`,
24
+ configKey,
25
+ context
26
+ }));
21
27
  }
22
28
  if (!type.isString(plugin.id)) {
23
- throw new Error(`Auth ${typeClass} id is not a string. Received ${JSON.stringify(plugin.id)}.`);
29
+ throw new Error(formatConfigError({
30
+ message: `Auth ${typeClass} id is not a string. Received ${JSON.stringify(plugin.id)}.`,
31
+ configKey,
32
+ context
33
+ }));
24
34
  }
25
35
  if (!type.isString(plugin.type)) {
26
- throw new Error(`Auth ${typeClass} type is not a string at ${typeClass} "${plugin.id}". Received ${JSON.stringify(plugin.type)}.`);
36
+ throw new Error(formatConfigError({
37
+ message: `Auth ${typeClass} type is not a string at ${typeClass} "${plugin.id}". Received ${JSON.stringify(plugin.type)}.`,
38
+ configKey,
39
+ context
40
+ }));
27
41
  }
28
- counter.increment(plugin.type);
42
+ counter.increment(plugin.type, plugin['~k']);
29
43
  });
30
44
  }
31
45
  }
@@ -34,16 +48,29 @@ function buildAdapter({ components, context }) {
34
48
  if (type.isNone(adapter)) {
35
49
  return;
36
50
  }
51
+ const configKey = adapter['~k'];
37
52
  if (type.isUndefined(adapter.id)) {
38
- throw new Error(`Auth adapter id missing.`);
53
+ throw new Error(formatConfigError({
54
+ message: 'Auth adapter id missing.',
55
+ configKey,
56
+ context
57
+ }));
39
58
  }
40
59
  if (!type.isString(adapter.id)) {
41
- throw new Error(`Auth adapter id is not a string. Received ${JSON.stringify(adapter.id)}.`);
60
+ throw new Error(formatConfigError({
61
+ message: `Auth adapter id is not a string. Received ${JSON.stringify(adapter.id)}.`,
62
+ configKey,
63
+ context
64
+ }));
42
65
  }
43
66
  if (!type.isString(adapter.type)) {
44
- throw new Error(`Auth adapter type is not a string at adapter "${adapter.id}". Received ${JSON.stringify(adapter.type)}.`);
67
+ throw new Error(formatConfigError({
68
+ message: `Auth adapter type is not a string at adapter "${adapter.id}". Received ${JSON.stringify(adapter.type)}.`,
69
+ configKey,
70
+ context
71
+ }));
45
72
  }
46
- context.typeCounters.auth.adapters.increment(adapter.type);
73
+ context.typeCounters.auth.adapters.increment(adapter.type, adapter['~k']);
47
74
  }
48
75
  function buildAuthPlugins({ components, context }) {
49
76
  const counters = context.typeCounters.auth;
@@ -55,17 +82,20 @@ function buildAuthPlugins({ components, context }) {
55
82
  buildAuthPlugin({
56
83
  counter: counters.callbacks,
57
84
  pluginConfig: authConfig.callbacks,
58
- typeClass: 'callback'
85
+ typeClass: 'callback',
86
+ context
59
87
  });
60
88
  buildAuthPlugin({
61
89
  counter: counters.events,
62
90
  pluginConfig: authConfig.events,
63
- typeClass: 'event'
91
+ typeClass: 'event',
92
+ context
64
93
  });
65
94
  buildAuthPlugin({
66
95
  counter: counters.providers,
67
96
  pluginConfig: authConfig.providers,
68
- typeClass: 'provider'
97
+ typeClass: 'provider',
98
+ context
69
99
  });
70
100
  }
71
101
  export default buildAuthPlugins;
@@ -15,13 +15,19 @@
15
15
  */ import { type } from '@lowdefy/helpers';
16
16
  import { validate } from '@lowdefy/ajv';
17
17
  import lowdefySchema from '../../lowdefySchema.js';
18
+ import formatConfigError from '../../utils/formatConfigError.js';
18
19
  import validateMutualExclusivity from './validateMutualExclusivity.js';
19
- async function validateAuthConfig({ components }) {
20
+ async function validateAuthConfig({ components, context }) {
20
21
  if (type.isNone(components.auth)) {
21
22
  components.auth = {};
22
23
  }
23
24
  if (!type.isObject(components.auth)) {
24
- throw new Error('lowdefy.auth is not an object.');
25
+ const configKey = components.auth?.['~k'];
26
+ throw new Error(formatConfigError({
27
+ message: 'lowdefy.auth is not an object.',
28
+ configKey,
29
+ context
30
+ }));
25
31
  }
26
32
  if (type.isNone(components.auth.api)) {
27
33
  components.auth.api = {};
@@ -59,10 +65,12 @@ async function validateAuthConfig({ components }) {
59
65
  });
60
66
  validateMutualExclusivity({
61
67
  components,
68
+ context,
62
69
  entity: 'api'
63
70
  });
64
71
  validateMutualExclusivity({
65
72
  components,
73
+ context,
66
74
  entity: 'pages'
67
75
  });
68
76
  return components;
@@ -13,15 +13,29 @@
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
- function validateMutualExclusivity({ components, entity }) {
16
+ import formatConfigError from '../../utils/formatConfigError.js';
17
+ function validateMutualExclusivity({ components, context, entity }) {
18
+ const configKey = components.auth[entity]?.['~k'] || components.auth?.['~k'];
17
19
  if (components.auth[entity].protected === true && components.auth[entity].public === true || type.isArray(components.auth[entity].protected) && type.isArray(components.auth[entity].public)) {
18
- throw new Error(`Protected and public ${entity} are mutually exclusive. When protected ${entity} are listed, all unlisted ${entity} are public by default and vice versa.`);
20
+ throw new Error(formatConfigError({
21
+ message: `Protected and public ${entity} are mutually exclusive. When protected ${entity} are listed, all unlisted ${entity} are public by default and vice versa.`,
22
+ configKey,
23
+ context
24
+ }));
19
25
  }
20
26
  if (components.auth[entity].protected === false) {
21
- throw new Error(`Protected ${entity} can not be set to false.`);
27
+ throw new Error(formatConfigError({
28
+ message: `Protected ${entity} can not be set to false.`,
29
+ configKey,
30
+ context
31
+ }));
22
32
  }
23
33
  if (components.auth[entity].public === false) {
24
- throw new Error(`Public ${entity} can not be set to false.`);
34
+ throw new Error(formatConfigError({
35
+ message: `Public ${entity} can not be set to false.`,
36
+ configKey,
37
+ context
38
+ }));
25
39
  }
26
40
  }
27
41
  export default validateMutualExclusivity;
@@ -15,26 +15,45 @@
15
15
  */ import { type } from '@lowdefy/helpers';
16
16
  import countOperators from '../utils/countOperators.js';
17
17
  import createCheckDuplicateId from '../utils/createCheckDuplicateId.js';
18
+ import formatConfigError from '../utils/formatConfigError.js';
18
19
  function buildConnections({ components, context }) {
20
+ // Store connection IDs for validation in buildRequests
21
+ context.connectionIds = new Set();
19
22
  const checkDuplicateConnectionId = createCheckDuplicateId({
20
- message: 'Duplicate connectionId "{{ id }}".'
23
+ message: 'Duplicate connectionId "{{ id }}".',
24
+ context
21
25
  });
22
26
  if (type.isArray(components.connections)) {
23
27
  components.connections.forEach((connection)=>{
28
+ const configKey = connection['~k'];
24
29
  if (type.isUndefined(connection.id)) {
25
- throw new Error(`Connection id missing.`);
30
+ throw new Error(formatConfigError({
31
+ message: 'Connection id missing.',
32
+ configKey,
33
+ context
34
+ }));
26
35
  }
27
36
  if (!type.isString(connection.id)) {
28
- throw new Error(`Connection id is not a string. Received ${JSON.stringify(connection.id)}.`);
37
+ throw new Error(formatConfigError({
38
+ message: `Connection id is not a string. Received ${JSON.stringify(connection.id)}.`,
39
+ configKey,
40
+ context
41
+ }));
29
42
  }
30
43
  checkDuplicateConnectionId({
31
- id: connection.id
44
+ id: connection.id,
45
+ configKey
32
46
  });
33
47
  if (!type.isString(connection.type)) {
34
- throw new Error(`Connection type is not a string at connection "${connection.id}". Received ${JSON.stringify(connection.type)}.`);
48
+ throw new Error(formatConfigError({
49
+ message: `Connection type is not a string at connection "${connection.id}". Received ${JSON.stringify(connection.type)}.`,
50
+ configKey,
51
+ context
52
+ }));
35
53
  }
36
- context.typeCounters.connections.increment(connection.type);
54
+ context.typeCounters.connections.increment(connection.type, connection['~k']);
37
55
  connection.connectionId = connection.id;
56
+ context.connectionIds.add(connection.connectionId);
38
57
  connection.id = `connection:${connection.id}`;
39
58
  countOperators(connection.properties || {}, {
40
59
  counter: context.typeCounters.operators.server
@@ -12,8 +12,7 @@
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 createBuildProfiler from '../../utils/createBuildProfiler.js';
16
- const iconPackages = {
15
+ */ const iconPackages = {
17
16
  'react-icons/ai': /"(Ai[A-Z0-9]\w*)"/gm,
18
17
  'react-icons/bi': /"(Bi[A-Z0-9]\w*)"/gm,
19
18
  'react-icons/bs': /"(Bs[A-Z0-9]\w*)"/gm,
@@ -43,40 +42,27 @@ const iconPackages = {
43
42
  'react-icons/vsc': /"(Vsc[A-Z0-9]\w*)"/gm,
44
43
  'react-icons/wi': /"(Wi[A-Z0-9]\w*)"/gm
45
44
  };
46
- function getConfigIcons({ components, icons, regex, timeSync }) {
47
- timeSync('stringify:global', ()=>{
48
- [
49
- ...JSON.stringify(components.global || {}).matchAll(regex)
50
- ].map((match)=>icons.add(match[1]));
51
- });
52
- timeSync('stringify:menus', ()=>{
53
- [
54
- ...JSON.stringify(components.menus || []).matchAll(regex)
55
- ].map((match)=>icons.add(match[1]));
56
- });
57
- timeSync('stringify:pages', ()=>{
58
- [
59
- ...JSON.stringify(components.pages || []).matchAll(regex)
60
- ].map((match)=>icons.add(match[1]));
61
- });
45
+ function getConfigIcons({ components, icons, regex }) {
46
+ [
47
+ ...JSON.stringify(components.global || {}).matchAll(regex)
48
+ ].map((match)=>icons.add(match[1]));
49
+ [
50
+ ...JSON.stringify(components.menus || []).matchAll(regex)
51
+ ].map((match)=>icons.add(match[1]));
52
+ [
53
+ ...JSON.stringify(components.pages || []).matchAll(regex)
54
+ ].map((match)=>icons.add(match[1]));
62
55
  }
63
- function getBlockDefaultIcons({ blocks, context, icons, regex, timeSync }) {
64
- timeSync('blockDefaultIcons', ()=>{
65
- blocks.forEach((block)=>{
66
- (context.typesMap.icons[block.typeName] || []).forEach((icon)=>{
67
- [
68
- ...JSON.stringify(icon).matchAll(regex)
69
- ].map((match)=>icons.add(match[1]));
70
- });
56
+ function getBlockDefaultIcons({ blocks, context, icons, regex }) {
57
+ blocks.forEach((block)=>{
58
+ (context.typesMap.icons[block.typeName] || []).forEach((icon)=>{
59
+ [
60
+ ...JSON.stringify(icon).matchAll(regex)
61
+ ].map((match)=>icons.add(match[1]));
71
62
  });
72
63
  });
73
64
  }
74
65
  function buildIconImports({ blocks, components, context, defaults = {} }) {
75
- const profiler = createBuildProfiler({
76
- logger: context.logger,
77
- prefix: 'buildIconImports'
78
- });
79
- const timeSync = profiler.timeSync;
80
66
  const iconImports = [];
81
67
  Object.entries(iconPackages).forEach(([iconPackage, regex])=>{
82
68
  defaults;
@@ -84,15 +70,13 @@ function buildIconImports({ blocks, components, context, defaults = {} }) {
84
70
  getConfigIcons({
85
71
  components,
86
72
  icons,
87
- regex,
88
- timeSync
73
+ regex
89
74
  });
90
75
  getBlockDefaultIcons({
91
76
  blocks,
92
77
  context,
93
78
  icons,
94
- regex,
95
- timeSync
79
+ regex
96
80
  });
97
81
  iconImports.push({
98
82
  icons: [
@@ -101,7 +85,6 @@ function buildIconImports({ blocks, components, context, defaults = {} }) {
101
85
  package: iconPackage
102
86
  });
103
87
  });
104
- profiler.printSummary();
105
88
  return iconImports;
106
89
  }
107
90
  export default buildIconImports;
@@ -0,0 +1,41 @@
1
+ /*
2
+ Copyright 2020-2024 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
+ const sentryDefaults = {
17
+ client: true,
18
+ server: true,
19
+ tracesSampleRate: 0.1,
20
+ replaysSessionSampleRate: 0,
21
+ replaysOnErrorSampleRate: 0.1,
22
+ feedback: false,
23
+ userFields: [
24
+ 'id',
25
+ '_id'
26
+ ]
27
+ };
28
+ function buildLogger({ components }) {
29
+ if (type.isNone(components.logger)) {
30
+ components.logger = {};
31
+ }
32
+ // Only apply defaults if sentry is explicitly configured
33
+ if (!type.isNone(components.logger.sentry)) {
34
+ components.logger.sentry = {
35
+ ...sentryDefaults,
36
+ ...components.logger.sentry
37
+ };
38
+ }
39
+ return components;
40
+ }
41
+ export default buildLogger;
@@ -12,8 +12,9 @@
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 { type } from '@lowdefy/helpers';
15
+ */ import { type, resolveConfigLocation } from '@lowdefy/helpers';
16
16
  import createCheckDuplicateId from '../utils/createCheckDuplicateId.js';
17
+ import formatConfigError from '../utils/formatConfigError.js';
17
18
  function buildDefaultMenu({ components, context }) {
18
19
  context.logger.warn('No menus found. Building default menu.');
19
20
  const pages = type.isArray(components.pages) ? components.pages : [];
@@ -30,16 +31,18 @@ function buildDefaultMenu({ components, context }) {
30
31
  ];
31
32
  return menus;
32
33
  }
33
- function loopItems({ parent, menuId, pages, missingPageWarnings, checkDuplicateMenuItemId }) {
34
+ function loopItems({ parent, menuId, pages, missingPageWarnings, checkDuplicateMenuItemId, context }) {
34
35
  if (type.isArray(parent.links)) {
35
36
  parent.links.forEach((menuItem)=>{
37
+ const configKey = menuItem['~k'];
36
38
  if (menuItem.type === 'MenuLink') {
37
39
  if (type.isString(menuItem.pageId)) {
38
40
  const page = pages.find((pg)=>pg.pageId === menuItem.pageId);
39
41
  if (!page) {
40
42
  missingPageWarnings.push({
41
43
  menuItemId: menuItem.id,
42
- pageId: menuItem.pageId
44
+ pageId: menuItem.pageId,
45
+ configKey
43
46
  });
44
47
  // remove menuItem from menu
45
48
  menuItem.remove = true;
@@ -60,7 +63,8 @@ function loopItems({ parent, menuId, pages, missingPageWarnings, checkDuplicateM
60
63
  }
61
64
  checkDuplicateMenuItemId({
62
65
  id: menuItem.id,
63
- menuId
66
+ menuId,
67
+ configKey
64
68
  });
65
69
  menuItem.menuItemId = menuItem.id;
66
70
  menuItem.id = `menuitem:${menuId}:${menuItem.id}`;
@@ -69,7 +73,8 @@ function loopItems({ parent, menuId, pages, missingPageWarnings, checkDuplicateM
69
73
  menuId,
70
74
  pages,
71
75
  missingPageWarnings,
72
- checkDuplicateMenuItemId
76
+ checkDuplicateMenuItemId,
77
+ context
73
78
  });
74
79
  });
75
80
  parent.links = parent.links.filter((item)=>item.remove !== true);
@@ -85,33 +90,55 @@ function buildMenu({ components, context }) {
85
90
  }
86
91
  const missingPageWarnings = [];
87
92
  const checkDuplicateMenuId = createCheckDuplicateId({
88
- message: 'Duplicate menuId "{{ id }}".'
93
+ message: 'Duplicate menuId "{{ id }}".',
94
+ context
89
95
  });
90
96
  components.menus.forEach((menu)=>{
97
+ const configKey = menu['~k'];
91
98
  if (type.isUndefined(menu.id)) {
92
- throw new Error(`Menu id missing.`);
99
+ throw new Error(formatConfigError({
100
+ message: 'Menu id missing.',
101
+ configKey,
102
+ context
103
+ }));
93
104
  }
94
105
  if (!type.isString(menu.id)) {
95
- throw new Error(`Menu id is not a string. Received ${JSON.stringify(menu.id)}.`);
106
+ throw new Error(formatConfigError({
107
+ message: `Menu id is not a string. Received ${JSON.stringify(menu.id)}.`,
108
+ configKey,
109
+ context
110
+ }));
96
111
  }
97
112
  checkDuplicateMenuId({
98
- id: menu.id
113
+ id: menu.id,
114
+ configKey
99
115
  });
100
116
  menu.menuId = menu.id;
101
117
  menu.id = `menu:${menu.id}`;
102
118
  const checkDuplicateMenuItemId = createCheckDuplicateId({
103
- message: 'Duplicate menuItemId "{{ id }}" on menu "{{ menuId }}".'
119
+ message: 'Duplicate menuItemId "{{ id }}" on menu "{{ menuId }}".',
120
+ context
104
121
  });
105
122
  loopItems({
106
123
  parent: menu,
107
124
  menuId: menu.menuId,
108
125
  pages,
109
126
  missingPageWarnings,
110
- checkDuplicateMenuItemId
127
+ checkDuplicateMenuItemId,
128
+ context
111
129
  });
112
130
  });
113
- missingPageWarnings.map(async (warning)=>{
114
- context.logger.warn(`Page "${warning.pageId}" referenced in menu link "${warning.menuItemId}" not found.`);
131
+ missingPageWarnings.forEach((warning)=>{
132
+ const location = resolveConfigLocation({
133
+ configKey: warning.configKey,
134
+ keyMap: context.keyMap,
135
+ refMap: context.refMap,
136
+ configDirectory: context.directories.config
137
+ });
138
+ const source = location?.source ? `${location.source} at ${location.config}` : '';
139
+ const link = location?.link || '';
140
+ const message = `Page "${warning.pageId}" referenced in menu link "${warning.menuItemId}" not found.`;
141
+ context.logger.warn(`[Config Error] ${message}\n ${source}\n ${link}`);
115
142
  });
116
143
  return components;
117
144
  }