@lowdefy/build 5.5.1 → 6.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 (107) hide show
  1. package/dist/build/buildAgents.js +70 -17
  2. package/dist/build/buildApi/buildApi.js +4 -1
  3. package/dist/build/buildApi/buildEndpoint.js +8 -2
  4. package/dist/build/buildApi/buildRoutine/countStepTypes.js +1 -1
  5. package/dist/build/buildApi/buildRoutine/setStepId.js +2 -0
  6. package/dist/build/buildApi/buildRoutine/validateStep.js +61 -0
  7. package/dist/build/buildApi/resolveEndpointSchedules.js +37 -0
  8. package/dist/build/buildApi/validateEndpoint.js +96 -1
  9. package/dist/build/buildApp.js +13 -20
  10. package/dist/build/buildAppMeta.js +130 -0
  11. package/dist/build/buildAuth/buildAgentAuth.js +69 -0
  12. package/dist/build/buildAuth/buildAuth.js +14 -0
  13. package/dist/build/buildAuth/buildAuthPlugins.js +6 -0
  14. package/dist/build/buildAuth/buildAuthStrategies.js +117 -0
  15. package/dist/build/buildAuth/buildWebsocketAuth.js +55 -0
  16. package/dist/build/buildAuth/getAgentRoles.js +39 -0
  17. package/dist/build/buildAuth/getProtectedAgents.js +29 -0
  18. package/dist/build/buildAuth/getProtectedWebsockets.js +29 -0
  19. package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
  20. package/dist/build/buildAuth/rejectReservedEntityIds.js +52 -0
  21. package/dist/build/buildAuth/validateAuthConfig.js +15 -3
  22. package/dist/build/buildImports/buildImportsDev.js +15 -0
  23. package/dist/build/buildImports/buildImportsProd.js +4 -1
  24. package/dist/build/buildJs/generateClientJsModule.js +27 -0
  25. package/dist/build/buildJs/writeJs.js +3 -5
  26. package/dist/build/buildMcp.js +73 -0
  27. package/dist/build/buildModuleDefs.js +90 -48
  28. package/dist/build/buildModules.js +61 -14
  29. package/dist/build/buildNotifications.js +98 -0
  30. package/dist/build/buildPages/buildBlock/buildBlock.js +3 -1
  31. package/dist/build/buildPages/buildBlock/buildDynamicBlock.js +131 -0
  32. package/dist/build/buildPages/buildBlock/buildEvents.js +197 -22
  33. package/dist/build/buildPages/buildBlock/buildRequests.js +9 -0
  34. package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +2 -11
  35. package/dist/build/buildPages/buildBlock/setBlockId.js +5 -2
  36. package/dist/build/buildPages/buildBlock/validateBlock.js +19 -1
  37. package/dist/build/buildPages/buildPage.js +27 -3
  38. package/dist/build/buildPages/buildSubscriptions.js +145 -0
  39. package/dist/build/buildPages/validateDynamicBlockRefs.js +30 -0
  40. package/dist/build/buildPages/validateStateReferences.js +4 -2
  41. package/dist/build/buildPages/validateWebsocketRefs.js +30 -0
  42. package/dist/build/buildRefs/buildRefs.js +21 -12
  43. package/dist/build/buildRefs/cloneWithMarkers.js +57 -0
  44. package/dist/build/buildRefs/deferredRegions.js +80 -0
  45. package/dist/build/buildRefs/deferredRegistry.js +292 -0
  46. package/dist/build/buildRefs/getKey.js +16 -5
  47. package/dist/build/buildRefs/getModuleRefContent.js +21 -6
  48. package/dist/build/buildRefs/getUserJavascriptFunction.js +1 -1
  49. package/dist/build/buildRefs/parseRefContent.js +24 -0
  50. package/dist/build/buildRefs/{evaluateStaticOperators.js → precomputeRuntimeOperators.js} +7 -6
  51. package/dist/build/buildRefs/walker.js +490 -209
  52. package/dist/build/buildTypes.js +22 -1
  53. package/dist/build/buildWebsockets.js +99 -0
  54. package/dist/build/collectPageContent.js +7 -1
  55. package/dist/build/copyAgentFileSystems.js +2 -2
  56. package/dist/build/fetchModules.js +18 -4
  57. package/dist/build/full/buildPages.js +16 -0
  58. package/dist/build/full/updateServerPackageJson.js +21 -0
  59. package/dist/build/jit/buildPageJit.js +46 -6
  60. package/dist/build/jit/collectSkeletonSourceFiles.js +14 -0
  61. package/dist/build/jit/pageContentKeys.js +1 -0
  62. package/dist/build/jit/shallowBuild.js +81 -1
  63. package/dist/build/jit/writePageJit.js +11 -8
  64. package/dist/build/registerModules.js +149 -42
  65. package/dist/build/validateCallAgentSteps.js +58 -0
  66. package/dist/build/validateConfig.js +4 -0
  67. package/dist/build/validateCronConfig.js +110 -0
  68. package/dist/build/validateRenderNotificationSteps.js +50 -0
  69. package/dist/build/writeApi.js +58 -1
  70. package/dist/build/writeI18n.js +8 -1
  71. package/dist/build/writeMcp.js +21 -0
  72. package/dist/build/writeNotifications.js +30 -0
  73. package/dist/build/writePluginImports/importPluginModule.js +39 -0
  74. package/dist/build/writePluginImports/writeActionSchemaMap.js +6 -7
  75. package/dist/build/writePluginImports/writeAuthImports.js +4 -0
  76. package/dist/build/writePluginImports/writeAvailableTypes.js +99 -0
  77. package/dist/build/writePluginImports/writeBlockSchemaMap.js +10 -9
  78. package/dist/build/writePluginImports/writeConnectionSchemaMap.js +101 -0
  79. package/dist/build/writePluginImports/writeGlobalsCss.js +20 -11
  80. package/dist/build/writePluginImports/writeNotificationImports.js +31 -0
  81. package/dist/build/writePluginImports/writeOperatorSchemaMap.js +6 -7
  82. package/dist/build/writePluginImports/writePluginImports.js +18 -4
  83. package/dist/build/writePluginImports/writeWebsocketImports.js +22 -0
  84. package/dist/build/writeWebsockets.js +30 -0
  85. package/dist/createContext.js +6 -1
  86. package/dist/defaultPackages.js +7 -1
  87. package/dist/defaultTypesMap.js +689 -386
  88. package/dist/dynamic/buildDynamicBlocks.js +120 -0
  89. package/dist/index.js +78 -8
  90. package/dist/indexDev.js +4 -0
  91. package/dist/lowdefySchema.js +861 -6
  92. package/dist/scripts/generateDefaultTypes.js +5 -2
  93. package/dist/test-utils/expectTerminates.js +33 -0
  94. package/dist/test-utils/runBuild.js +20 -1
  95. package/dist/test-utils/runBuildForSnapshots.js +34 -4
  96. package/dist/test-utils/testContext.js +8 -2
  97. package/dist/utils/countOperators.js +5 -17
  98. package/dist/utils/createPluginTypesMap.js +21 -0
  99. package/dist/utils/getCronEnvironmentNames.js +25 -0
  100. package/dist/utils/logCollectedErrors.js +44 -10
  101. package/dist/utils/serializeBuildException.js +29 -0
  102. package/dist/utils/validateCronExpression.js +131 -0
  103. package/dist/utils/validateId.js +11 -0
  104. package/dist/utils/writeBuildArtifact.js +5 -2
  105. package/package.json +66 -59
  106. package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
  107. /package/dist/build/{resolveModuleOperators.js → buildRefs/scopeMenuItemIds.js} +0 -0
@@ -26,17 +26,20 @@ async function generateDefaultTypesMap() {
26
26
  adapters: {},
27
27
  callbacks: {},
28
28
  events: {},
29
- providers: {}
29
+ providers: {},
30
+ strategies: {}
30
31
  },
31
32
  blockMetas: {},
32
33
  blocks: {},
33
34
  connections: {},
34
35
  icons: {},
36
+ notifications: {},
35
37
  operators: {
36
38
  client: {},
37
39
  server: {}
38
40
  },
39
- requests: {}
41
+ requests: {},
42
+ websockets: {}
40
43
  };
41
44
  for (const packageName of defaultPackages){
42
45
  const { default: types } = await import(`${packageName}/types`);
@@ -0,0 +1,33 @@
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
+ */ // Termination guard for cycle-sensitive tests: a cycle-detection regression
16
+ // that deadlocks should fail fast with an actionable message, not an opaque
17
+ // CI timeout. Passes the promise's settlement (value or rejection) through
18
+ // unchanged when it settles in time.
19
+ async function expectTerminates(promise, ms, hint) {
20
+ let timer;
21
+ const timeout = new Promise((_, reject)=>{
22
+ timer = setTimeout(()=>reject(new Error(`promise did not settle within ${ms}ms — ${hint}`)), ms);
23
+ });
24
+ try {
25
+ return await Promise.race([
26
+ promise,
27
+ timeout
28
+ ]);
29
+ } finally{
30
+ clearTimeout(timer);
31
+ }
32
+ }
33
+ export default expectTerminates;
@@ -57,6 +57,9 @@ import createTestLogger from './createTestLogger.js';
57
57
  DangerousHtml: {
58
58
  package: '@lowdefy/blocks-basic'
59
59
  },
60
+ Dynamic: {
61
+ package: '@lowdefy/blocks-basic'
62
+ },
60
63
  Html: {
61
64
  package: '@lowdefy/blocks-basic'
62
65
  },
@@ -112,7 +115,15 @@ import createTestLogger from './createTestLogger.js';
112
115
  package: '@lowdefy/blocks-basic'
113
116
  }
114
117
  },
118
+ agents: {
119
+ ClaudeAgent: {
120
+ package: '@lowdefy/connection-anthropic'
121
+ }
122
+ },
115
123
  connections: {
124
+ Anthropic: {
125
+ package: '@lowdefy/connection-anthropic'
126
+ },
116
127
  AxiosHttp: {
117
128
  package: '@lowdefy/connection-axios-http'
118
129
  },
@@ -134,7 +145,15 @@ import createTestLogger from './createTestLogger.js';
134
145
  events: {},
135
146
  providers: {
136
147
  GoogleProvider: {
137
- package: 'next-auth'
148
+ package: '@lowdefy/plugin-next-auth'
149
+ }
150
+ },
151
+ strategies: {
152
+ apiKey: {
153
+ package: '@lowdefy/plugin-next-auth'
154
+ },
155
+ jwt: {
156
+ package: '@lowdefy/plugin-next-auth'
138
157
  }
139
158
  }
140
159
  },
@@ -90,6 +90,9 @@ import path from 'path';
90
90
  DangerousHtml: {
91
91
  package: '@lowdefy/blocks-basic'
92
92
  },
93
+ Dynamic: {
94
+ package: '@lowdefy/blocks-basic'
95
+ },
93
96
  Html: {
94
97
  package: '@lowdefy/blocks-basic'
95
98
  },
@@ -280,6 +283,9 @@ import path from 'path';
280
283
  }
281
284
  },
282
285
  connections: {
286
+ Anthropic: {
287
+ package: '@lowdefy/connection-anthropic'
288
+ },
283
289
  AxiosHttp: {
284
290
  package: '@lowdefy/connection-axios-http'
285
291
  },
@@ -312,6 +318,12 @@ import path from 'path';
312
318
  AxiosHttp: {
313
319
  package: '@lowdefy/connection-axios-http'
314
320
  },
321
+ GenerateObject: {
322
+ package: '@lowdefy/connection-anthropic'
323
+ },
324
+ GenerateText: {
325
+ package: '@lowdefy/connection-anthropic'
326
+ },
315
327
  MongoDBFind: {
316
328
  package: '@lowdefy/connection-mongodb'
317
329
  },
@@ -370,6 +382,16 @@ import path from 'path';
370
382
  package: '@lowdefy/plugin-aws'
371
383
  }
372
384
  },
385
+ agents: {
386
+ ClaudeAgent: {
387
+ package: '@lowdefy/connection-anthropic'
388
+ }
389
+ },
390
+ notifications: {
391
+ NotificationEmail: {
392
+ package: '@lowdefy/email-templates'
393
+ }
394
+ },
373
395
  auth: {
374
396
  adapters: {
375
397
  MongoDBAdapter: {
@@ -415,16 +437,24 @@ import path from 'path';
415
437
  package: '@lowdefy/plugin-auth0'
416
438
  },
417
439
  GoogleProvider: {
418
- package: 'next-auth'
440
+ package: '@lowdefy/plugin-next-auth'
419
441
  },
420
442
  GitHubProvider: {
421
- package: 'next-auth'
443
+ package: '@lowdefy/plugin-next-auth'
422
444
  },
423
445
  CredentialsProvider: {
424
- package: 'next-auth'
446
+ package: '@lowdefy/plugin-next-auth'
425
447
  },
426
448
  EmailProvider: {
427
- package: 'next-auth'
449
+ package: '@lowdefy/plugin-next-auth'
450
+ }
451
+ },
452
+ strategies: {
453
+ apiKey: {
454
+ package: '@lowdefy/plugin-next-auth'
455
+ },
456
+ jwt: {
457
+ package: '@lowdefy/plugin-next-auth'
428
458
  }
429
459
  }
430
460
  },
@@ -34,11 +34,13 @@ function testContext({ writeBuildArtifact, configDirectory, readConfigFile, logg
34
34
  adapters: createCounter(),
35
35
  callbacks: createCounter(),
36
36
  events: createCounter(),
37
- providers: createCounter()
37
+ providers: createCounter(),
38
+ strategies: createCounter()
38
39
  },
39
40
  blocks: createCounter(),
40
41
  connections: createCounter(),
41
42
  requests: createCounter(),
43
+ websockets: createCounter(),
42
44
  controls: createCounter(),
43
45
  operators: {
44
46
  client: createCounter(),
@@ -51,7 +53,8 @@ function testContext({ writeBuildArtifact, configDirectory, readConfigFile, logg
51
53
  keyMap: {},
52
54
  jsMap: {},
53
55
  agentIds: new Set(),
54
- connectionIds: new Set()
56
+ connectionIds: new Set(),
57
+ websocketIds: new Set()
55
58
  };
56
59
  context.logger = {
57
60
  ...defaultLogger,
@@ -66,6 +69,9 @@ function testContext({ writeBuildArtifact, configDirectory, readConfigFile, logg
66
69
  };
67
70
  // handleError delegates to logger.error
68
71
  context.handleError = context.logger.error;
72
+ // No-op stub for demand-driven module entry resolution used by resolveModuleConnectionId.
73
+ // In tests, module entries are already in their final state so no resolution is needed.
74
+ context.ensureEntryConfigResolved = ()=>Promise.resolve();
69
75
  return context;
70
76
  }
71
77
  export default testContext;
@@ -12,11 +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 { type } from '@lowdefy/helpers';
16
- // Keys that look like operators (single key starting with _) but are not.
17
- const KNOWN_NON_OPERATORS = new Set([
18
- '_id'
19
- ]);
15
+ */ import { getOperatorType, type } from '@lowdefy/helpers';
20
16
  function walkAndCount(value, counter, parentConfigKey) {
21
17
  if (type.isArray(value)) {
22
18
  value.forEach((item)=>walkAndCount(item, counter, parentConfigKey));
@@ -26,20 +22,12 @@ function walkAndCount(value, counter, parentConfigKey) {
26
22
  return;
27
23
  }
28
24
  const configKey = value['~k'] || parentConfigKey;
29
- const keys = Object.keys(value);
30
- // Check if this object is an operator (single key starting with _, ignoring ~ prefixed keys)
31
- // This allows ~ignoreBuildCheck to be on the same object as an operator
32
- const nonTildeKeys = keys.filter((k)=>!k.startsWith('~'));
33
- if (nonTildeKeys.length === 1) {
34
- const key = nonTildeKeys[0];
35
- const [op] = key.split('.');
36
- const operator = op.replace(/^(_+)/gm, '_');
37
- if (operator.length > 1 && operator[0] === '_' && !KNOWN_NON_OPERATORS.has(operator)) {
38
- counter.increment(operator, configKey);
39
- }
25
+ const operator = getOperatorType(value);
26
+ if (operator) {
27
+ counter.increment(operator, configKey);
40
28
  }
41
29
  // Recurse into all values
42
- keys.forEach((key)=>{
30
+ Object.keys(value).forEach((key)=>{
43
31
  walkAndCount(value[key], counter, configKey);
44
32
  });
45
33
  }
@@ -67,6 +67,13 @@ function createPluginTypesMap({ packageName, packageTypes, typePrefix = '', type
67
67
  typePrefix,
68
68
  version
69
69
  });
70
+ createTypeDefinitions({
71
+ typeNames: type.isObject(packageTypes.auth) ? packageTypes.auth.strategies : [],
72
+ store: typesMap.auth.strategies,
73
+ packageName,
74
+ typePrefix,
75
+ version
76
+ });
70
77
  createTypeDefinitions({
71
78
  typeNames: packageTypes.blocks,
72
79
  store: typesMap.blocks,
@@ -95,6 +102,13 @@ function createPluginTypesMap({ packageName, packageTypes, typePrefix = '', type
95
102
  typePrefix,
96
103
  version
97
104
  });
105
+ createTypeDefinitions({
106
+ typeNames: packageTypes.notifications,
107
+ store: typesMap.notifications,
108
+ packageName,
109
+ typePrefix,
110
+ version
111
+ });
98
112
  createTypeDefinitions({
99
113
  typeNames: packageTypes.requests,
100
114
  store: typesMap.requests,
@@ -102,6 +116,13 @@ function createPluginTypesMap({ packageName, packageTypes, typePrefix = '', type
102
116
  typePrefix,
103
117
  version
104
118
  });
119
+ createTypeDefinitions({
120
+ typeNames: packageTypes.websockets,
121
+ store: typesMap.websockets,
122
+ packageName,
123
+ typePrefix,
124
+ version
125
+ });
105
126
  if (type.isObject(packageTypes.icons)) {
106
127
  Object.entries(packageTypes.icons).forEach(([blockType, icons])=>{
107
128
  typesMap.icons[`${typePrefix}${blockType}`] = icons;
@@ -0,0 +1,25 @@
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
+ */ // The declared environment names in config.cron.environments. Build key markers (~k, ~r, ~l) live
16
+ // on the same object as the environments, so they are skipped rather than treated as names.
17
+ const markerKeys = new Set([
18
+ '~k',
19
+ '~r',
20
+ '~l'
21
+ ]);
22
+ function getCronEnvironmentNames(environments) {
23
+ return Object.keys(environments ?? {}).filter((key)=>!markerKeys.has(key));
24
+ }
25
+ export default getCronEnvironmentNames;
@@ -12,19 +12,53 @@
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 { BuildError, LowdefyInternalError } from '@lowdefy/errors';
15
+ */ import { BuildError, LowdefyInternalError, resolveErrorLocation } from '@lowdefy/errors';
16
+ import serializeBuildException from './serializeBuildException.js';
16
17
  function logCollectedErrors(context) {
17
18
  if (context.errors.length === 0) return;
18
- context.errors.forEach((err)=>{
19
- if (err.isLowdefyError) {
20
- context.handleError(err);
21
- } else {
22
- const lowdefyErr = new LowdefyInternalError(err.message, {
23
- cause: err
19
+ // handleError mutates each error in place (resolves source/config), so
20
+ // capture the handled instance (not the original) for serialization below.
21
+ const handledErrors = [];
22
+ const seenSourceMessages = new Set();
23
+ for (const err of context.errors){
24
+ const lowdefyErr = err.isLowdefyError ? err : new LowdefyInternalError(err.message, {
25
+ cause: err
26
+ });
27
+ // handleError resolves the location and logs in one step, so the location
28
+ // has to be resolved here to decide whether the entry is worth logging at
29
+ // all. Resolution is pure, so handleError repeating it yields the same
30
+ // location. Resolution reads author-supplied data (keyMap entries, refMap
31
+ // paths), so a malformed entry can throw; the whole error report is worth
32
+ // more than one error's source line, so a failure leaves the error
33
+ // unlocated here and handleError logs it with its own guarded resolution.
34
+ let location = null;
35
+ try {
36
+ location = resolveErrorLocation(lowdefyErr, {
37
+ keyMap: context.keyMap,
38
+ refMap: context.refMap,
39
+ configDirectory: context.directories?.config
24
40
  });
25
- context.handleError(lowdefyErr);
41
+ } catch {
42
+ location = null;
26
43
  }
27
- });
28
- throw new BuildError(`Build failed with ${context.errors.length} error(s). See above for details.`);
44
+ // Two build steps can report the same config line: buildAuth reaches page,
45
+ // endpoint and websocket ids before validateId does, and both gates give
46
+ // the same message. Two genuinely distinct errors never share both a
47
+ // resolved source line and a message, so that pair is safe to collapse.
48
+ // Without a resolved source line there is no such evidence — the message
49
+ // alone would collapse distinct internal errors, and under-count the
50
+ // failures — so unlocated errors are always reported.
51
+ if (location?.source) {
52
+ const dedupKey = `${location.source}\n${lowdefyErr.message}`;
53
+ if (seenSourceMessages.has(dedupKey)) continue;
54
+ seenSourceMessages.add(dedupKey);
55
+ }
56
+ context.handleError(lowdefyErr);
57
+ handledErrors.push(lowdefyErr);
58
+ }
59
+ const buildError = new BuildError(`Build failed with ${handledErrors.length} error(s). See above for details.`);
60
+ buildError.errors = handledErrors.map(serializeBuildException);
61
+ buildError.warnings = (context.warnings ?? []).map(serializeBuildException);
62
+ throw buildError;
29
63
  }
30
64
  export default logCollectedErrors;
@@ -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
+ */ // Error.prototype.message is non-enumerable, so JSON.stringify(error) drops it
16
+ // (while enumerable fields like configKey/source survive). Build a plain,
17
+ // JSON-serializable summary instead - no stack, no cause, no `received`
18
+ // (arbitrary config values that may be huge or circular).
19
+ function serializeBuildException(exception) {
20
+ return {
21
+ message: exception.message,
22
+ name: exception.name,
23
+ source: exception.source ?? null,
24
+ config: exception.config ?? null,
25
+ configKey: exception.configKey ?? null,
26
+ checkSlug: exception.checkSlug ?? null
27
+ };
28
+ }
29
+ export default serializeBuildException;
@@ -0,0 +1,131 @@
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
+ */ // Validates a cron expression against the restricted subset Vercel Cron Jobs accept, so an invalid
16
+ // schedule fails at build instead of during the Vercel deployment. Vercel rules:
17
+ // - Exactly 5 fields: minute hour day-of-month month day-of-week.
18
+ // - Numeric values only — named values like MON or JAN are not supported.
19
+ // - Day-of-month and day-of-week are mutually exclusive: when one is set the other must be "*".
20
+ // Returns null when valid, otherwise a human-readable reason string.
21
+ const FIELDS = [
22
+ {
23
+ name: 'minute',
24
+ min: 0,
25
+ max: 59
26
+ },
27
+ {
28
+ name: 'hour',
29
+ min: 0,
30
+ max: 23
31
+ },
32
+ {
33
+ name: 'day-of-month',
34
+ min: 1,
35
+ max: 31
36
+ },
37
+ {
38
+ name: 'month',
39
+ min: 1,
40
+ max: 12
41
+ },
42
+ {
43
+ name: 'day-of-week',
44
+ min: 0,
45
+ max: 6
46
+ }
47
+ ];
48
+ function validateInt(value, { min, max }) {
49
+ if (!/^[0-9]+$/.test(value)) {
50
+ return `contains non-numeric value "${value}" (named values like MON/JAN are not supported)`;
51
+ }
52
+ const n = Number(value);
53
+ if (n < min || n > max) {
54
+ return `value "${value}" is out of range (${min}-${max})`;
55
+ }
56
+ return null;
57
+ }
58
+ function validatePart(part, { min, max }) {
59
+ let range = part;
60
+ if (part.includes('/')) {
61
+ const stepSplit = part.split('/');
62
+ if (stepSplit.length !== 2) {
63
+ return `has an invalid step in "${part}"`;
64
+ }
65
+ const [rangePart, stepPart] = stepSplit;
66
+ if (!/^[1-9][0-9]*$/.test(stepPart)) {
67
+ return `has an invalid step in "${part}"`;
68
+ }
69
+ range = rangePart;
70
+ }
71
+ if (range === '*') {
72
+ return null;
73
+ }
74
+ if (range.includes('-')) {
75
+ const rangeSplit = range.split('-');
76
+ if (rangeSplit.length !== 2) {
77
+ return `has an invalid range in "${part}"`;
78
+ }
79
+ const [startStr, endStr] = rangeSplit;
80
+ const startReason = validateInt(startStr, {
81
+ min,
82
+ max
83
+ });
84
+ if (startReason) return startReason;
85
+ const endReason = validateInt(endStr, {
86
+ min,
87
+ max
88
+ });
89
+ if (endReason) return endReason;
90
+ if (Number(startStr) > Number(endStr)) {
91
+ return `has a descending range in "${part}"`;
92
+ }
93
+ return null;
94
+ }
95
+ return validateInt(range, {
96
+ min,
97
+ max
98
+ });
99
+ }
100
+ function validateField(field, spec) {
101
+ if (field === '') {
102
+ return `${spec.name} field is empty`;
103
+ }
104
+ const parts = field.split(',');
105
+ for (const part of parts){
106
+ const reason = validatePart(part, spec);
107
+ if (reason) {
108
+ return `${spec.name} field "${field}" ${reason}`;
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+ function validateCronExpression(expression) {
114
+ if (typeof expression !== 'string') {
115
+ return 'cron expression must be a string';
116
+ }
117
+ const fields = expression.trim().split(/\s+/);
118
+ if (fields.length !== 5) {
119
+ return 'cron expression must have exactly 5 fields (minute hour day-of-month month day-of-week)';
120
+ }
121
+ for(let i = 0; i < FIELDS.length; i += 1){
122
+ const reason = validateField(fields[i], FIELDS[i]);
123
+ if (reason) return reason;
124
+ }
125
+ const [, , dayOfMonth, , dayOfWeek] = fields;
126
+ if (dayOfMonth !== '*' && dayOfWeek !== '*') {
127
+ return 'cannot specify both day-of-month and day-of-week; one must be "*"';
128
+ }
129
+ return null;
130
+ }
131
+ export default validateCronExpression;
@@ -13,6 +13,7 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import { ConfigError } from '@lowdefy/errors';
16
+ import { isReserved } from '@lowdefy/helpers';
16
17
  const validIdPattern = /^[A-Za-z0-9\-_/:]+$/;
17
18
  function validateId({ id, field, location, configKey }) {
18
19
  if (!validIdPattern.test(id)) {
@@ -20,5 +21,15 @@ function validateId({ id, field, location, configKey }) {
20
21
  configKey
21
22
  });
22
23
  }
24
+ // The pattern above admits every reserved name - underscores and letters are in its allowed set.
25
+ // A reserved id re-parents the plain-object registries that key on it - the engine's `requests`
26
+ // and `requestConfig` maps, and the connection/websocket/notification/endpoint maps - so reject
27
+ // it here where the config location is still in hand. Block ids do not reach this gate: they are
28
+ // dot-paths, so the pattern above excludes them and they carry their own per-segment check.
29
+ if (isReserved(id)) {
30
+ throw new ConfigError(`${field} "${id}"${location ? ` at ${location}` : ''} is a reserved name and cannot be used as an id.`, {
31
+ configKey
32
+ });
33
+ }
23
34
  }
24
35
  export default validateId;
@@ -13,10 +13,13 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */ import path from 'path';
16
- import { writeFile } from '@lowdefy/node-utils';
16
+ import { writeFileIfChanged } from '@lowdefy/node-utils';
17
17
  function createWriteBuildArtifact({ directories }) {
18
+ // Skipping byte-identical writes keeps artifact mtimes stable so Vite does
19
+ // not invalidate modules (clientJsMap → Routing HMR, serverJsMap → SSR
20
+ // reload) on JIT page builds that changed nothing.
18
21
  async function writeBuildArtifact(filePath, content) {
19
- await writeFile(path.join(directories.build, filePath), content);
22
+ await writeFileIfChanged(path.join(directories.build, filePath), content);
20
23
  }
21
24
  return writeBuildArtifact;
22
25
  }