@lowdefy/build 5.6.0 → 6.1.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 (102) hide show
  1. package/dist/build/buildAgents.js +61 -16
  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 +40 -0
  8. package/dist/build/buildApi/validateEndpoint.js +96 -1
  9. package/dist/build/buildApp.js +13 -20
  10. package/dist/build/buildAppMeta.js +138 -0
  11. package/dist/build/buildAuth/buildAgentAuth.js +14 -1
  12. package/dist/build/buildAuth/buildAuth.js +8 -3
  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/getProtectedWebsockets.js +29 -0
  17. package/dist/build/buildAuth/getWebsocketRoles.js +39 -0
  18. package/dist/build/buildAuth/validateAuthConfig.js +15 -3
  19. package/dist/build/buildEnvironments.js +261 -0
  20. package/dist/build/buildImports/buildImportsDev.js +15 -0
  21. package/dist/build/buildImports/buildImportsProd.js +4 -1
  22. package/dist/build/buildJs/generateClientJsModule.js +27 -0
  23. package/dist/build/buildJs/writeJs.js +3 -5
  24. package/dist/build/buildLogger.js +22 -6
  25. package/dist/build/buildMcp.js +73 -0
  26. package/dist/build/buildModuleDefs.js +90 -48
  27. package/dist/build/buildModules.js +61 -14
  28. package/dist/build/buildNotifications.js +98 -0
  29. package/dist/build/buildPages/buildBlock/buildBlock.js +3 -1
  30. package/dist/build/buildPages/buildBlock/buildDynamicBlock.js +131 -0
  31. package/dist/build/buildPages/buildBlock/buildEvents.js +186 -21
  32. package/dist/build/buildPages/buildBlock/buildRequests.js +9 -0
  33. package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +2 -11
  34. package/dist/build/buildPages/buildBlock/setBlockId.js +5 -2
  35. package/dist/build/buildPages/buildBlock/validateBlock.js +7 -0
  36. package/dist/build/buildPages/buildPage.js +27 -3
  37. package/dist/build/buildPages/buildSubscriptions.js +145 -0
  38. package/dist/build/buildPages/validateDynamicBlockRefs.js +30 -0
  39. package/dist/build/buildPages/validateStateReferences.js +4 -2
  40. package/dist/build/buildPages/validateWebsocketRefs.js +30 -0
  41. package/dist/build/buildRefs/buildRefs.js +21 -12
  42. package/dist/build/buildRefs/cloneWithMarkers.js +57 -0
  43. package/dist/build/buildRefs/deferredRegions.js +80 -0
  44. package/dist/build/buildRefs/deferredRegistry.js +292 -0
  45. package/dist/build/buildRefs/getModuleRefContent.js +21 -6
  46. package/dist/build/buildRefs/getUserJavascriptFunction.js +1 -1
  47. package/dist/build/buildRefs/parseRefContent.js +24 -0
  48. package/dist/build/buildRefs/{evaluateStaticOperators.js → precomputeRuntimeOperators.js} +7 -6
  49. package/dist/build/buildRefs/walker.js +471 -207
  50. package/dist/build/buildTypes.js +22 -1
  51. package/dist/build/buildWebsockets.js +99 -0
  52. package/dist/build/collectPageContent.js +7 -1
  53. package/dist/build/copyAgentFileSystems.js +2 -2
  54. package/dist/build/full/buildPages.js +16 -0
  55. package/dist/build/full/updateServerPackageJson.js +21 -0
  56. package/dist/build/jit/buildPageJit.js +46 -6
  57. package/dist/build/jit/collectSkeletonSourceFiles.js +14 -0
  58. package/dist/build/jit/pageContentKeys.js +1 -0
  59. package/dist/build/jit/shallowBuild.js +84 -3
  60. package/dist/build/jit/writePageJit.js +11 -8
  61. package/dist/build/registerModules.js +125 -24
  62. package/dist/build/validateCallAgentSteps.js +58 -0
  63. package/dist/build/validateConfig.js +6 -1
  64. package/dist/build/validateEnvironmentGuards.js +69 -0
  65. package/dist/build/validateRenderNotificationSteps.js +50 -0
  66. package/dist/build/writeApi.js +98 -1
  67. package/dist/build/writeMcp.js +21 -0
  68. package/dist/build/writeNotifications.js +30 -0
  69. package/dist/build/writePluginImports/importPluginModule.js +39 -0
  70. package/dist/build/writePluginImports/writeActionSchemaMap.js +6 -7
  71. package/dist/build/writePluginImports/writeAuthImports.js +4 -0
  72. package/dist/build/writePluginImports/writeAvailableTypes.js +99 -0
  73. package/dist/build/writePluginImports/writeBlockSchemaMap.js +10 -9
  74. package/dist/build/writePluginImports/writeConnectionSchemaMap.js +101 -0
  75. package/dist/build/writePluginImports/writeGlobalsCss.js +20 -10
  76. package/dist/build/writePluginImports/writeNotificationImports.js +31 -0
  77. package/dist/build/writePluginImports/writeOperatorSchemaMap.js +6 -7
  78. package/dist/build/writePluginImports/writePluginImports.js +18 -4
  79. package/dist/build/writePluginImports/writeWebsocketImports.js +22 -0
  80. package/dist/build/writeWebsockets.js +30 -0
  81. package/dist/createContext.js +6 -1
  82. package/dist/defaultPackages.js +7 -1
  83. package/dist/defaultTypesMap.js +688 -388
  84. package/dist/dynamic/buildDynamicBlocks.js +120 -0
  85. package/dist/index.js +81 -10
  86. package/dist/indexDev.js +4 -0
  87. package/dist/lowdefySchema.js +969 -6
  88. package/dist/scripts/generateDefaultTypes.js +5 -2
  89. package/dist/test-utils/expectTerminates.js +33 -0
  90. package/dist/test-utils/runBuild.js +15 -1
  91. package/dist/test-utils/runBuildForSnapshots.js +37 -4
  92. package/dist/test-utils/testContext.js +8 -2
  93. package/dist/utils/countOperators.js +5 -17
  94. package/dist/utils/createPluginTypesMap.js +21 -0
  95. package/dist/utils/getEnvironmentNames.js +26 -0
  96. package/dist/utils/logCollectedErrors.js +15 -9
  97. package/dist/utils/serializeBuildException.js +29 -0
  98. package/dist/utils/validateCronExpression.js +131 -0
  99. package/dist/utils/writeBuildArtifact.js +5 -2
  100. package/package.json +67 -60
  101. package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
  102. /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;
@@ -54,9 +54,15 @@ import createTestLogger from './createTestLogger.js';
54
54
  Button: {
55
55
  package: '@lowdefy/blocks-basic'
56
56
  },
57
+ ClickableHtml: {
58
+ package: '@lowdefy/blocks-basic'
59
+ },
57
60
  DangerousHtml: {
58
61
  package: '@lowdefy/blocks-basic'
59
62
  },
63
+ Dynamic: {
64
+ package: '@lowdefy/blocks-basic'
65
+ },
60
66
  Html: {
61
67
  package: '@lowdefy/blocks-basic'
62
68
  },
@@ -142,7 +148,15 @@ import createTestLogger from './createTestLogger.js';
142
148
  events: {},
143
149
  providers: {
144
150
  GoogleProvider: {
145
- package: 'next-auth'
151
+ package: '@lowdefy/plugin-next-auth'
152
+ }
153
+ },
154
+ strategies: {
155
+ apiKey: {
156
+ package: '@lowdefy/plugin-next-auth'
157
+ },
158
+ jwt: {
159
+ package: '@lowdefy/plugin-next-auth'
146
160
  }
147
161
  }
148
162
  },
@@ -87,9 +87,15 @@ import path from 'path';
87
87
  Button: {
88
88
  package: '@lowdefy/blocks-basic'
89
89
  },
90
+ ClickableHtml: {
91
+ package: '@lowdefy/blocks-basic'
92
+ },
90
93
  DangerousHtml: {
91
94
  package: '@lowdefy/blocks-basic'
92
95
  },
96
+ Dynamic: {
97
+ package: '@lowdefy/blocks-basic'
98
+ },
93
99
  Html: {
94
100
  package: '@lowdefy/blocks-basic'
95
101
  },
@@ -280,6 +286,9 @@ import path from 'path';
280
286
  }
281
287
  },
282
288
  connections: {
289
+ Anthropic: {
290
+ package: '@lowdefy/connection-anthropic'
291
+ },
283
292
  AxiosHttp: {
284
293
  package: '@lowdefy/connection-axios-http'
285
294
  },
@@ -312,6 +321,12 @@ import path from 'path';
312
321
  AxiosHttp: {
313
322
  package: '@lowdefy/connection-axios-http'
314
323
  },
324
+ GenerateObject: {
325
+ package: '@lowdefy/connection-anthropic'
326
+ },
327
+ GenerateText: {
328
+ package: '@lowdefy/connection-anthropic'
329
+ },
315
330
  MongoDBFind: {
316
331
  package: '@lowdefy/connection-mongodb'
317
332
  },
@@ -370,6 +385,16 @@ import path from 'path';
370
385
  package: '@lowdefy/plugin-aws'
371
386
  }
372
387
  },
388
+ agents: {
389
+ ClaudeAgent: {
390
+ package: '@lowdefy/connection-anthropic'
391
+ }
392
+ },
393
+ notifications: {
394
+ NotificationEmail: {
395
+ package: '@lowdefy/email-templates'
396
+ }
397
+ },
373
398
  auth: {
374
399
  adapters: {
375
400
  MongoDBAdapter: {
@@ -415,16 +440,24 @@ import path from 'path';
415
440
  package: '@lowdefy/plugin-auth0'
416
441
  },
417
442
  GoogleProvider: {
418
- package: 'next-auth'
443
+ package: '@lowdefy/plugin-next-auth'
419
444
  },
420
445
  GitHubProvider: {
421
- package: 'next-auth'
446
+ package: '@lowdefy/plugin-next-auth'
422
447
  },
423
448
  CredentialsProvider: {
424
- package: 'next-auth'
449
+ package: '@lowdefy/plugin-next-auth'
425
450
  },
426
451
  EmailProvider: {
427
- package: 'next-auth'
452
+ package: '@lowdefy/plugin-next-auth'
453
+ }
454
+ },
455
+ strategies: {
456
+ apiKey: {
457
+ package: '@lowdefy/plugin-next-auth'
458
+ },
459
+ jwt: {
460
+ package: '@lowdefy/plugin-next-auth'
428
461
  }
429
462
  }
430
463
  },
@@ -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,26 @@
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.environments.
16
+ // Build key markers (~k, ~r, ~l) live on the same object as the environments, so they are skipped
17
+ // rather than treated as names.
18
+ const markerKeys = new Set([
19
+ '~k',
20
+ '~r',
21
+ '~l'
22
+ ]);
23
+ function getEnvironmentNames(environments) {
24
+ return Object.keys(environments ?? {}).filter((key)=>!markerKeys.has(key));
25
+ }
26
+ export default getEnvironmentNames;
@@ -13,9 +13,12 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
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
- let loggedCount = 0;
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 = [];
19
22
  const seenSourceMessages = new Set();
20
23
  for (const err of context.errors){
21
24
  const lowdefyErr = err.isLowdefyError ? err : new LowdefyInternalError(err.message, {
@@ -39,20 +42,23 @@ function logCollectedErrors(context) {
39
42
  location = null;
40
43
  }
41
44
  // Two build steps can report the same config line: buildAuth reaches page,
42
- // endpoint and agent ids before validateId does, and both gates give the
43
- // same message. Two genuinely distinct errors never share both a resolved
44
- // source line and a message, so that pair is safe to collapse. Without a
45
- // resolved source line there is no such evidence - the message alone would
46
- // collapse distinct internal errors and under-count the failures - so
47
- // unlocated errors are always reported.
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.
48
51
  if (location?.source) {
49
52
  const dedupKey = `${location.source}\n${lowdefyErr.message}`;
50
53
  if (seenSourceMessages.has(dedupKey)) continue;
51
54
  seenSourceMessages.add(dedupKey);
52
55
  }
53
56
  context.handleError(lowdefyErr);
54
- loggedCount += 1;
57
+ handledErrors.push(lowdefyErr);
55
58
  }
56
- throw new BuildError(`Build failed with ${loggedCount} error(s). See above for details.`);
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;
57
63
  }
58
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,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
  }