@lowdefy/build 5.6.0 → 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 (100) 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 +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 +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/buildImports/buildImportsDev.js +15 -0
  20. package/dist/build/buildImports/buildImportsProd.js +4 -1
  21. package/dist/build/buildJs/generateClientJsModule.js +27 -0
  22. package/dist/build/buildJs/writeJs.js +3 -5
  23. package/dist/build/buildMcp.js +73 -0
  24. package/dist/build/buildModuleDefs.js +90 -48
  25. package/dist/build/buildModules.js +61 -14
  26. package/dist/build/buildNotifications.js +98 -0
  27. package/dist/build/buildPages/buildBlock/buildBlock.js +3 -1
  28. package/dist/build/buildPages/buildBlock/buildDynamicBlock.js +131 -0
  29. package/dist/build/buildPages/buildBlock/buildEvents.js +186 -21
  30. package/dist/build/buildPages/buildBlock/buildRequests.js +9 -0
  31. package/dist/build/buildPages/buildBlock/normalizeClassAndStyles.js +2 -11
  32. package/dist/build/buildPages/buildBlock/setBlockId.js +5 -2
  33. package/dist/build/buildPages/buildBlock/validateBlock.js +7 -0
  34. package/dist/build/buildPages/buildPage.js +27 -3
  35. package/dist/build/buildPages/buildSubscriptions.js +145 -0
  36. package/dist/build/buildPages/validateDynamicBlockRefs.js +30 -0
  37. package/dist/build/buildPages/validateStateReferences.js +4 -2
  38. package/dist/build/buildPages/validateWebsocketRefs.js +30 -0
  39. package/dist/build/buildRefs/buildRefs.js +21 -12
  40. package/dist/build/buildRefs/cloneWithMarkers.js +57 -0
  41. package/dist/build/buildRefs/deferredRegions.js +80 -0
  42. package/dist/build/buildRefs/deferredRegistry.js +292 -0
  43. package/dist/build/buildRefs/getModuleRefContent.js +21 -6
  44. package/dist/build/buildRefs/getUserJavascriptFunction.js +1 -1
  45. package/dist/build/buildRefs/parseRefContent.js +24 -0
  46. package/dist/build/buildRefs/{evaluateStaticOperators.js → precomputeRuntimeOperators.js} +7 -6
  47. package/dist/build/buildRefs/walker.js +471 -207
  48. package/dist/build/buildTypes.js +22 -1
  49. package/dist/build/buildWebsockets.js +99 -0
  50. package/dist/build/collectPageContent.js +7 -1
  51. package/dist/build/copyAgentFileSystems.js +2 -2
  52. package/dist/build/full/buildPages.js +16 -0
  53. package/dist/build/full/updateServerPackageJson.js +21 -0
  54. package/dist/build/jit/buildPageJit.js +46 -6
  55. package/dist/build/jit/collectSkeletonSourceFiles.js +14 -0
  56. package/dist/build/jit/pageContentKeys.js +1 -0
  57. package/dist/build/jit/shallowBuild.js +81 -1
  58. package/dist/build/jit/writePageJit.js +11 -8
  59. package/dist/build/registerModules.js +125 -24
  60. package/dist/build/validateCallAgentSteps.js +58 -0
  61. package/dist/build/validateConfig.js +4 -0
  62. package/dist/build/validateCronConfig.js +110 -0
  63. package/dist/build/validateRenderNotificationSteps.js +50 -0
  64. package/dist/build/writeApi.js +58 -1
  65. package/dist/build/writeMcp.js +21 -0
  66. package/dist/build/writeNotifications.js +30 -0
  67. package/dist/build/writePluginImports/importPluginModule.js +39 -0
  68. package/dist/build/writePluginImports/writeActionSchemaMap.js +6 -7
  69. package/dist/build/writePluginImports/writeAuthImports.js +4 -0
  70. package/dist/build/writePluginImports/writeAvailableTypes.js +99 -0
  71. package/dist/build/writePluginImports/writeBlockSchemaMap.js +10 -9
  72. package/dist/build/writePluginImports/writeConnectionSchemaMap.js +101 -0
  73. package/dist/build/writePluginImports/writeGlobalsCss.js +20 -10
  74. package/dist/build/writePluginImports/writeNotificationImports.js +31 -0
  75. package/dist/build/writePluginImports/writeOperatorSchemaMap.js +6 -7
  76. package/dist/build/writePluginImports/writePluginImports.js +18 -4
  77. package/dist/build/writePluginImports/writeWebsocketImports.js +22 -0
  78. package/dist/build/writeWebsockets.js +30 -0
  79. package/dist/createContext.js +6 -1
  80. package/dist/defaultPackages.js +7 -1
  81. package/dist/defaultTypesMap.js +666 -388
  82. package/dist/dynamic/buildDynamicBlocks.js +120 -0
  83. package/dist/index.js +78 -8
  84. package/dist/indexDev.js +4 -0
  85. package/dist/lowdefySchema.js +861 -6
  86. package/dist/scripts/generateDefaultTypes.js +5 -2
  87. package/dist/test-utils/expectTerminates.js +33 -0
  88. package/dist/test-utils/runBuild.js +12 -1
  89. package/dist/test-utils/runBuildForSnapshots.js +34 -4
  90. package/dist/test-utils/testContext.js +8 -2
  91. package/dist/utils/countOperators.js +5 -17
  92. package/dist/utils/createPluginTypesMap.js +21 -0
  93. package/dist/utils/getCronEnvironmentNames.js +25 -0
  94. package/dist/utils/logCollectedErrors.js +15 -9
  95. package/dist/utils/serializeBuildException.js +29 -0
  96. package/dist/utils/validateCronExpression.js +131 -0
  97. package/dist/utils/writeBuildArtifact.js +5 -2
  98. package/package.json +66 -59
  99. package/dist/build/writePluginImports/writeServerExternalPackages.js +0 -121
  100. /package/dist/build/{resolveModuleOperators.js → buildRefs/scopeMenuItemIds.js} +0 -0
@@ -70,11 +70,14 @@ function buildTypes({ components, context }) {
70
70
  adapters: {},
71
71
  callbacks: {},
72
72
  events: {},
73
- providers: {}
73
+ providers: {},
74
+ strategies: {}
74
75
  },
75
76
  blocks: {},
76
77
  connections: {},
78
+ notifications: {},
77
79
  requests: {},
80
+ websockets: {},
78
81
  api: {},
79
82
  operators: {
80
83
  client: {},
@@ -117,6 +120,12 @@ function buildTypes({ components, context }) {
117
120
  store: components.types.auth.providers,
118
121
  typeClass: 'Auth provider'
119
122
  });
123
+ buildTypeClass(context, {
124
+ counter: typeCounters.auth.strategies,
125
+ definitions: context.typesMap.auth.strategies,
126
+ store: components.types.auth.strategies,
127
+ typeClass: 'Auth strategy'
128
+ });
120
129
  buildTypeClass(context, {
121
130
  counter: typeCounters.blocks,
122
131
  definitions: context.typesMap.blocks,
@@ -129,12 +138,24 @@ function buildTypes({ components, context }) {
129
138
  store: components.types.connections,
130
139
  typeClass: 'Connection'
131
140
  });
141
+ buildTypeClass(context, {
142
+ counter: typeCounters.notifications,
143
+ definitions: context.typesMap.notifications,
144
+ store: components.types.notifications,
145
+ typeClass: 'Notification'
146
+ });
132
147
  buildTypeClass(context, {
133
148
  counter: typeCounters.requests,
134
149
  definitions: context.typesMap.requests,
135
150
  store: components.types.requests,
136
151
  typeClass: 'Request'
137
152
  });
153
+ buildTypeClass(context, {
154
+ counter: typeCounters.websockets,
155
+ definitions: context.typesMap.websockets,
156
+ store: components.types.websockets,
157
+ typeClass: 'Websocket'
158
+ });
138
159
  buildTypeClass(context, {
139
160
  counter: typeCounters.operators.client,
140
161
  definitions: context.typesMap.operators.client,
@@ -0,0 +1,99 @@
1
+ /* eslint-disable no-param-reassign */ /*
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 { ConfigError } from '@lowdefy/errors';
17
+ import countOperators from '../utils/countOperators.js';
18
+ import createCheckDuplicateId from '../utils/createCheckDuplicateId.js';
19
+ import validateId from '../utils/validateId.js';
20
+ function buildWebsockets({ components, context }) {
21
+ if (components.websockets && !type.isArray(components.websockets)) {
22
+ throw new ConfigError('Websockets is not an array.', {
23
+ received: components.websockets
24
+ });
25
+ }
26
+ const websockets = type.isArray(components.websockets) ? components.websockets : [];
27
+ context.websocketIds = new Set();
28
+ const checkDuplicateWebsocketId = createCheckDuplicateId({
29
+ message: 'Duplicate websocketId "{{ id }}".'
30
+ });
31
+ websockets.forEach((websocket, index)=>{
32
+ const configKey = websocket['~k'];
33
+ if (type.isUndefined(websocket.id)) {
34
+ throw new ConfigError(`Websocket id missing at websocket ${index}.`, {
35
+ configKey
36
+ });
37
+ }
38
+ if (!type.isString(websocket.id)) {
39
+ throw new ConfigError(`Websocket id is not a string at websocket ${index}.`, {
40
+ received: websocket.id,
41
+ configKey
42
+ });
43
+ }
44
+ validateId({
45
+ id: websocket.id,
46
+ field: 'Websocket id',
47
+ configKey
48
+ });
49
+ checkDuplicateWebsocketId({
50
+ id: websocket.id,
51
+ configKey
52
+ });
53
+ if (!type.isString(websocket.type)) {
54
+ throw new ConfigError(`Websocket type is not a string at websocket "${websocket.id}".`, {
55
+ received: websocket.type,
56
+ configKey
57
+ });
58
+ }
59
+ // Track type usage for buildTypes validation
60
+ context.typeCounters.websockets.increment(websocket.type, configKey);
61
+ // connectionId is optional — Channel and other connectionless types don't need one
62
+ if (!type.isNone(websocket.connectionId)) {
63
+ if (!type.isString(websocket.connectionId)) {
64
+ throw new ConfigError(`Websocket connectionId is not a string at websocket "${websocket.id}".`, {
65
+ received: websocket.connectionId,
66
+ configKey
67
+ });
68
+ }
69
+ // Connections may have been renamed by buildConnections:
70
+ // connection.connectionId = original id, connection.id = 'connection:' + original id
71
+ const connectionExists = (components.connections ?? []).some((c)=>c.id === websocket.connectionId || c.connectionId === websocket.connectionId);
72
+ if (!connectionExists) {
73
+ throw new ConfigError(`Websocket "${websocket.id}" references connectionId "${websocket.connectionId}" which does not exist.`, {
74
+ configKey,
75
+ checkSlug: 'connection-refs'
76
+ });
77
+ }
78
+ }
79
+ if (type.isNone(websocket.properties)) {
80
+ websocket.properties = {};
81
+ }
82
+ if (!type.isObject(websocket.properties)) {
83
+ throw new ConfigError(`Websocket properties is not an object at websocket "${websocket.id}".`, {
84
+ received: websocket.properties,
85
+ configKey
86
+ });
87
+ }
88
+ // Rename id to internal format
89
+ websocket.websocketId = websocket.id;
90
+ context.websocketIds.add(websocket.websocketId);
91
+ websocket.id = `websocket:${websocket.websocketId}`;
92
+ // Count server operators in properties
93
+ countOperators(websocket.properties, {
94
+ counter: context.typeCounters.operators.server
95
+ });
96
+ });
97
+ return components;
98
+ }
99
+ export default buildWebsockets;
@@ -24,7 +24,13 @@
24
24
  return;
25
25
  }
26
26
  if (value && typeof value === 'object') {
27
- for (const v of Object.values(value)){
27
+ for (const [key, v] of Object.entries(value)){
28
+ // Build marker values (~k keys regenerate on every JIT rebuild) are
29
+ // never tailwind candidates — including them would make the collected
30
+ // content differ between byte-identical page builds.
31
+ if (key === '~k' || key === '~r' || key === '~l') {
32
+ continue;
33
+ }
28
34
  walkValue(v, strings);
29
35
  }
30
36
  }
@@ -25,8 +25,8 @@ async function copyAgentFileSystems({ components, context }) {
25
25
  seen.add(basePath);
26
26
  basePaths.push(basePath);
27
27
  }
28
- // Manifest is consumed by next.config.js to populate outputFileTracingIncludes,
29
- // so the Next.js tracer bundles these directories on Vercel and standalone builds.
28
+ // Manifest of agent file system base paths copied into the server directory —
29
+ // available to deployment tooling that needs to include these directories.
30
30
  await context.writeBuildArtifact('agentFileSystems.json', JSON.stringify(basePaths));
31
31
  if (context.directories.config === context.directories.server) return;
32
32
  for (const basePath of basePaths){
@@ -17,10 +17,12 @@ import { ConfigError, shouldSuppressBuildCheck } from '@lowdefy/errors';
17
17
  import buildPage from '../buildPages/buildPage.js';
18
18
  import createCheckDuplicateId from '../../utils/createCheckDuplicateId.js';
19
19
  import validateCallApiRefs from '../buildPages/validateCallApiRefs.js';
20
+ import validateDynamicBlockRefs from '../buildPages/validateDynamicBlockRefs.js';
20
21
  import validateLinkReferences from '../buildPages/validateLinkReferences.js';
21
22
  import validatePayloadReferences from '../buildPages/validatePayloadReferences.js';
22
23
  import validateServerStateReferences from '../buildPages/validateServerStateReferences.js';
23
24
  import validateStateReferences from '../buildPages/validateStateReferences.js';
25
+ import validateWebsocketRefs from '../buildPages/validateWebsocketRefs.js';
24
26
  function buildPages({ components, context }) {
25
27
  const pages = type.isArray(components.pages) ? components.pages : [];
26
28
  const checkDuplicatePageId = createCheckDuplicateId({
@@ -29,6 +31,8 @@ function buildPages({ components, context }) {
29
31
  // Initialize action ref collections across all pages
30
32
  context.linkActionRefs = [];
31
33
  context.callApiActionRefs = [];
34
+ context.websocketActionRefs = [];
35
+ context.dynamicBlockRefs = [];
32
36
  // Track which pages failed to build so we skip them in validation
33
37
  const failedPageIndices = new Set();
34
38
  // Wrap each page build to collect errors instead of stopping on first error
@@ -73,6 +77,18 @@ function buildPages({ components, context }) {
73
77
  endpointConfigs,
74
78
  context
75
79
  });
80
+ // Validate that Dynamic blocks reference existing endpoints
81
+ validateDynamicBlockRefs({
82
+ dynamicBlockRefs: context.dynamicBlockRefs,
83
+ endpointConfigs,
84
+ context
85
+ });
86
+ // Validate that Subscribe/Unsubscribe/Publish actions reference defined websockets
87
+ validateWebsocketRefs({
88
+ websocketActionRefs: context.websocketActionRefs,
89
+ websocketIds: context.websocketIds ?? new Set(),
90
+ context
91
+ });
76
92
  // Validate that _state references use defined block IDs
77
93
  // and _payload references use defined payload keys
78
94
  // Skip pages that failed to build
@@ -21,6 +21,12 @@ async function updateServerPackageJson({ components, context }) {
21
21
  const dependencies = packageJson.dependencies;
22
22
  function getPackages(types) {
23
23
  Object.values(types).forEach((type)=>{
24
+ // Deployment tooling may have rewritten a workspace plugin to a link:
25
+ // path — overwriting it with the configured version (e.g. workspace:*)
26
+ // would break installs outside the monorepo workspace.
27
+ if (dependencies[type.package]?.startsWith('link:')) {
28
+ return;
29
+ }
24
30
  dependencies[type.package] = type.version;
25
31
  });
26
32
  }
@@ -30,11 +36,26 @@ async function updateServerPackageJson({ components, context }) {
30
36
  getPackages(components.types.auth.callbacks);
31
37
  getPackages(components.types.auth.events);
32
38
  getPackages(components.types.auth.providers);
39
+ getPackages(components.types.auth.strategies);
33
40
  getPackages(components.types.blocks);
34
41
  getPackages(components.types.connections);
42
+ getPackages(components.types.notifications);
35
43
  getPackages(components.types.requests);
44
+ getPackages(components.types.websockets);
36
45
  getPackages(components.types.operators.client);
37
46
  getPackages(components.types.operators.server);
47
+ if ((components.notifications ?? []).length > 0) {
48
+ // plugins/notifications.js re-exports renderEmail from @lowdefy/email-templates
49
+ // for every app with notifications — including apps that only use custom
50
+ // template types, where no framework template appears in components.types.
51
+ // The react-email preview CLI is deliberately NOT added here — it is a
52
+ // dev-only tool the `lowdefy emails` command installs just-in-time, so
53
+ // production servers never carry it.
54
+ const emailTemplates = Object.values(context.typesMap.notifications ?? {}).find((t)=>t.package === '@lowdefy/email-templates');
55
+ if (emailTemplates && !dependencies['@lowdefy/email-templates']?.startsWith('link:')) {
56
+ dependencies['@lowdefy/email-templates'] = emailTemplates.version;
57
+ }
58
+ }
38
59
  // Sort dependencies
39
60
  packageJson.dependencies = {};
40
61
  Object.keys(dependencies).sort().forEach((name)=>{
@@ -20,19 +20,23 @@ import operators from '@lowdefy/operators-js/operators/build';
20
20
  import addKeys from '../addKeys.js';
21
21
  import buildPage from '../buildPages/buildPage.js';
22
22
  import validateCallApiRefs from '../buildPages/validateCallApiRefs.js';
23
+ import validateDynamicBlockRefs from '../buildPages/validateDynamicBlockRefs.js';
23
24
  import validateLinkReferences from '../buildPages/validateLinkReferences.js';
24
25
  import validatePayloadReferences from '../buildPages/validatePayloadReferences.js';
25
26
  import validateServerStateReferences from '../buildPages/validateServerStateReferences.js';
26
27
  import validateStateReferences from '../buildPages/validateStateReferences.js';
28
+ import validateWebsocketRefs from '../buildPages/validateWebsocketRefs.js';
27
29
  import collectDynamicIdentifiers from '../collectDynamicIdentifiers.js';
30
+ import collectPageContent from '../collectPageContent.js';
28
31
  import createCheckDuplicateId from '../../utils/createCheckDuplicateId.js';
29
32
  import createContext from '../../createContext.js';
30
- import evaluateStaticOperators from '../buildRefs/evaluateStaticOperators.js';
33
+ import precomputeRuntimeOperators from '../buildRefs/precomputeRuntimeOperators.js';
31
34
  import getRefContent from '../buildRefs/getRefContent.js';
32
35
  import jsMapParser from '../buildJs/jsMapParser.js';
33
36
  import makeRefDefinition from '../buildRefs/makeRefDefinition.js';
34
37
  import rebaseModuleRefPaths from '../buildRefs/rebaseModuleRefPaths.js';
35
- import { resolve, WalkContext, cloneForResolve, tagRefDeep } from '../buildRefs/walker.js';
38
+ import { resolve, WalkContext, tagRefDeep } from '../buildRefs/walker.js';
39
+ import cloneWithMarkers from '../buildRefs/cloneWithMarkers.js';
36
40
  import validateOperatorsDynamic from '../validateOperatorsDynamic.js';
37
41
  import writeMaps from '../writeMaps.js';
38
42
  import detectMissingIcons from './detectMissingIcons.js';
@@ -131,10 +135,11 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
131
135
  refChain: new Set(),
132
136
  operators,
133
137
  env: process.env,
138
+ lowdefyApp: buildContext.appMeta,
134
139
  dynamicIdentifiers,
135
140
  shouldStop: null
136
141
  });
137
- resolvedVars = await resolve(cloneForResolve(unresolvedVars), varCtx);
142
+ resolvedVars = await resolve(cloneWithMarkers(unresolvedVars), varCtx);
138
143
  }
139
144
  let refDef;
140
145
  if (pageEntry.resolverOriginal) {
@@ -188,11 +193,12 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
188
193
  refChain: new Set(),
189
194
  operators,
190
195
  env: process.env,
196
+ lowdefyApp: buildContext.appMeta,
191
197
  dynamicIdentifiers,
192
198
  shouldStop: null
193
199
  });
194
200
  let processed = await resolve(pageContent, pageCtx);
195
- processed = evaluateStaticOperators({
201
+ processed = precomputeRuntimeOperators({
196
202
  context: buildContext,
197
203
  input: processed,
198
204
  refDef
@@ -232,6 +238,19 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
232
238
  if (!buildContext.callApiActionRefs) {
233
239
  buildContext.callApiActionRefs = [];
234
240
  }
241
+ if (!buildContext.websocketActionRefs) {
242
+ buildContext.websocketActionRefs = [];
243
+ }
244
+ if (!buildContext.dynamicBlockRefs) {
245
+ buildContext.dynamicBlockRefs = [];
246
+ }
247
+ // buildSubscriptions validates against websocketIds — the dev server
248
+ // restores the set from the websocketIds.json skeleton artifact. Rebuild
249
+ // it from skeleton-built websockets when the context doesn't carry it
250
+ // (createContext initializes an empty set, so check size, not presence).
251
+ if (!buildContext.websocketIds?.size) {
252
+ buildContext.websocketIds = new Set((buildContext.components?.websockets ?? []).map((websocket)=>websocket.websocketId));
253
+ }
235
254
  // Build the page (validation, block processing)
236
255
  const checkDuplicatePageId = createCheckDuplicateId({
237
256
  message: 'Duplicate pageId "{{ id }}".'
@@ -285,6 +304,16 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
285
304
  endpointConfigs,
286
305
  context: buildContext
287
306
  });
307
+ validateDynamicBlockRefs({
308
+ dynamicBlockRefs: buildContext.dynamicBlockRefs,
309
+ endpointConfigs,
310
+ context: buildContext
311
+ });
312
+ validateWebsocketRefs({
313
+ websocketActionRefs: buildContext.websocketActionRefs,
314
+ websocketIds: buildContext.websocketIds,
315
+ context: buildContext
316
+ });
288
317
  validateStateReferences({
289
318
  page: processed,
290
319
  context: buildContext
@@ -297,6 +326,12 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
297
326
  page: processed,
298
327
  context: buildContext
299
328
  });
329
+ // Collect Tailwind class candidates before _js extraction — jsMapParser
330
+ // replaces _js source with hashes, so classes used only inside _js source
331
+ // would otherwise never reach the Tailwind scanner.
332
+ const tailwindContent = collectPageContent([
333
+ processed
334
+ ]);
300
335
  // Extract JS functions from the page
301
336
  const pageRequests = [
302
337
  ...processed.requests ?? []
@@ -323,10 +358,15 @@ async function buildPageJit({ pageId, pageRegistry, context, directories, logger
323
358
  throw error;
324
359
  }
325
360
  // Write page artifacts
326
- await writePageJit({
361
+ const { tailwindChanged } = await writePageJit({
327
362
  page: finalPage,
328
- context: buildContext
363
+ context: buildContext,
364
+ tailwindContent
329
365
  });
366
+ // Attached after the disk write (like _warnings) so it never persists in
367
+ // artifacts — the JIT server uses it to decide whether to trigger a CSS
368
+ // recompile for newly discovered tailwind class candidates.
369
+ finalPage._tailwindChanged = tailwindChanged;
330
370
  // Attach warnings after disk write so they don't persist in artifacts
331
371
  if (buildWarnings.length > 0) {
332
372
  finalPage._warnings = buildWarnings.map((w)=>({
@@ -47,6 +47,18 @@ function collectSkeletonSourceFiles({ components, context }) {
47
47
  for (const moduleEntry of Object.values(context.modules ?? {})){
48
48
  walkRefIds(moduleEntry.consumerVars, refIds);
49
49
  }
50
+ // The walker only ~r-tags resolved _ref content, never the root file's own
51
+ // objects — so a scalar ref parented directly on the root (e.g.
52
+ // app.html.appendHead: {_ref: head.html}) has no collected ancestor and
53
+ // would be missed by the descendant scan. Add the root ref id explicitly.
54
+ if (context.rootRefDef?.id != null) {
55
+ refIds.add(context.rootRefDef.id);
56
+ }
57
+ // Page refs are stop boundaries for the descendant scan: with the root id
58
+ // collected, a scalar ref'd inside a page file would otherwise reach the
59
+ // root through its page ref and wrongly become a skeleton source.
60
+ const pageRefIds = new Set();
61
+ walkRefIds(components.pages ?? [], pageRefIds);
50
62
  const sourceFiles = new Set();
51
63
  // Walk parent chains for each collected ref ID
52
64
  for (const refId of refIds){
@@ -65,6 +77,7 @@ function collectSkeletonSourceFiles({ components, context }) {
65
77
  // their paths if any ancestor in their parent chain is a collected ref ID.
66
78
  for (const [id, entry] of Object.entries(context.refMap)){
67
79
  if (refIds.has(id)) continue;
80
+ if (pageRefIds.has(id)) continue;
68
81
  let current = entry.parent;
69
82
  while(current != null){
70
83
  if (refIds.has(current)) {
@@ -73,6 +86,7 @@ function collectSkeletonSourceFiles({ components, context }) {
73
86
  }
74
87
  break;
75
88
  }
89
+ if (pageRefIds.has(current)) break;
76
90
  const parentEntry = context.refMap[current];
77
91
  if (!parentEntry) break;
78
92
  current = parentEntry.parent;
@@ -22,6 +22,7 @@ const PAGE_CONTENT_KEYS = [
22
22
  'slots',
23
23
  'events',
24
24
  'requests',
25
+ 'subscriptions',
25
26
  'layout'
26
27
  ];
27
28
  export default PAGE_CONTENT_KEYS;
@@ -21,17 +21,25 @@ import tryBuildStep from '../../utils/tryBuildStep.js';
21
21
  import addDefaultPages from '../addDefaultPages/addDefaultPages.js';
22
22
  import addKeys from '../addKeys.js';
23
23
  import buildApp from '../buildApp.js';
24
+ import buildAppMeta from '../buildAppMeta.js';
24
25
  import buildAuth from '../buildAuth/buildAuth.js';
25
26
  import buildConnections from '../buildConnections.js';
26
27
  import buildAgents from '../buildAgents.js';
27
28
  import buildApi from '../buildApi/buildApi.js';
28
29
  import buildLogger from '../buildLogger.js';
29
30
  import buildImports from '../buildImports/buildImports.js';
31
+ import buildMcp from '../buildMcp.js';
30
32
  import buildMenu from '../buildMenu.js';
31
33
  import buildModuleDefs from '../buildModuleDefs.js';
32
34
  import buildModules from '../buildModules.js';
35
+ import buildNotifications from '../buildNotifications.js';
33
36
  import buildRefs from '../buildRefs/buildRefs.js';
37
+ import precomputeRuntimeOperators from '../buildRefs/precomputeRuntimeOperators.js';
38
+ import { serializeRegistry } from '../buildRefs/deferredRegistry.js';
34
39
  import buildTypes from '../buildTypes.js';
40
+ import buildWebsockets from '../buildWebsockets.js';
41
+ import validateCallAgentSteps from '../validateCallAgentSteps.js';
42
+ import validateRenderNotificationSteps from '../validateRenderNotificationSteps.js';
35
43
  import cleanBuildDirectory from '../cleanBuildDirectory.js';
36
44
  import copyAgentFileSystems from '../copyAgentFileSystems.js';
37
45
  import copyPublicFolder from '../copyPublicFolder.js';
@@ -44,8 +52,11 @@ import writeConfig from '../writeConfig.js';
44
52
  import writeConnections from '../writeConnections.js';
45
53
  import writeAgents from '../writeAgents.js';
46
54
  import writeApi from '../writeApi.js';
55
+ import writeMcp from '../writeMcp.js';
56
+ import writeNotifications from '../writeNotifications.js';
47
57
  import writeGlobal from '../writeGlobal.js';
48
58
  import writeJs from '../buildJs/writeJs.js';
59
+ import writeWebsockets from '../writeWebsockets.js';
49
60
  import writeLogger from '../writeLogger.js';
50
61
  import codegenI18nLocales from '../codegenI18nLocales.js';
51
62
  import writeI18n from '../writeI18n.js';
@@ -53,6 +64,7 @@ import writeTheme from '../writeTheme.js';
53
64
  import writeMaps from '../writeMaps.js';
54
65
  import updateServerPackageJson from '../full/updateServerPackageJson.js';
55
66
  import writeMenus from '../writeMenus.js';
67
+ import writeTypes from '../full/writeTypes.js';
56
68
  import writePageRegistry from './writePageRegistry.js';
57
69
  import writePluginImports from '../writePluginImports/writePluginImports.js';
58
70
  import addInstalledTypes from './addInstalledTypes.js';
@@ -66,6 +78,12 @@ async function shallowBuild(options) {
66
78
  let context;
67
79
  try {
68
80
  context = createContext(options);
81
+ // Phase 0: Resolve root app metadata before any operator evaluation.
82
+ await buildAppMeta({
83
+ context
84
+ });
85
+ // Surface bad root metadata before module operators evaluate against it.
86
+ logCollectedErrors(context);
69
87
  // Phase 1: Build module definitions
70
88
  await buildModuleDefs({
71
89
  context
@@ -98,6 +116,18 @@ async function shallowBuild(options) {
98
116
  components,
99
117
  context
100
118
  });
119
+ // Phase 3.5: Constant-fold static runtime operators, mirroring the full
120
+ // build (index.js). Without this, content preserved at skeleton — inline
121
+ // pages, slot content, module components consumed into them — reaches
122
+ // testSchema (spurious warnings) and the served dev artifacts (broken
123
+ // block ids: the client never operator-evaluates id positions) with raw
124
+ // runtime operators. Ref-backed page content is already stripped here and
125
+ // folds per page in buildPageJit.
126
+ components = precomputeRuntimeOperators({
127
+ context,
128
+ input: components,
129
+ refDef: context.rootRefDef
130
+ });
101
131
  // addKeys + testSchema first for error location info
102
132
  tryBuildStep(addKeys, 'addKeys', {
103
133
  components,
@@ -124,6 +154,9 @@ async function shallowBuild(options) {
124
154
  components,
125
155
  context
126
156
  });
157
+ // appMeta is computed in Phase 0; attach it here (where buildApp used to
158
+ // create it) so the following addKeys pass keys it identically.
159
+ components.appMeta = context.appMeta;
127
160
  tryBuildStep(buildLogger, 'buildLogger', {
128
161
  components,
129
162
  context
@@ -156,6 +189,28 @@ async function shallowBuild(options) {
156
189
  components,
157
190
  context
158
191
  });
192
+ tryBuildStep(buildMcp, 'buildMcp', {
193
+ components,
194
+ context
195
+ });
196
+ tryBuildStep(buildWebsockets, 'buildWebsockets', {
197
+ components,
198
+ context
199
+ });
200
+ tryBuildStep(buildNotifications, 'buildNotifications', {
201
+ components,
202
+ context
203
+ });
204
+ // Cross-config validations — need buildApi (stepIds) and the buildAgents/
205
+ // buildNotifications id sets. Match the full build (index.js).
206
+ tryBuildStep(validateCallAgentSteps, 'validateCallAgentSteps', {
207
+ components,
208
+ context
209
+ });
210
+ tryBuildStep(validateRenderNotificationSteps, 'validateRenderNotificationSteps', {
211
+ components,
212
+ context
213
+ });
159
214
  const { pageRegistry, sourcelessPageArtifacts } = buildShallowPages({
160
215
  components,
161
216
  context
@@ -175,7 +230,7 @@ async function shallowBuild(options) {
175
230
  // Update server package.json before addInstalledTypes so that addInstalledTypes
176
231
  // sees the full set of dependencies on every run (not just after the first build).
177
232
  // This prevents plugin import files from differing between the initial and
178
- // subsequent builds, which would trigger unnecessary Next.js rebuilds.
233
+ // subsequent builds, which would trigger unnecessary server restarts.
179
234
  await updateServerPackageJson({
180
235
  components,
181
236
  context
@@ -221,10 +276,22 @@ async function shallowBuild(options) {
221
276
  components,
222
277
  context
223
278
  });
279
+ await writeMcp({
280
+ components,
281
+ context
282
+ });
224
283
  await writeAgents({
225
284
  components,
226
285
  context
227
286
  });
287
+ await writeWebsockets({
288
+ components,
289
+ context
290
+ });
291
+ await writeNotifications({
292
+ components,
293
+ context
294
+ });
228
295
  await writeConfig({
229
296
  components,
230
297
  context
@@ -255,6 +322,9 @@ async function shallowBuild(options) {
255
322
  await context.writeBuildArtifact('connectionIds.json', JSON.stringify([
256
323
  ...context.connectionIds
257
324
  ].sort()));
325
+ await context.writeBuildArtifact('websocketIds.json', JSON.stringify([
326
+ ...context.websocketIds
327
+ ].sort()));
258
328
  await context.writeBuildArtifact('skeletonSourceFiles.json', JSON.stringify([
259
329
  ...skeletonSourceFiles
260
330
  ].sort()));
@@ -262,6 +332,13 @@ async function shallowBuild(options) {
262
332
  components,
263
333
  context
264
334
  });
335
+ // The dev client bundle imports every installed type (addInstalledTypes),
336
+ // so types.json here describes that full bundle — dynamic page content
337
+ // resolution validates fragment types against it at page get.
338
+ await writeTypes({
339
+ components,
340
+ context
341
+ });
265
342
  await writeJs({
266
343
  context
267
344
  });
@@ -276,6 +353,9 @@ async function shallowBuild(options) {
276
353
  ...context.installedPackages ?? []
277
354
  ]));
278
355
  await context.writeBuildArtifact('modules.json', serializer.serializeToString(context.modules ?? {}));
356
+ // Deferred-record bodies referenced by placeholders in modules.json.
357
+ // JIT hydrates the registry from this artifact (hydrateDeferredRecords).
358
+ await context.writeBuildArtifact('deferredRecords.json', serializeRegistry(context));
279
359
  await writePluginImports({
280
360
  components,
281
361
  context
@@ -14,10 +14,9 @@
14
14
  limitations under the License.
15
15
  */ import path from 'path';
16
16
  import { serializer, type } from '@lowdefy/helpers';
17
- import { writeFile } from '@lowdefy/node-utils';
18
- import collectPageContent from '../collectPageContent.js';
17
+ import { writeFileIfChanged } from '@lowdefy/node-utils';
19
18
  import writeJs from '../buildJs/writeJs.js';
20
- async function writePageJit({ page, context }) {
19
+ async function writePageJit({ page, context, tailwindContent }) {
21
20
  // Write page JSON
22
21
  await context.writeBuildArtifact(`pages/${page.pageId}.json`, serializer.serializeToString(page ?? {}));
23
22
  // Write page request JSONs
@@ -43,10 +42,14 @@ async function writePageJit({ page, context }) {
43
42
  await writeJs({
44
43
  context
45
44
  });
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', `${encodeURIComponent(page.pageId)}.html`), '<!-- Generated by Lowdefy build -->\n' + (pageContent ?? ''));
45
+ // Write per-page content file for Tailwind to scan class and property strings.
46
+ // Content is collected by the caller before _js extraction so class
47
+ // candidates inside _js source are included. Skipping unchanged content lets
48
+ // the caller avoid touching the CSS recompilation trigger
49
+ // (tailwind-candidates.css) for builds that introduced no new class candidates.
50
+ const tailwindChanged = await writeFileIfChanged(path.join(context.directories.server, 'lowdefy-build', 'tailwind', `${encodeURIComponent(page.pageId)}.html`), '<!-- Generated by Lowdefy build -->\n' + (tailwindContent ?? ''));
51
+ return {
52
+ tailwindChanged
53
+ };
51
54
  }
52
55
  export default writePageJit;