@lowdefy/build 4.7.3 → 5.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.
Files changed (56) hide show
  1. package/dist/build/addDefaultPages/404.js +8 -2
  2. package/dist/build/buildApi/buildRoutine/validateStep.js +7 -5
  3. package/dist/build/buildApi/validateEndpoint.js +6 -5
  4. package/dist/build/buildConnections.js +6 -0
  5. package/dist/build/buildImports/buildIconImports.js +5 -1
  6. package/dist/build/buildImports/buildImportsDev.js +1 -6
  7. package/dist/build/buildImports/buildImportsProd.js +1 -6
  8. package/dist/build/buildImports/validateIconImports.js +65 -0
  9. package/dist/build/buildJs/jsMapParser.js +5 -2
  10. package/dist/build/buildPages/buildBlock/buildBlock.js +12 -4
  11. package/dist/build/buildPages/buildBlock/buildEvents.js +34 -1
  12. package/dist/build/buildPages/buildBlock/buildRequests.js +7 -5
  13. package/dist/build/buildPages/buildBlock/buildSubBlocks.js +9 -9
  14. package/dist/build/buildPages/buildBlock/countBlockOperators.js +1 -1
  15. package/dist/build/buildPages/buildBlock/moveAreasToSlots.js +31 -0
  16. package/dist/build/buildPages/buildBlock/{moveSkeletonBlocksToArea.js → moveSkeletonBlocksToSlot.js} +8 -8
  17. package/dist/build/buildPages/buildBlock/{moveSubBlocksToArea.js → moveSubBlocksToSlot.js} +3 -3
  18. package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +124 -0
  19. package/dist/build/buildPages/buildBlock/normalizeLayout.js +68 -0
  20. package/dist/build/buildPages/buildBlock/setBlockId.js +7 -1
  21. package/dist/build/buildPages/buildBlock/validateSlots.js +34 -0
  22. package/dist/build/buildPages/buildPage.js +23 -1
  23. package/dist/build/buildRefs/addLineNumbers.js +76 -0
  24. package/dist/build/{buildImports/buildStyleImports.js → buildRefs/getLineNumber.js} +4 -10
  25. package/dist/build/buildRefs/getRefContent.js +9 -1
  26. package/dist/build/buildRefs/parseRefContent.js +4 -66
  27. package/dist/build/buildTypes.js +4 -2
  28. package/dist/build/cleanBuildDirectory.js +3 -1
  29. package/dist/build/collectPageContent.js +57 -0
  30. package/dist/build/jit/buildPageJit.js +14 -3
  31. package/dist/build/jit/extractIconData.js +16 -1
  32. package/dist/build/jit/pageContentKeys.js +1 -0
  33. package/dist/build/jit/shallowBuild.js +24 -0
  34. package/dist/build/jit/stripPageContent.js +29 -0
  35. package/dist/build/jit/writePageJit.js +9 -1
  36. package/dist/build/testSchema.js +3 -0
  37. package/dist/build/writePluginImports/collectBlockSourceContent.js +65 -0
  38. package/dist/build/writePluginImports/writeActionSchemaMap.js +1 -1
  39. package/dist/build/writePluginImports/writeBlockSchemaMap.js +45 -7
  40. package/dist/build/writePluginImports/writeGlobalsCss.js +126 -0
  41. package/dist/build/writePluginImports/writeOperatorSchemaMap.js +1 -1
  42. package/dist/build/writePluginImports/writePluginImports.js +7 -2
  43. package/dist/build/writeTheme.js +28 -0
  44. package/dist/createContext.js +2 -0
  45. package/dist/defaultTypesMap.js +1693 -837
  46. package/dist/index.js +16 -0
  47. package/dist/lowdefySchema.js +100 -0
  48. package/dist/scripts/generateDefaultTypes.js +5 -10
  49. package/dist/test-utils/runBuild.js +3 -0
  50. package/dist/test-utils/runBuildForSnapshots.js +5 -2
  51. package/dist/test-utils/testContext.js +2 -1
  52. package/dist/utils/createHandleWarning.js +3 -0
  53. package/dist/utils/createPluginTypesMap.js +5 -9
  54. package/dist/utils/validateId.js +24 -0
  55. package/package.json +47 -47
  56. package/dist/build/writePluginImports/writeStyleImports.js +0 -34
@@ -0,0 +1,68 @@
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 { ConfigWarning } from '@lowdefy/errors';
17
+ const DEPRECATED_LAYOUT_KEYS = {
18
+ contentGutter: 'gap',
19
+ contentGap: 'gap',
20
+ contentJustify: 'justify',
21
+ contentDirection: 'direction',
22
+ contentWrap: 'wrap',
23
+ contentOverflow: 'overflow'
24
+ };
25
+ function normalizeLayout(block, pageContext) {
26
+ const layout = block.layout;
27
+ if (type.isNone(layout) || !type.isObject(layout)) return;
28
+ // Warn and rename deprecated content* layout properties
29
+ for (const [oldKey, newKey] of Object.entries(DEPRECATED_LAYOUT_KEYS)){
30
+ if (!type.isNone(layout[oldKey])) {
31
+ pageContext.context.handleWarning(new ConfigWarning(`Block "${block.blockId}": layout.${oldKey} is deprecated. Use layout.${newKey} instead.`, {
32
+ configKey: block['~k'],
33
+ prodError: true
34
+ }));
35
+ if (type.isNone(layout[newKey])) {
36
+ layout[newKey] = layout[oldKey];
37
+ }
38
+ delete layout[oldKey];
39
+ }
40
+ }
41
+ // Warn about gutter in slot/area configs
42
+ for (const slot of Object.values(block.slots ?? {})){
43
+ if (!type.isNone(slot.gutter)) {
44
+ pageContext.context.handleWarning(new ConfigWarning(`Block "${block.blockId}": slots.*.gutter is deprecated. Use gap instead.`, {
45
+ configKey: block['~k'],
46
+ prodError: true
47
+ }));
48
+ if (type.isNone(slot.gap)) {
49
+ slot.gap = slot.gutter;
50
+ }
51
+ delete slot.gutter;
52
+ }
53
+ }
54
+ // Also check areas (before they're moved to slots)
55
+ for (const area of Object.values(block.areas ?? {})){
56
+ if (!type.isNone(area.gutter)) {
57
+ pageContext.context.handleWarning(new ConfigWarning(`Block "${block.blockId}": areas.*.gutter is deprecated. Use gap instead.`, {
58
+ configKey: block['~k'],
59
+ prodError: true
60
+ }));
61
+ if (type.isNone(area.gap)) {
62
+ area.gap = area.gutter;
63
+ }
64
+ delete area.gutter;
65
+ }
66
+ }
67
+ }
68
+ export default normalizeLayout;
@@ -12,8 +12,14 @@
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 setBlockId(block, { pageId, blockIdCounter }) {
15
+ */ import { ConfigError } from '@lowdefy/errors';
16
+ function setBlockId(block, { pageId, blockIdCounter }) {
16
17
  block.blockId = block.id;
18
+ if (block.blockId === pageId && blockIdCounter.getCount(block.blockId) > 0) {
19
+ throw new ConfigError(`Block id "${block.blockId}" on page "${pageId}" collides with the page id. A block cannot have the same id as its page.`, {
20
+ configKey: block['~k']
21
+ });
22
+ }
17
23
  block.id = `block:${pageId}:${block.blockId}:${blockIdCounter.getCount(block.blockId)}`;
18
24
  blockIdCounter.increment(block.blockId);
19
25
  }
@@ -0,0 +1,34 @@
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 { ConfigWarning } from '@lowdefy/errors';
17
+ function validateSlots(block, pageContext) {
18
+ const blockMeta = pageContext.context.blockMetas?.[block.type];
19
+ if (!blockMeta) return;
20
+ if (blockMeta.slots === false) return;
21
+ if (!type.isArray(blockMeta.slots) && !type.isObject(blockMeta.slots)) return;
22
+ if (!type.isObject(block.slots)) return;
23
+ const validSlots = type.isArray(blockMeta.slots) ? new Set(blockMeta.slots) : new Set(Object.keys(blockMeta.slots));
24
+ for (const slotKey of Object.keys(block.slots)){
25
+ if (!validSlots.has(slotKey)) {
26
+ pageContext.context.handleWarning(new ConfigWarning(`Block "${block.blockId}" (${block.type}): Unknown slot "${slotKey}". Valid slots: ${[
27
+ ...validSlots
28
+ ].join(', ')}.`, {
29
+ configKey: block.slots[slotKey]?.['~k'] ?? block['~k']
30
+ }));
31
+ }
32
+ }
33
+ }
34
+ export default validateSlots;
@@ -13,10 +13,11 @@
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 { ConfigError } from '@lowdefy/errors';
16
+ import { ConfigError, ConfigWarning } from '@lowdefy/errors';
17
17
  import buildBlock from './buildBlock/buildBlock.js';
18
18
  import collectExceptions from '../../utils/collectExceptions.js';
19
19
  import createCheckDuplicateId from '../../utils/createCheckDuplicateId.js';
20
+ import validateId from '../../utils/validateId.js';
20
21
  import createCounter from '../../utils/createCounter.js';
21
22
  import validateRequestReferences from './validateRequestReferences.js';
22
23
  function buildPage({ page, index, context, checkDuplicatePageId }) {
@@ -38,6 +39,11 @@ function buildPage({ page, index, context, checkDuplicatePageId }) {
38
39
  failed: true
39
40
  };
40
41
  }
42
+ validateId({
43
+ id: page.id,
44
+ field: 'Page id',
45
+ configKey
46
+ });
41
47
  if (checkDuplicatePageId) {
42
48
  checkDuplicatePageId({
43
49
  id: page.id,
@@ -47,6 +53,7 @@ function buildPage({ page, index, context, checkDuplicatePageId }) {
47
53
  page.pageId = page.id;
48
54
  const requests = [];
49
55
  const requestActionRefs = [];
56
+ const shortcutRefs = [];
50
57
  buildBlock(page, {
51
58
  auth: page.auth,
52
59
  blockIdCounter: createCounter(),
@@ -57,6 +64,7 @@ function buildPage({ page, index, context, checkDuplicatePageId }) {
57
64
  pageId: page.pageId,
58
65
  requests,
59
66
  requestActionRefs,
67
+ shortcutRefs,
60
68
  linkActionRefs: context.linkActionRefs,
61
69
  typeCounters: context.typeCounters
62
70
  });
@@ -69,6 +77,20 @@ function buildPage({ page, index, context, checkDuplicatePageId }) {
69
77
  pageId: page.pageId,
70
78
  context
71
79
  });
80
+ // Warn on duplicate shortcuts within the page
81
+ const seenShortcuts = {};
82
+ shortcutRefs.forEach(({ shortcut, blockId, eventId, configKey })=>{
83
+ if (seenShortcuts[shortcut]) {
84
+ context.handleWarning(new ConfigWarning(`Duplicate shortcut "${shortcut}" on event "${eventId}" on block "${blockId}" on page "${page.pageId}" — already defined on block "${seenShortcuts[shortcut].blockId}".`, {
85
+ configKey
86
+ }));
87
+ } else {
88
+ seenShortcuts[shortcut] = {
89
+ blockId,
90
+ eventId
91
+ };
92
+ }
93
+ });
72
94
  page.requests = requests;
73
95
  }
74
96
  export default buildPage;
@@ -0,0 +1,76 @@
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
+ */ /* eslint-disable no-param-reassign */ import { isMap, isSeq, isPair, isScalar } from 'yaml';
16
+ import setNonEnumerableProperty from '../../utils/setNonEnumerableProperty.js';
17
+ import getLineNumber from './getLineNumber.js';
18
+ function addLineNumbers(node, content, result) {
19
+ if (isMap(node)) {
20
+ const obj = result || {};
21
+ if (node.range) {
22
+ setNonEnumerableProperty(obj, '~l', getLineNumber(content, node.range[0]));
23
+ }
24
+ for (const pair of node.items){
25
+ if (isPair(pair) && isScalar(pair.key)) {
26
+ const key = pair.key.value;
27
+ const value = pair.value;
28
+ // Use key's line number for the value's ~l (more useful for error messages)
29
+ const keyLineNumber = pair.key.range ? getLineNumber(content, pair.key.range[0]) : null;
30
+ if (isMap(value)) {
31
+ const mapResult = addLineNumbers(value, content, {});
32
+ // Override ~l with key's line number if available
33
+ if (keyLineNumber) {
34
+ setNonEnumerableProperty(mapResult, '~l', keyLineNumber);
35
+ }
36
+ obj[key] = mapResult;
37
+ } else if (isSeq(value)) {
38
+ const arrResult = addLineNumbers(value, content, []);
39
+ // Override ~l with key's line number if available
40
+ if (keyLineNumber) {
41
+ setNonEnumerableProperty(arrResult, '~l', keyLineNumber);
42
+ }
43
+ obj[key] = arrResult;
44
+ } else if (isScalar(value)) {
45
+ obj[key] = value.value;
46
+ } else {
47
+ obj[key] = value?.toJSON?.() ?? value;
48
+ }
49
+ }
50
+ }
51
+ return obj;
52
+ }
53
+ if (isSeq(node)) {
54
+ const arr = result || [];
55
+ if (node.range) {
56
+ setNonEnumerableProperty(arr, '~l', getLineNumber(content, node.range[0]));
57
+ }
58
+ for (const item of node.items){
59
+ if (isMap(item)) {
60
+ arr.push(addLineNumbers(item, content, {}));
61
+ } else if (isSeq(item)) {
62
+ arr.push(addLineNumbers(item, content, []));
63
+ } else if (isScalar(item)) {
64
+ arr.push(item.value);
65
+ } else {
66
+ arr.push(item?.toJSON?.() ?? item);
67
+ }
68
+ }
69
+ return arr;
70
+ }
71
+ if (isScalar(node)) {
72
+ return node.value;
73
+ }
74
+ return node?.toJSON?.() ?? node;
75
+ }
76
+ export default addLineNumbers;
@@ -12,14 +12,8 @@
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 buildStyleImports({ blocks, context }) {
16
- const styles = new Set();
17
- blocks.forEach((block)=>{
18
- styles.add(...(context.typesMap.styles.packages[block.package] || []).map((style)=>`${block.package}/${style}`));
19
- styles.add(...(context.typesMap.styles.blocks[block.typeName] || []).map((style)=>`${block.package}/${style}`));
20
- });
21
- return [
22
- ...styles
23
- ].filter((style)=>!!style);
15
+ */ function getLineNumber(content, offset) {
16
+ if (offset == null || offset < 0) return null;
17
+ return content.substring(0, offset).split('\n').length;
24
18
  }
25
- export default buildStyleImports;
19
+ export default getLineNumber;
@@ -12,7 +12,10 @@
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 getConfigFile from './getConfigFile.js';
15
+ */ import { type } from '@lowdefy/helpers';
16
+ import { getFileExtension } from '@lowdefy/node-utils';
17
+ import getConfigFile from './getConfigFile.js';
18
+ import getUserJavascriptFunction from './getUserJavascriptFunction.js';
16
19
  import parseRefContent from './parseRefContent.js';
17
20
  import runRefResolver from './runRefResolver.js';
18
21
  async function getRefContent({ context, refDef, referencedFrom }) {
@@ -29,6 +32,11 @@ async function getRefContent({ context, refDef, referencedFrom }) {
29
32
  refDef,
30
33
  referencedFrom
31
34
  });
35
+ } else if (type.isString(refDef.path) && getFileExtension(refDef.path) === 'js') {
36
+ return getUserJavascriptFunction({
37
+ context,
38
+ filePath: refDef.path
39
+ });
32
40
  } else {
33
41
  content = await getConfigFile({
34
42
  context,
@@ -12,75 +12,13 @@
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
- */ /* eslint-disable no-param-reassign */ import { ConfigError } from '@lowdefy/errors';
15
+ */ import { ConfigError } from '@lowdefy/errors';
16
16
  import { type } from '@lowdefy/helpers';
17
17
  import { getFileExtension, getFileSubExtension } from '@lowdefy/node-utils';
18
18
  import JSON5 from 'json5';
19
- import YAML, { isMap, isSeq, isPair, isScalar } from 'yaml';
19
+ import YAML from 'yaml';
20
+ import addLineNumbers from './addLineNumbers.js';
20
21
  import parseNunjucks from './parseNunjucks.js';
21
- import setNonEnumerableProperty from '../../utils/setNonEnumerableProperty.js';
22
- function getLineNumber(content, offset) {
23
- if (offset == null || offset < 0) return null;
24
- return content.substring(0, offset).split('\n').length;
25
- }
26
- function addLineNumbers(node, content, result) {
27
- if (isMap(node)) {
28
- const obj = result || {};
29
- if (node.range) {
30
- setNonEnumerableProperty(obj, '~l', getLineNumber(content, node.range[0]));
31
- }
32
- for (const pair of node.items){
33
- if (isPair(pair) && isScalar(pair.key)) {
34
- const key = pair.key.value;
35
- const value = pair.value;
36
- // Use key's line number for the value's ~l (more useful for error messages)
37
- const keyLineNumber = pair.key.range ? getLineNumber(content, pair.key.range[0]) : null;
38
- if (isMap(value)) {
39
- const mapResult = addLineNumbers(value, content, {});
40
- // Override ~l with key's line number if available
41
- if (keyLineNumber) {
42
- setNonEnumerableProperty(mapResult, '~l', keyLineNumber);
43
- }
44
- obj[key] = mapResult;
45
- } else if (isSeq(value)) {
46
- const arrResult = addLineNumbers(value, content, []);
47
- // Override ~l with key's line number if available
48
- if (keyLineNumber) {
49
- setNonEnumerableProperty(arrResult, '~l', keyLineNumber);
50
- }
51
- obj[key] = arrResult;
52
- } else if (isScalar(value)) {
53
- obj[key] = value.value;
54
- } else {
55
- obj[key] = value?.toJSON?.() ?? value;
56
- }
57
- }
58
- }
59
- return obj;
60
- }
61
- if (isSeq(node)) {
62
- const arr = result || [];
63
- if (node.range) {
64
- setNonEnumerableProperty(arr, '~l', getLineNumber(content, node.range[0]));
65
- }
66
- for (const item of node.items){
67
- if (isMap(item)) {
68
- arr.push(addLineNumbers(item, content, {}));
69
- } else if (isSeq(item)) {
70
- arr.push(addLineNumbers(item, content, []));
71
- } else if (isScalar(item)) {
72
- arr.push(item.value);
73
- } else {
74
- arr.push(item?.toJSON?.() ?? item);
75
- }
76
- }
77
- return arr;
78
- }
79
- if (isScalar(node)) {
80
- return node.value;
81
- }
82
- return node?.toJSON?.() ?? node;
83
- }
84
22
  function parseYamlWithLineNumbers(content) {
85
23
  const doc = YAML.parseDocument(content);
86
24
  if (doc.errors && doc.errors.length > 0) {
@@ -88,7 +26,7 @@ function parseYamlWithLineNumbers(content) {
88
26
  }
89
27
  return addLineNumbers(doc.contents, content);
90
28
  }
91
- function parseRefContent({ content, refDef }) {
29
+ async function parseRefContent({ content, refDef }) {
92
30
  const { path, vars } = refDef;
93
31
  if (type.isString(path)) {
94
32
  let ext = getFileExtension(path);
@@ -12,9 +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 basicTypes from '@lowdefy/blocks-basic/types';
15
+ */ import { ConfigError, ConfigWarning } from '@lowdefy/errors';
16
+ import basicTypes from '@lowdefy/blocks-basic/types';
16
17
  import loaderTypes from '@lowdefy/blocks-loaders/types';
17
- import { ConfigError, ConfigWarning } from '@lowdefy/errors';
18
18
  import findSimilarString from '../utils/findSimilarString.js';
19
19
  function buildTypeClass(context, { counter, definitions, store, typeClass, warnIfMissing = false }) {
20
20
  const counts = counter.getCounts();
@@ -61,6 +61,8 @@ function buildTypes({ components, context }) {
61
61
  loaderTypes.blocks.forEach((block)=>typeCounters.blocks.increment(block));
62
62
  // Used for DisplayMessage in @lowdefy/client
63
63
  typeCounters.blocks.increment('Message');
64
+ // Used by blocks-antd Header/PageHeaderMenu/PageSiderMenu darkModeToggle
65
+ typeCounters.actions.increment('SetDarkMode');
64
66
  components.types = {
65
67
  actions: {},
66
68
  auth: {
@@ -12,8 +12,10 @@
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 { cleanDirectory } from '@lowdefy/node-utils';
15
+ */ import path from 'path';
16
+ import { cleanDirectory } from '@lowdefy/node-utils';
16
17
  async function cleanBuildDirectory({ context }) {
17
18
  await cleanDirectory(context.directories.build);
19
+ await cleanDirectory(path.join(context.directories.server, 'lowdefy-build', 'tailwind'));
18
20
  }
19
21
  export default cleanBuildDirectory;
@@ -0,0 +1,57 @@
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
+ */ function walkValue(value, strings) {
16
+ if (typeof value === 'string') {
17
+ strings.push(value);
18
+ return;
19
+ }
20
+ if (Array.isArray(value)) {
21
+ for (const item of value){
22
+ walkValue(item, strings);
23
+ }
24
+ return;
25
+ }
26
+ if (value && typeof value === 'object') {
27
+ for (const v of Object.values(value)){
28
+ walkValue(v, strings);
29
+ }
30
+ }
31
+ }
32
+ function walkBlockProperties(blocks, strings) {
33
+ if (!Array.isArray(blocks)) return;
34
+ for (const block of blocks){
35
+ if (!block) continue;
36
+ if (block.class) {
37
+ walkValue(block.class, strings);
38
+ }
39
+ if (block.properties) {
40
+ walkValue(block.properties, strings);
41
+ }
42
+ walkBlockProperties(block.blocks, strings);
43
+ for (const area of Object.values(block.areas ?? {})){
44
+ walkBlockProperties(area.blocks, strings);
45
+ }
46
+ for (const slot of Object.values(block.slots ?? {})){
47
+ walkBlockProperties(slot.blocks, strings);
48
+ }
49
+ }
50
+ }
51
+ function collectPageContent(pages) {
52
+ const strings = [];
53
+ walkBlockProperties(pages, strings);
54
+ return strings.join('\n');
55
+ }
56
+ export default collectPageContent;
57
+ export { walkBlockProperties };
@@ -70,11 +70,13 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
70
70
  if (!pageEntry) {
71
71
  return null;
72
72
  }
73
- // Reset errors for this build. Keep a local reference so that concurrent
74
- // JIT builds (different pages sharing buildContext) cannot corrupt our
75
- // error list by reassigning buildContext.errors during an await.
73
+ // Reset errors and warnings for this build. Keep local references so that
74
+ // concurrent JIT builds (different pages sharing buildContext) cannot corrupt
75
+ // our lists by reassigning during an await.
76
76
  const buildErrors = [];
77
+ const buildWarnings = [];
77
78
  buildContext.errors = buildErrors;
79
+ buildContext.warnings = buildWarnings;
78
80
  try {
79
81
  // Pages without a source file (e.g., default 404) can only be served from
80
82
  // their pre-built artifact — they have no YAML to re-resolve from.
@@ -277,6 +279,15 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
277
279
  page: finalPage,
278
280
  context: buildContext
279
281
  });
282
+ // Attach warnings after disk write so they don't persist in artifacts
283
+ if (buildWarnings.length > 0) {
284
+ finalPage._warnings = buildWarnings.map((w)=>({
285
+ type: w.name ?? 'ConfigWarning',
286
+ message: w.message,
287
+ source: w.source ?? null,
288
+ stack: w.stack ?? null
289
+ }));
290
+ }
280
291
  return finalPage;
281
292
  } catch (err) {
282
293
  // Attach any collected errors to the thrown error
@@ -14,6 +14,7 @@
14
14
  limitations under the License.
15
15
  */ import { createRequire } from 'module';
16
16
  import path from 'path';
17
+ import findSimilarString from '../../utils/findSimilarString.js';
17
18
  // Matches the JSON data argument inside GenIcon({...})(props) in react-icons source.
18
19
  // react-icons icons are generated functions of the form:
19
20
  // function IconName(props) { return GenIcon({...})(props); }
@@ -35,7 +36,21 @@ function extractIconData({ icons, directories, logger }) {
35
36
  }
36
37
  }
37
38
  const iconFn = moduleCache[pkg][icon];
38
- if (!iconFn) continue;
39
+ if (!iconFn) {
40
+ if (logger) {
41
+ let message = `Icon "${icon}" not found in "${pkg}".`;
42
+ const suggestion = findSimilarString({
43
+ input: icon,
44
+ candidates: Object.keys(moduleCache[pkg]),
45
+ maxDistance: Math.max(3, Math.ceil(icon.length * 0.4))
46
+ });
47
+ if (suggestion) {
48
+ message += ` Did you mean "${suggestion}"?`;
49
+ }
50
+ logger.warn(message);
51
+ }
52
+ continue;
53
+ }
39
54
  const match = iconFn.toString().match(genIconDataRegex);
40
55
  if (match) {
41
56
  try {
@@ -19,6 +19,7 @@
19
19
  const PAGE_CONTENT_KEYS = [
20
20
  'blocks',
21
21
  'areas',
22
+ 'slots',
22
23
  'events',
23
24
  'requests',
24
25
  'layout'
@@ -40,6 +40,7 @@ import writeApi from '../writeApi.js';
40
40
  import writeGlobal from '../writeGlobal.js';
41
41
  import writeJs from '../buildJs/writeJs.js';
42
42
  import writeLogger from '../writeLogger.js';
43
+ import writeTheme from '../writeTheme.js';
43
44
  import writeMaps from '../writeMaps.js';
44
45
  import updateServerPackageJson from '../full/updateServerPackageJson.js';
45
46
  import writeMenus from '../writeMenus.js';
@@ -48,7 +49,9 @@ import writePluginImports from '../writePluginImports/writePluginImports.js';
48
49
  import addInstalledTypes from './addInstalledTypes.js';
49
50
  import buildJsShallow from './buildJsShallow.js';
50
51
  import buildShallowPages from './buildShallowPages.js';
52
+ import collectPageContent from '../collectPageContent.js';
51
53
  import collectSkeletonSourceFiles from './collectSkeletonSourceFiles.js';
54
+ import stripPageContent from './stripPageContent.js';
52
55
  import writeSourcelessPages from './writeSourcelessPages.js';
53
56
  async function shallowBuild(options) {
54
57
  makeId.reset();
@@ -68,6 +71,10 @@ async function shallowBuild(options) {
68
71
  }
69
72
  throw err;
70
73
  }
74
+ // Stop early if buildRefs collected errors (e.g., YAML parse errors).
75
+ // Failed _ref resolutions leave null entries in arrays — logging now
76
+ // surfaces the real error before downstream code crashes on nulls.
77
+ logCollectedErrors(context);
71
78
  // Collect skeleton source files while ~r markers still exist on objects.
72
79
  const skeletonSourceFiles = collectSkeletonSourceFiles({
73
80
  components,
@@ -78,6 +85,19 @@ async function shallowBuild(options) {
78
85
  components,
79
86
  context
80
87
  });
88
+ context.tailwindContentMap = new Map();
89
+ for (const page of components.pages ?? []){
90
+ const content = collectPageContent([
91
+ page
92
+ ]);
93
+ if (content) {
94
+ context.tailwindContentMap.set(page.id, content);
95
+ }
96
+ }
97
+ stripPageContent({
98
+ components,
99
+ context
100
+ });
81
101
  tryBuildStep(testSchema, 'testSchema', {
82
102
  components,
83
103
  context
@@ -185,6 +205,10 @@ async function shallowBuild(options) {
185
205
  components,
186
206
  context
187
207
  });
208
+ await writeTheme({
209
+ components,
210
+ context
211
+ });
188
212
  await writeLogger({
189
213
  components,
190
214
  context
@@ -0,0 +1,29 @@
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 PAGE_CONTENT_KEYS from './pageContentKeys.js';
17
+ function stripPageContent({ components, context }) {
18
+ for (const page of components.pages ?? []){
19
+ // Only strip pages that have a source ref (will be JIT-rebuilt).
20
+ // Inline pages (no ~r) must keep their content for buildShallowPages.
21
+ const keyMapEntry = context.keyMap[page['~k']];
22
+ const refId = keyMapEntry?.['~r'] ?? null;
23
+ if (type.isNone(refId)) continue;
24
+ for (const key of PAGE_CONTENT_KEYS){
25
+ delete page[key];
26
+ }
27
+ }
28
+ }
29
+ export default stripPageContent;
@@ -12,7 +12,10 @@
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 { serializer, type } from '@lowdefy/helpers';
15
+ */ import path from 'path';
16
+ import { serializer, type } from '@lowdefy/helpers';
17
+ import { writeFile } from '@lowdefy/node-utils';
18
+ import collectPageContent from '../collectPageContent.js';
16
19
  import writeJs from '../buildJs/writeJs.js';
17
20
  async function writePageJit({ page, context }) {
18
21
  // Write page JSON
@@ -40,5 +43,10 @@ async function writePageJit({ page, context }) {
40
43
  await writeJs({
41
44
  context
42
45
  });
46
+ // Write per-page content file for Tailwind to scan class and property strings
47
+ const pageContent = collectPageContent([
48
+ page
49
+ ]);
50
+ await writeFile(path.join(context.directories.server, 'lowdefy-build', 'tailwind', `${page.pageId}.html`), '<!-- Generated by Lowdefy build -->\n' + (pageContent ?? ''));
43
51
  }
44
52
  export default writePageJit;