@lowdefy/build 4.6.0 → 4.7.1

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.
@@ -16,24 +16,49 @@
16
16
  import path from 'path';
17
17
  import { serializer, type } from '@lowdefy/helpers';
18
18
  import { ConfigError, LowdefyInternalError } from '@lowdefy/errors';
19
+ import operators from '@lowdefy/operators-js/operators/build';
19
20
  import addKeys from '../addKeys.js';
20
21
  import buildPage from '../buildPages/buildPage.js';
21
22
  import validateLinkReferences from '../buildPages/validateLinkReferences.js';
22
23
  import validatePayloadReferences from '../buildPages/validatePayloadReferences.js';
23
24
  import validateServerStateReferences from '../buildPages/validateServerStateReferences.js';
24
25
  import validateStateReferences from '../buildPages/validateStateReferences.js';
26
+ import collectDynamicIdentifiers from '../collectDynamicIdentifiers.js';
25
27
  import createCheckDuplicateId from '../../utils/createCheckDuplicateId.js';
26
28
  import createContext from '../../createContext.js';
27
- import createRefReviver from '../buildRefs/createRefReviver.js';
28
- import evaluateBuildOperators from '../buildRefs/evaluateBuildOperators.js';
29
29
  import evaluateStaticOperators from '../buildRefs/evaluateStaticOperators.js';
30
+ import getRefContent from '../buildRefs/getRefContent.js';
30
31
  import jsMapParser from '../buildJs/jsMapParser.js';
31
32
  import makeRefDefinition from '../buildRefs/makeRefDefinition.js';
32
- import recursiveBuild from '../buildRefs/recursiveBuild.js';
33
+ import { resolve, WalkContext, cloneForResolve, tagRefDeep } from '../buildRefs/walker.js';
34
+ import validateOperatorsDynamic from '../validateOperatorsDynamic.js';
35
+ import writeMaps from '../writeMaps.js';
36
+ import detectMissingIcons from './detectMissingIcons.js';
33
37
  import detectMissingPluginPackages from './detectMissingPluginPackages.js';
38
+ import updateIconImportsJit from './updateIconImportsJit.js';
34
39
  import updateServerPackageJsonJit from './updateServerPackageJsonJit.js';
35
40
  import validatePageTypes from './validatePageTypes.js';
36
41
  import writePageJit from './writePageJit.js';
42
+ validateOperatorsDynamic({
43
+ operators
44
+ });
45
+ const dynamicIdentifiers = collectDynamicIdentifiers({
46
+ operators
47
+ });
48
+ async function updateDynamicIcons({ page, context }) {
49
+ if (!context.iconImports) return;
50
+ const missingIcons = detectMissingIcons({
51
+ page,
52
+ iconImports: context.iconImports
53
+ });
54
+ if (missingIcons.length > 0) {
55
+ await updateIconImportsJit({
56
+ newIcons: missingIcons,
57
+ iconImports: context.iconImports,
58
+ context
59
+ });
60
+ }
61
+ }
37
62
  async function buildPageJit({ pageId, pageRegistry, context, directories, logger }) {
38
63
  // Use provided context or create a minimal one for JIT builds
39
64
  const buildContext = context ?? createContext({
@@ -59,7 +84,12 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
59
84
  const pagePath = path.join(buildContext.directories.build, 'pages', pageId, `${pageId}.json`);
60
85
  try {
61
86
  const content = await fs.promises.readFile(pagePath, 'utf8');
62
- return serializer.deserialize(JSON.parse(content));
87
+ const page = serializer.deserialize(JSON.parse(content));
88
+ await updateDynamicIcons({
89
+ page,
90
+ context: buildContext
91
+ });
92
+ return page;
63
93
  } catch (err) {
64
94
  if (err.code !== 'ENOENT') throw err;
65
95
  }
@@ -76,18 +106,20 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
76
106
  let resolvedVars = null;
77
107
  if (unresolvedVars) {
78
108
  const varRefDef = makeRefDefinition({}, null, buildContext.refMap);
79
- resolvedVars = await recursiveBuild({
80
- context: buildContext,
81
- refDef: varRefDef,
82
- count: 0,
83
- content: unresolvedVars,
84
- referencedFrom: pageEntry.refPath ?? pageEntry.resolverOriginal?.resolver
85
- });
86
- resolvedVars = await evaluateBuildOperators({
87
- context: buildContext,
88
- input: resolvedVars,
89
- refDef: varRefDef
109
+ const varCtx = new WalkContext({
110
+ buildContext,
111
+ refId: varRefDef.id,
112
+ sourceRefId: null,
113
+ vars: {},
114
+ path: '',
115
+ currentFile: pageEntry.refPath ?? pageEntry.resolverOriginal?.resolver ?? '',
116
+ refChain: new Set(),
117
+ operators,
118
+ env: process.env,
119
+ dynamicIdentifiers,
120
+ shouldStop: null
90
121
  });
122
+ resolvedVars = await resolve(cloneForResolve(unresolvedVars), varCtx);
91
123
  }
92
124
  let refDef;
93
125
  if (pageEntry.resolverOriginal) {
@@ -105,17 +137,25 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
105
137
  refDef = makeRefDefinition(refDefinition, null, buildContext.refMap);
106
138
  buildContext.refMap[refDef.id].path = refDef.path;
107
139
  }
108
- let processed = await recursiveBuild({
140
+ const pageContent = await getRefContent({
109
141
  context: buildContext,
110
142
  refDef,
111
- count: 0
143
+ referencedFrom: null
112
144
  });
113
- // Top-level operator evaluation (same as buildRefs does after recursiveBuild)
114
- processed = await evaluateBuildOperators({
115
- context: buildContext,
116
- input: processed,
117
- refDef
145
+ const pageCtx = new WalkContext({
146
+ buildContext,
147
+ refId: refDef.id,
148
+ sourceRefId: null,
149
+ vars: refDef.vars ?? {},
150
+ path: '',
151
+ currentFile: refDef.path ?? '',
152
+ refChain: new Set(),
153
+ operators,
154
+ env: process.env,
155
+ dynamicIdentifiers,
156
+ shouldStop: null
118
157
  });
158
+ let processed = await resolve(pageContent, pageCtx);
119
159
  processed = evaluateStaticOperators({
120
160
  context: buildContext,
121
161
  input: processed,
@@ -129,14 +169,9 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
129
169
  throw new ConfigError(`Page "${pageId}" not found in resolved page source file.`);
130
170
  }
131
171
  }
132
- // Stamp root-level content with ~r for correct error file tracing.
133
- // recursiveBuild stamps child _ref content via createRefReviver, but the
134
- // root file's own objects have no parent to do this. Without ~r, addKeys
135
- // can't link objects to their source file and errors fall back to lowdefy.yaml.
136
- const reviver = createRefReviver(refDef.id);
137
- processed = serializer.copy(processed, {
138
- reviver
139
- });
172
+ // Tag all objects with ~r for ref provenance (normally done inside _ref
173
+ // resolution by the walker; JIT resolves the page file directly).
174
+ tagRefDeep(processed, refDef.id);
140
175
  // Apply skeleton-computed auth (buildAuth ran during skeleton build)
141
176
  processed.auth = pageEntry.auth;
142
177
  // Add keys to the resolved page
@@ -144,6 +179,11 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
144
179
  components: processed,
145
180
  context: buildContext
146
181
  });
182
+ // Write keyMap/refMap so the error handler reads JIT entries from disk.
183
+ // JIT addKeys assigns fresh ~k values that aren't in the skeleton keyMap.
184
+ await writeMaps({
185
+ context: buildContext
186
+ });
147
187
  // Initialize linkActionRefs for buildPage (normally done by buildPages)
148
188
  if (!buildContext.linkActionRefs) {
149
189
  buildContext.linkActionRefs = [];
@@ -181,6 +221,13 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
181
221
  ]
182
222
  };
183
223
  }
224
+ // Detect icons in the JIT-resolved page that weren't discovered during skeleton build.
225
+ // Placed after detectMissingPluginPackages so we skip this when packages are being
226
+ // installed (the server restarts and icons will be discovered on the next build).
227
+ await updateDynamicIcons({
228
+ page: processed,
229
+ context: buildContext
230
+ });
184
231
  // Validate link, state, payload, and server-state references
185
232
  const pageIds = Object.keys(pageRegistry);
186
233
  validateLinkReferences({
@@ -66,10 +66,15 @@ function createPageRegistry({ components, context }) {
66
66
  const keyMapEntry = context.keyMap[page['~k']];
67
67
  const refId = keyMapEntry?.['~r'] ?? null;
68
68
  const sourceRef = !type.isNone(refId) ? findPageSourceRef(refId, context.refMap, unresolvedRefVars) : null;
69
+ // Inline pages (defined directly in lowdefy.yaml) have a refId pointing to
70
+ // the root ref but findPageSourceRef returns null because there is no
71
+ // separate source file. Set refId to null so buildPageJit serves them from
72
+ // the pre-built artifact written by buildShallowPages.
73
+ const isInline = !type.isNone(refId) && sourceRef === null && !type.isNone(context.refMap[refId]) && type.isNone(context.refMap[refId].parent);
69
74
  registry.set(page.id, {
70
75
  pageId: page.id,
71
76
  auth: page.auth,
72
- refId,
77
+ refId: isInline ? null : refId,
73
78
  refPath: sourceRef?.path ?? null,
74
79
  unresolvedVars: sourceRef?.unresolvedVars ?? null,
75
80
  resolverOriginal: sourceRef?.original ?? null
@@ -0,0 +1,36 @@
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 iconPackages from '../buildImports/iconPackages.js';
16
+ function detectMissingIcons({ page, iconImports }) {
17
+ const pageJson = JSON.stringify(page);
18
+ const newIcons = [];
19
+ for (const [iconPackage, regex] of Object.entries(iconPackages)){
20
+ const existing = iconImports.find((entry)=>entry.package === iconPackage);
21
+ const existingSet = new Set(existing?.icons ?? []);
22
+ const seen = new Set();
23
+ for (const match of pageJson.matchAll(regex)){
24
+ const iconName = match[1];
25
+ if (!existingSet.has(iconName) && !seen.has(iconName)) {
26
+ seen.add(iconName);
27
+ newIcons.push({
28
+ icon: iconName,
29
+ package: iconPackage
30
+ });
31
+ }
32
+ }
33
+ }
34
+ return newIcons;
35
+ }
36
+ export default detectMissingIcons;
@@ -0,0 +1,54 @@
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 { createRequire } from 'module';
16
+ import path from 'path';
17
+ // Matches the JSON data argument inside GenIcon({...})(props) in react-icons source.
18
+ // react-icons icons are generated functions of the form:
19
+ // function IconName(props) { return GenIcon({...})(props); }
20
+ // Tolerates optional whitespace around GenIcon call and props argument.
21
+ const genIconDataRegex = /GenIcon\s*\(([\s\S]*?)\)\s*\(\s*props\s*\)/;
22
+ function extractIconData({ icons, directories, logger }) {
23
+ const serverRequire = createRequire(path.join(directories.server, 'package.json'));
24
+ const iconDataMap = {};
25
+ const moduleCache = {};
26
+ for (const { icon, package: pkg } of icons){
27
+ if (!moduleCache[pkg]) {
28
+ try {
29
+ moduleCache[pkg] = serverRequire(pkg);
30
+ } catch {
31
+ if (logger) {
32
+ logger.warn(`Could not load icon package "${pkg}" for dynamic icon extraction.`);
33
+ }
34
+ continue;
35
+ }
36
+ }
37
+ const iconFn = moduleCache[pkg][icon];
38
+ if (!iconFn) continue;
39
+ const match = iconFn.toString().match(genIconDataRegex);
40
+ if (match) {
41
+ try {
42
+ iconDataMap[icon] = JSON.parse(match[1]);
43
+ } catch {
44
+ if (logger) {
45
+ logger.warn(`Could not parse icon data for "${icon}" from "${pkg}".`);
46
+ }
47
+ }
48
+ } else if (logger) {
49
+ logger.warn(`Could not extract icon data for "${icon}" from "${pkg}". The icon will show as a fallback.`);
50
+ }
51
+ }
52
+ return iconDataMap;
53
+ }
54
+ export default extractIconData;
@@ -48,7 +48,6 @@ import writePluginImports from '../writePluginImports/writePluginImports.js';
48
48
  import addInstalledTypes from './addInstalledTypes.js';
49
49
  import buildJsShallow from './buildJsShallow.js';
50
50
  import buildShallowPages from './buildShallowPages.js';
51
- import stripPageContent from './stripPageContent.js';
52
51
  import writeSourcelessPages from './writeSourcelessPages.js';
53
52
  async function shallowBuild(options) {
54
53
  makeId.reset();
@@ -73,9 +72,6 @@ async function shallowBuild(options) {
73
72
  components,
74
73
  context
75
74
  });
76
- stripPageContent({
77
- components
78
- });
79
75
  tryBuildStep(testSchema, 'testSchema', {
80
76
  components,
81
77
  context
@@ -201,6 +197,7 @@ async function shallowBuild(options) {
201
197
  context
202
198
  });
203
199
  await context.writeBuildArtifact('jsMap.json', JSON.stringify(context.jsMap));
200
+ await context.writeBuildArtifact('idCounter.json', JSON.stringify(makeId.counter));
204
201
  await context.writeBuildArtifact('customTypesMap.json', JSON.stringify(options.customTypesMap ?? {}));
205
202
  // Persist snapshot of installed packages for JIT missing-package detection.
206
203
  // Written as a build artifact so JIT builds compare against the skeleton
@@ -212,6 +209,10 @@ async function shallowBuild(options) {
212
209
  components,
213
210
  context
214
211
  });
212
+ // Persist icon imports snapshot for JIT icon detection.
213
+ // When buildPageJit resolves a page, it compares discovered icons against
214
+ // this snapshot and regenerates plugins/icons.js if new icons are found.
215
+ await context.writeBuildArtifact('iconImports.json', JSON.stringify(components.imports.icons));
215
216
  await writePageRegistry({
216
217
  pageRegistry,
217
218
  context
@@ -0,0 +1,45 @@
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 extractIconData from './extractIconData.js';
16
+ import writeIconsDynamic from './writeIconsDynamic.js';
17
+ async function updateIconImportsJit({ newIcons, iconImports, context }) {
18
+ for (const { icon, package: pkg } of newIcons){
19
+ let entry = iconImports.find((e)=>e.package === pkg);
20
+ if (!entry) {
21
+ entry = {
22
+ icons: [],
23
+ package: pkg
24
+ };
25
+ iconImports.push(entry);
26
+ }
27
+ // Guard against concurrent JIT builds adding the same icon
28
+ if (!entry.icons.includes(icon)) {
29
+ entry.icons.push(icon);
30
+ }
31
+ }
32
+ await context.writeBuildArtifact('iconImports.json', JSON.stringify(iconImports));
33
+ // Extract SVG tree data from react-icons and write a self-contained JS module
34
+ // that the client can fetch at runtime without a Next.js rebuild.
35
+ const newIconData = extractIconData({
36
+ icons: newIcons,
37
+ directories: context.directories,
38
+ logger: context.logger
39
+ });
40
+ await writeIconsDynamic({
41
+ newIconData,
42
+ context
43
+ });
44
+ }
45
+ export default updateIconImportsJit;
@@ -12,12 +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 PAGE_CONTENT_KEYS from './pageContentKeys.js';
16
- function stripPageContent({ components }) {
17
- for (const page of components.pages ?? []){
18
- for (const key of PAGE_CONTENT_KEYS){
19
- delete page[key];
20
- }
21
- }
15
+ */ async function writeIconsDynamic({ newIconData, context }) {
16
+ Object.assign(context.dynamicIconData, newIconData);
17
+ const content = `export default ${JSON.stringify(context.dynamicIconData)};\n`;
18
+ await context.writeBuildArtifact('plugins/iconsDynamic.js', content);
22
19
  }
23
- export default stripPageContent;
20
+ export default writeIconsDynamic;