@lowdefy/build 4.7.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.
@@ -12,36 +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
- */ const iconPackages = {
16
- 'react-icons/ai': /"(Ai[A-Z0-9]\w*)"/gm,
17
- 'react-icons/bi': /"(Bi[A-Z0-9]\w*)"/gm,
18
- 'react-icons/bs': /"(Bs[A-Z0-9]\w*)"/gm,
19
- 'react-icons/cg': /"(Cg[A-Z0-9]\w*)"/gm,
20
- 'react-icons/ci': /"(Ci[A-Z0-9]\w*)"/gm,
21
- 'react-icons/di': /"(Di[A-Z0-9]\w*)"/gm,
22
- 'react-icons/fa': /"(Fa[A-Z0-9]\w*)"/gm,
23
- 'react-icons/fc': /"(Fc[A-Z0-9]\w*)"/gm,
24
- 'react-icons/fi': /"(Fi[A-Z0-9]\w*)"/gm,
25
- 'react-icons/gi': /"(Gi[A-Z0-9]\w*)"/gm,
26
- 'react-icons/go': /"(Go[A-Z0-9]\w*)"/gm,
27
- 'react-icons/gr': /"(Gr[A-Z0-9]\w*)"/gm,
28
- 'react-icons/hi': /"(Hi[A-Z0-9]\w*)"/gm,
29
- 'react-icons/im': /"(Im[A-Z0-9]\w*)"/gm,
30
- 'react-icons/io': /"(IoIos[A-Z0-9]\w*)"/gm,
31
- 'react-icons/io5': /"(Io[A-Z0-9]\w*)"/gm,
32
- 'react-icons/lu': /"(Lu[A-Z0-9]\w*)"/gm,
33
- 'react-icons/md': /"(Md[A-Z0-9]\w*)"/gm,
34
- 'react-icons/pi': /"(Pi[A-Z0-9]\w*)"/gm,
35
- 'react-icons/ri': /"(Ri[A-Z0-9]\w*)"/gm,
36
- 'react-icons/rx': /"(Rx[A-Z0-9]\w*)"/gm,
37
- 'react-icons/si': /"(Si[A-Z0-9]\w*)"/gm,
38
- 'react-icons/sl': /"(Sl[A-Z0-9]\w*)"/gm,
39
- 'react-icons/tb': /"(Tb[A-Z0-9]\w*)"/gm,
40
- 'react-icons/tfi': /"(Tfi[A-Z0-9]\w*)"/gm,
41
- 'react-icons/ti': /"(Ti[A-Z0-9]\w*)"/gm,
42
- 'react-icons/vsc': /"(Vsc[A-Z0-9]\w*)"/gm,
43
- 'react-icons/wi': /"(Wi[A-Z0-9]\w*)"/gm
44
- };
15
+ */ import iconPackages from './iconPackages.js';
45
16
  function getConfigIcons({ components, icons, regex }) {
46
17
  [
47
18
  ...JSON.stringify(components.global || {}).matchAll(regex)
@@ -65,7 +36,6 @@ function getBlockDefaultIcons({ blocks, context, icons, regex }) {
65
36
  function buildIconImports({ blocks, components, context, defaults = {} }) {
66
37
  const iconImports = [];
67
38
  Object.entries(iconPackages).forEach(([iconPackage, regex])=>{
68
- defaults;
69
39
  const icons = new Set(defaults[iconPackage]);
70
40
  getConfigIcons({
71
41
  components,
@@ -0,0 +1,49 @@
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
+ */ // Icon name regex patterns keyed by react-icons package.
16
+ // Note: 'react-icons/io' and 'react-icons/io5' overlap — IoIos* icons match
17
+ // both packages. This is intentional and matches the existing buildIconImports
18
+ // behavior where both packages receive the IoIos* icons.
19
+ const iconPackages = {
20
+ 'react-icons/ai': /"(Ai[A-Z0-9]\w*)"/gm,
21
+ 'react-icons/bi': /"(Bi[A-Z0-9]\w*)"/gm,
22
+ 'react-icons/bs': /"(Bs[A-Z0-9]\w*)"/gm,
23
+ 'react-icons/cg': /"(Cg[A-Z0-9]\w*)"/gm,
24
+ 'react-icons/ci': /"(Ci[A-Z0-9]\w*)"/gm,
25
+ 'react-icons/di': /"(Di[A-Z0-9]\w*)"/gm,
26
+ 'react-icons/fa': /"(Fa[A-Z0-9]\w*)"/gm,
27
+ 'react-icons/fc': /"(Fc[A-Z0-9]\w*)"/gm,
28
+ 'react-icons/fi': /"(Fi[A-Z0-9]\w*)"/gm,
29
+ 'react-icons/gi': /"(Gi[A-Z0-9]\w*)"/gm,
30
+ 'react-icons/go': /"(Go[A-Z0-9]\w*)"/gm,
31
+ 'react-icons/gr': /"(Gr[A-Z0-9]\w*)"/gm,
32
+ 'react-icons/hi': /"(Hi[A-Z0-9]\w*)"/gm,
33
+ 'react-icons/im': /"(Im[A-Z0-9]\w*)"/gm,
34
+ 'react-icons/io': /"(IoIos[A-Z0-9]\w*)"/gm,
35
+ 'react-icons/io5': /"(Io[A-Z0-9]\w*)"/gm,
36
+ 'react-icons/lu': /"(Lu[A-Z0-9]\w*)"/gm,
37
+ 'react-icons/md': /"(Md[A-Z0-9]\w*)"/gm,
38
+ 'react-icons/pi': /"(Pi[A-Z0-9]\w*)"/gm,
39
+ 'react-icons/ri': /"(Ri[A-Z0-9]\w*)"/gm,
40
+ 'react-icons/rx': /"(Rx[A-Z0-9]\w*)"/gm,
41
+ 'react-icons/si': /"(Si[A-Z0-9]\w*)"/gm,
42
+ 'react-icons/sl': /"(Sl[A-Z0-9]\w*)"/gm,
43
+ 'react-icons/tb': /"(Tb[A-Z0-9]\w*)"/gm,
44
+ 'react-icons/tfi': /"(Tfi[A-Z0-9]\w*)"/gm,
45
+ 'react-icons/ti': /"(Ti[A-Z0-9]\w*)"/gm,
46
+ 'react-icons/vsc': /"(Vsc[A-Z0-9]\w*)"/gm,
47
+ 'react-icons/wi': /"(Wi[A-Z0-9]\w*)"/gm
48
+ };
49
+ export default iconPackages;
@@ -15,8 +15,10 @@
15
15
  */ import { get } from '@lowdefy/helpers';
16
16
  import getRefPath from './getRefPath.js';
17
17
  import makeId from '../../utils/makeId.js';
18
- function makeRefDefinition(refDefinition, parent, refMap, lineNumber) {
19
- const id = makeId.next();
18
+ function makeRefDefinition(refDefinition, parent, refMap, lineNumber, walkerPath) {
19
+ // Use walker tree path when available for deterministic IDs under parallel
20
+ // resolution. Falls back to counter for root ref and JIT-created refs.
21
+ const id = walkerPath != null ? walkerPath : makeId.next();
20
22
  const refDef = {
21
23
  parent,
22
24
  lineNumber
@@ -208,7 +208,7 @@ function resolveVar(node, ctx) {
208
208
  async function resolveRef(node, ctx) {
209
209
  // 1. Create ref definition
210
210
  const lineNumber = node['~l'];
211
- const refDef = makeRefDefinition(node._ref, ctx.refId, ctx.refMap, lineNumber);
211
+ const refDef = makeRefDefinition(node._ref, ctx.refId, ctx.refMap, lineNumber, ctx.path);
212
212
  // 2. Store unresolved vars before resolution mutates them, and clone so
213
213
  // resolution operates on a copy (preserving original.vars for resolver refs).
214
214
  const varKeys = Object.keys(refDef.vars);
@@ -220,11 +220,11 @@ async function resolveRef(node, ctx) {
220
220
  if (type.isObject(refDef.path)) {
221
221
  refDef.path = await resolve(cloneForResolve(refDef.path), ctx);
222
222
  }
223
- for (const varKey of varKeys){
223
+ await Promise.all(varKeys.map(async (varKey)=>{
224
224
  if (type.isObject(refDef.vars[varKey]) || type.isArray(refDef.vars[varKey])) {
225
225
  refDef.vars[varKey] = await resolve(refDef.vars[varKey], ctx);
226
226
  }
227
- }
227
+ }));
228
228
  if (type.isObject(refDef.key)) {
229
229
  refDef.key = await resolve(cloneForResolve(refDef.key), ctx);
230
230
  }
@@ -307,28 +307,36 @@ async function resolve(node, ctx) {
307
307
  // 4. Object with _var — resolve, then re-walk the result so any
308
308
  // _ref or _build.* operators inside the default value get processed.
309
309
  if (type.isObject(node) && !type.isUndefined(node._var)) {
310
- const varResult = resolveVar(node, ctx);
311
- return resolve(varResult, ctx);
310
+ try {
311
+ const varResult = resolveVar(node, ctx);
312
+ return await resolve(varResult, ctx);
313
+ } catch (error) {
314
+ if (error instanceof ConfigError) {
315
+ ctx.collectError(error);
316
+ return null;
317
+ }
318
+ throw error;
319
+ }
312
320
  }
313
- // 5. Array — walk children in-place
321
+ // 5. Array — walk children in parallel
314
322
  if (type.isArray(node)) {
315
- for(let i = 0; i < node.length; i++){
316
- node[i] = await resolve(node[i], ctx.child(String(i)));
317
- }
323
+ await Promise.all(node.map(async (item, i)=>{
324
+ node[i] = await resolve(item, ctx.child(String(i)));
325
+ }));
318
326
  return node;
319
327
  }
320
- // 6. Object — walk children in-place
328
+ // 6. Object — walk children in parallel
321
329
  const keys = Object.keys(node);
322
- for (const key of keys){
330
+ await Promise.all(keys.map(async (key)=>{
323
331
  if (ctx.shouldStop) {
324
332
  const childPath = ctx.path ? `${ctx.path}.${key}` : key;
325
333
  if (ctx.shouldStop(childPath, ctx.refId)) {
326
334
  delete node[key];
327
- continue;
335
+ return;
328
336
  }
329
337
  }
330
338
  node[key] = await resolve(node[key], ctx.child(key));
331
- }
339
+ }));
332
340
  // Check if this is a _build.* operator
333
341
  if (isBuildOperator(node)) {
334
342
  const result = evaluateBuildOperator(node, ctx);
@@ -33,7 +33,9 @@ import makeRefDefinition from '../buildRefs/makeRefDefinition.js';
33
33
  import { resolve, WalkContext, cloneForResolve, tagRefDeep } from '../buildRefs/walker.js';
34
34
  import validateOperatorsDynamic from '../validateOperatorsDynamic.js';
35
35
  import writeMaps from '../writeMaps.js';
36
+ import detectMissingIcons from './detectMissingIcons.js';
36
37
  import detectMissingPluginPackages from './detectMissingPluginPackages.js';
38
+ import updateIconImportsJit from './updateIconImportsJit.js';
37
39
  import updateServerPackageJsonJit from './updateServerPackageJsonJit.js';
38
40
  import validatePageTypes from './validatePageTypes.js';
39
41
  import writePageJit from './writePageJit.js';
@@ -43,6 +45,20 @@ validateOperatorsDynamic({
43
45
  const dynamicIdentifiers = collectDynamicIdentifiers({
44
46
  operators
45
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
+ }
46
62
  async function buildPageJit({ pageId, pageRegistry, context, directories, logger }) {
47
63
  // Use provided context or create a minimal one for JIT builds
48
64
  const buildContext = context ?? createContext({
@@ -68,7 +84,12 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
68
84
  const pagePath = path.join(buildContext.directories.build, 'pages', pageId, `${pageId}.json`);
69
85
  try {
70
86
  const content = await fs.promises.readFile(pagePath, 'utf8');
71
- 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;
72
93
  } catch (err) {
73
94
  if (err.code !== 'ENOENT') throw err;
74
95
  }
@@ -200,6 +221,13 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
200
221
  ]
201
222
  };
202
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
+ });
203
231
  // Validate link, state, payload, and server-state references
204
232
  const pageIds = Object.keys(pageRegistry);
205
233
  validateLinkReferences({
@@ -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;
@@ -209,6 +209,10 @@ async function shallowBuild(options) {
209
209
  components,
210
210
  context
211
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));
212
216
  await writePageRegistry({
213
217
  pageRegistry,
214
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;
@@ -0,0 +1,20 @@
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
+ */ 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);
19
+ }
20
+ export default writeIconsDynamic;