@lowdefy/server-dev 4.5.1 → 4.6.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 (73) hide show
  1. package/{manager/utils/createLogger.mjs → lib/build/app.js} +4 -18
  2. package/lib/build/auth.js +19 -0
  3. package/lib/build/config.js +19 -0
  4. package/lib/build/logger.js +19 -0
  5. package/lib/client/App.js +25 -21
  6. package/lib/client/BuildErrorPage.js +91 -0
  7. package/lib/client/BuildingPage.js +74 -0
  8. package/lib/client/InstallingPluginsPage.js +50 -0
  9. package/lib/client/Page.js +20 -2
  10. package/lib/client/Reload.js +1 -1
  11. package/lib/client/RestartingPage.js +3 -1
  12. package/lib/client/auth/Auth.js +2 -2
  13. package/lib/client/auth/AuthConfigured.js +2 -2
  14. package/lib/client/auth/AuthNotConfigured.js +1 -1
  15. package/lib/client/setPageId.js +1 -1
  16. package/lib/client/utils/request.js +2 -5
  17. package/lib/client/utils/useMutateCache.js +16 -3
  18. package/lib/client/utils/usePageConfig.js +51 -5
  19. package/lib/client/utils/useRootConfig.js +1 -1
  20. package/lib/client/utils/waitForRestartedServer.js +1 -1
  21. package/lib/server/apiWrapper.js +48 -8
  22. package/lib/server/auth/getAuthOptions.js +2 -2
  23. package/lib/server/auth/getMockSession.js +67 -0
  24. package/lib/server/auth/getServerSession.js +10 -3
  25. package/lib/server/fileCache.js +1 -1
  26. package/lib/server/jitPageBuilder.js +162 -0
  27. package/lib/server/log/createHandleError.js +47 -0
  28. package/lib/server/log/createLogger.js +4 -4
  29. package/lib/server/log/logRequest.js +1 -1
  30. package/lib/server/pageCache.mjs +61 -0
  31. package/lib/server/pageCache.test.mjs +80 -0
  32. package/manager/getContext.mjs +26 -4
  33. package/manager/processes/checkMockUserWarning.mjs +42 -0
  34. package/manager/processes/initialBuild.mjs +2 -1
  35. package/manager/processes/installPlugins.mjs +3 -3
  36. package/manager/processes/lowdefyBuild.mjs +16 -5
  37. package/manager/processes/nextBuild.mjs +27 -8
  38. package/manager/processes/readDotEnv.mjs +1 -1
  39. package/manager/processes/reloadClients.mjs +1 -1
  40. package/manager/processes/restartServer.mjs +3 -3
  41. package/manager/processes/shutdownServer.mjs +2 -2
  42. package/manager/processes/startServer.mjs +32 -15
  43. package/manager/processes/startWatchers.mjs +1 -1
  44. package/manager/run.mjs +4 -12
  45. package/manager/utils/BatchChanges.mjs +1 -1
  46. package/manager/utils/checkPortAvailable.mjs +41 -0
  47. package/manager/utils/createCustomPluginTypesMap.mjs +1 -1
  48. package/manager/utils/getLowdefyVersion.mjs +8 -7
  49. package/manager/utils/getNextBin.mjs +1 -1
  50. package/manager/utils/setupWatcher.mjs +1 -1
  51. package/manager/watchers/envWatcher.mjs +1 -1
  52. package/manager/watchers/lowdefyBuildWatcher.mjs +26 -5
  53. package/manager/watchers/nextBuildWatcher.mjs +12 -2
  54. package/next.config.js +3 -2
  55. package/package.json +33 -29
  56. package/package.original.json +32 -28
  57. package/pages/404.js +1 -1
  58. package/pages/[pageId].js +1 -1
  59. package/pages/_app.js +17 -4
  60. package/pages/_document.js +3 -3
  61. package/pages/api/auth/[...nextauth].js +13 -2
  62. package/pages/api/client-error.js +45 -0
  63. package/pages/api/dev-tools.js +1 -1
  64. package/pages/api/endpoints/[endpointId].js +1 -1
  65. package/pages/api/js/[env].js +42 -0
  66. package/pages/api/page/[pageId].js +40 -2
  67. package/pages/api/ping.js +1 -1
  68. package/pages/api/reload.js +6 -3
  69. package/pages/api/request/[pageId]/[requestId].js +6 -3
  70. package/pages/api/root.js +1 -1
  71. package/pages/index.js +1 -1
  72. package/lib/server/log/logError.js +0 -40
  73. package/manager/utils/createStdOutLineHandler.mjs +0 -33
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -13,35 +13,68 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+ import fs from 'fs';
16
17
  import path from 'path';
17
18
  import { createApiContext } from '@lowdefy/api';
18
19
  import { getSecretsFromEnv } from '@lowdefy/node-utils';
20
+ import { serializer } from '@lowdefy/helpers';
19
21
  import { v4 as uuid } from 'uuid';
20
22
 
21
- import config from '../../build/config.json';
23
+ import config from '../build/config.js';
22
24
  import connections from '../../build/plugins/connections.js';
23
25
  import createLogger from './log/createLogger.js';
24
26
  import fileCache from './fileCache.js';
25
27
  import getServerSession from './auth/getServerSession.js';
26
- import logError from './log/logError.js';
28
+ import createHandleError from './log/createHandleError.js';
27
29
  import logRequest from './log/logRequest.js';
28
30
  import operators from '../../build/plugins/operators/server.js';
29
- import jsMap from '../../build/plugins/operators/serverJsMap.js';
31
+ import staticJsMap from '../../build/plugins/operators/serverJsMap.js';
30
32
  import getAuthOptions from './auth/getAuthOptions.js';
31
33
 
32
34
  const secrets = getSecretsFromEnv();
33
35
 
36
+ // Dynamic JS map loading for JIT-built pages
37
+ let cachedJsMapMtime = null;
38
+ let cachedJsMap = staticJsMap;
39
+
40
+ function loadDynamicJsMap(buildDirectory) {
41
+ const jsMapPath = path.join(buildDirectory, 'plugins', 'operators', 'serverJsMap.js');
42
+ try {
43
+ const stat = fs.statSync(jsMapPath);
44
+ if (cachedJsMapMtime && stat.mtimeMs === cachedJsMapMtime) {
45
+ return cachedJsMap;
46
+ }
47
+ cachedJsMapMtime = stat.mtimeMs;
48
+ // For server-side, we can read and eval the JS file
49
+ const content = fs.readFileSync(jsMapPath, 'utf8');
50
+ const fn = new Function('exports', content.replace('export default', 'exports.default ='));
51
+ const exports = {};
52
+ fn(exports);
53
+ cachedJsMap = { ...staticJsMap, ...(exports.default ?? {}) };
54
+ return cachedJsMap;
55
+ } catch {
56
+ return cachedJsMap;
57
+ }
58
+ }
59
+
34
60
  function apiWrapper(handler) {
35
61
  return async function wrappedHandler(req, res) {
62
+ const buildDirectory = path.join(process.cwd(), 'build');
63
+ const jsMap = loadDynamicJsMap(buildDirectory);
64
+
36
65
  const context = {
37
66
  // Important to give absolute path so Next can trace build files
38
67
  rid: uuid(),
39
- buildDirectory: path.join(process.cwd(), 'build'),
68
+ buildDirectory,
69
+ configDirectory: process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd(),
40
70
  config,
41
71
  connections,
42
72
  fileCache,
43
73
  headers: req?.headers,
44
74
  jsMap,
75
+ handleError: async (err) => {
76
+ console.error(err);
77
+ },
45
78
  logger: console,
46
79
  operators,
47
80
  req,
@@ -49,7 +82,8 @@ function apiWrapper(handler) {
49
82
  secrets,
50
83
  };
51
84
  try {
52
- context.logger = createLogger({ rid: context.rid });
85
+ context.logger = createLogger();
86
+ context.handleError = createHandleError({ context });
53
87
  context.authOptions = getAuthOptions(context);
54
88
  if (!req.url.startsWith('/api/auth')) {
55
89
  context.session = await getServerSession(context);
@@ -61,8 +95,14 @@ function apiWrapper(handler) {
61
95
  // TODO: Log response time?
62
96
  return response;
63
97
  } catch (error) {
64
- logError({ error, context });
65
- res.status(500).json({ name: error.name, message: error.message });
98
+ await context.handleError(error);
99
+ const serialized = serializer.serialize(error);
100
+ if (serialized?.['~e']) {
101
+ delete serialized['~e'].received;
102
+ delete serialized['~e'].stack;
103
+ delete serialized['~e'].configKey;
104
+ }
105
+ res.status(500).json(serialized);
66
106
  }
67
107
  };
68
108
  }
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -18,7 +18,7 @@ import { getNextAuthConfig } from '@lowdefy/api';
18
18
  import { getSecretsFromEnv } from '@lowdefy/node-utils';
19
19
 
20
20
  import adapters from '../../../build/plugins/auth/adapters.js';
21
- import authJson from '../../../build/auth.json';
21
+ import authJson from '../../build/auth.js';
22
22
  import callbacks from '../../../build/plugins/auth/callbacks.js';
23
23
  import events from '../../../build/plugins/auth/events.js';
24
24
  import providers from '../../../build/plugins/auth/providers.js';
@@ -0,0 +1,67 @@
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
+ */
16
+
17
+ import { createSessionCallback } from '@lowdefy/api';
18
+ import { serializer } from '@lowdefy/helpers';
19
+
20
+ import authJson from '../../build/auth.js';
21
+ import callbacks from '../../../build/plugins/auth/callbacks.js';
22
+
23
+ async function getMockSession() {
24
+ const mockUserJson = process.env.LOWDEFY_DEV_USER;
25
+ let mockUser;
26
+
27
+ if (mockUserJson) {
28
+ try {
29
+ mockUser = JSON.parse(mockUserJson);
30
+ } catch (error) {
31
+ throw new Error('Invalid JSON in LOWDEFY_DEV_USER environment variable.', { cause: error });
32
+ }
33
+ } else {
34
+ mockUser = authJson.dev?.mockUser;
35
+ }
36
+
37
+ if (!mockUser) {
38
+ return undefined;
39
+ }
40
+
41
+ // Deserialize to restore arrays from ~arr markers and remove other build markers
42
+ mockUser = serializer.deserialize(mockUser);
43
+
44
+ if (authJson.configured !== true) {
45
+ throw new Error(
46
+ 'Mock user configured but auth is not configured in lowdefy.yaml. ' +
47
+ 'Add auth configuration to use mock user feature.'
48
+ );
49
+ }
50
+
51
+ // Create session callback to transform mock user
52
+ const sessionCallback = createSessionCallback({
53
+ authConfig: authJson,
54
+ plugins: { callbacks },
55
+ });
56
+
57
+ // Transform mock user through session callback (mock user acts as token)
58
+ const session = await sessionCallback({
59
+ session: { user: {} },
60
+ token: mockUser,
61
+ user: mockUser,
62
+ });
63
+
64
+ return session;
65
+ }
66
+
67
+ export default getMockSession;
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -16,9 +16,16 @@
16
16
 
17
17
  import { getServerSession as getNextAuthServerSession } from 'next-auth/next';
18
18
 
19
- import authJson from '../../../build/auth.json';
19
+ import authJson from '../../build/auth.js';
20
+ import getMockSession from './getMockSession.js';
21
+
22
+ async function getServerSession({ authOptions, req, res }) {
23
+ // Check for mock user first (dev server only)
24
+ const mockSession = await getMockSession();
25
+ if (mockSession) {
26
+ return mockSession;
27
+ }
20
28
 
21
- function getServerSession({ authOptions, req, res }) {
22
29
  if (authJson.configured === true) {
23
30
  return getNextAuthServerSession(req, res, authOptions);
24
31
  }
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -0,0 +1,162 @@
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
+ */
16
+
17
+ import fs from 'fs';
18
+ import path from 'path';
19
+ import { serializer } from '@lowdefy/helpers';
20
+ import { buildPageJit, createContext } from '@lowdefy/build/dev';
21
+
22
+ import createLogger from './log/createLogger.js';
23
+ import PageCache from './pageCache.mjs';
24
+
25
+ const jitLogger = createLogger({ name: 'jit-build' });
26
+
27
+ function formatDuration(ms) {
28
+ if (ms < 1000) return `${ms}ms`;
29
+ return `${(ms / 1000).toFixed(2)}s`;
30
+ }
31
+ const pageCache = new PageCache();
32
+ let cachedRegistryMtime = null;
33
+ let cachedRegistry = null;
34
+ let cachedBuildContext = null;
35
+ let lastInvalidationMtime = null;
36
+
37
+ function readJsonFile(filePath) {
38
+ try {
39
+ const content = fs.readFileSync(filePath, 'utf8');
40
+ return serializer.deserialize(JSON.parse(content));
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function checkPageInvalidations(buildDirectory) {
47
+ const invalidatePath = path.join(buildDirectory, 'invalidatePages');
48
+ try {
49
+ const stat = fs.statSync(invalidatePath);
50
+ if (lastInvalidationMtime && stat.mtimeMs === lastInvalidationMtime) {
51
+ return;
52
+ }
53
+ lastInvalidationMtime = stat.mtimeMs;
54
+ pageCache.invalidateAll();
55
+ cachedBuildContext = null;
56
+ } catch {
57
+ // File doesn't exist yet — nothing to invalidate
58
+ }
59
+ }
60
+
61
+ function loadPageRegistry(buildDirectory) {
62
+ const registryPath = path.join(buildDirectory, 'pageRegistry.json');
63
+ try {
64
+ const stat = fs.statSync(registryPath);
65
+ // Only reload if file has changed
66
+ if (cachedRegistryMtime && stat.mtimeMs === cachedRegistryMtime) {
67
+ return cachedRegistry;
68
+ }
69
+ cachedRegistryMtime = stat.mtimeMs;
70
+ cachedRegistry = readJsonFile(registryPath);
71
+ // Invalidate all pages when registry changes (skeleton rebuild happened)
72
+ pageCache.invalidateAll();
73
+ cachedBuildContext = null;
74
+ return cachedRegistry;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function getBuildContext(buildDirectory, configDirectory) {
81
+ if (cachedBuildContext) return cachedBuildContext;
82
+
83
+ const refMap = readJsonFile(path.join(buildDirectory, 'refMap.json')) ?? {};
84
+ const keyMap = readJsonFile(path.join(buildDirectory, 'keyMap.json')) ?? {};
85
+ const jsMap = readJsonFile(path.join(buildDirectory, 'jsMap.json')) ?? { client: {}, server: {} };
86
+ const connectionIds = readJsonFile(path.join(buildDirectory, 'connectionIds.json')) ?? [];
87
+
88
+ const customTypesMap = readJsonFile(path.join(buildDirectory, 'customTypesMap.json')) ?? {};
89
+
90
+ cachedBuildContext = createContext({
91
+ customTypesMap,
92
+ directories: {
93
+ build: buildDirectory,
94
+ config: configDirectory,
95
+ server: path.resolve(buildDirectory, '..'),
96
+ },
97
+ logger: jitLogger,
98
+ stage: 'dev',
99
+ });
100
+
101
+ // Restore refMap, keyMap, jsMap, and connectionIds from skeleton build
102
+ Object.assign(cachedBuildContext.refMap, refMap);
103
+ Object.assign(cachedBuildContext.keyMap, keyMap);
104
+ cachedBuildContext.jsMap.client = jsMap.client ?? {};
105
+ cachedBuildContext.jsMap.server = jsMap.server ?? {};
106
+ for (const id of connectionIds) {
107
+ cachedBuildContext.connectionIds.add(id);
108
+ }
109
+
110
+ // Load installed packages snapshot from skeleton build for missing-package detection
111
+ const installedPluginPackages =
112
+ readJsonFile(path.join(buildDirectory, 'installedPluginPackages.json')) ?? [];
113
+ cachedBuildContext.installedPluginPackages = new Set(installedPluginPackages);
114
+
115
+ return cachedBuildContext;
116
+ }
117
+
118
+ async function buildPageIfNeeded({ pageId, buildDirectory, configDirectory }) {
119
+ checkPageInvalidations(buildDirectory);
120
+ const registry = loadPageRegistry(buildDirectory);
121
+ if (!registry || !registry[pageId]) {
122
+ return false;
123
+ }
124
+
125
+ if (pageCache.isCompiled(pageId)) {
126
+ return true;
127
+ }
128
+
129
+ const shouldBuild = await pageCache.acquireBuildLock(pageId);
130
+ if (!shouldBuild) {
131
+ // Another request completed the build
132
+ return true;
133
+ }
134
+
135
+ jitLogger.info({ spin: 'start' }, `Building page "${pageId}"...`);
136
+ const startTime = Date.now();
137
+ try {
138
+ const context = getBuildContext(buildDirectory, configDirectory);
139
+ const result = await buildPageJit({
140
+ pageId,
141
+ pageRegistry: registry,
142
+ context,
143
+ });
144
+ if (result && result.installing) {
145
+ jitLogger.info(
146
+ `Installing plugin packages for page "${pageId}": ${result.packages.join(', ')}. ` +
147
+ 'The page will be available after the server restarts.'
148
+ );
149
+ return result;
150
+ }
151
+ pageCache.markCompiled(pageId);
152
+ jitLogger.info(
153
+ { spin: 'succeed', color: 'white' },
154
+ `Built page "${pageId}" in ${formatDuration(Date.now() - startTime)}.`
155
+ );
156
+ return true;
157
+ } finally {
158
+ pageCache.releaseBuildLock(pageId);
159
+ }
160
+ }
161
+
162
+ export default buildPageIfNeeded;
@@ -0,0 +1,47 @@
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
+ */
16
+
17
+ import { LowdefyInternalError, loadAndResolveErrorLocation } from '@lowdefy/errors';
18
+
19
+ function createHandleError({ context }) {
20
+ return async function handleError(error) {
21
+ try {
22
+ // For internal lowdefy errors, don't resolve config location
23
+ const location =
24
+ error instanceof LowdefyInternalError
25
+ ? null
26
+ : await loadAndResolveErrorLocation({
27
+ error,
28
+ readConfigFile: context.readConfigFile,
29
+ configDirectory: context.configDirectory,
30
+ });
31
+
32
+ // Attach resolved location to error for display layer
33
+ if (location) {
34
+ error.source = location.source;
35
+ error.config = location.config;
36
+ }
37
+
38
+ context.logger.error(error);
39
+ } catch (e) {
40
+ console.error(error);
41
+ console.error('An error occurred while logging the error.');
42
+ console.error(e);
43
+ }
44
+ };
45
+ }
46
+
47
+ export default createHandleError;
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -14,10 +14,10 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import pino from 'pino';
17
+ import { createNodeLogger } from '@lowdefy/logger/node';
18
18
 
19
- const logger = pino({
20
- name: 'lowdefy_server',
19
+ const logger = createNodeLogger({
20
+ name: 'lowdefy_server_dev',
21
21
  level: process.env.LOWDEFY_LOG_LEVEL ?? 'info',
22
22
  base: { pid: undefined, hostname: undefined },
23
23
  });
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -0,0 +1,61 @@
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
+ */
16
+
17
+ class PageCache {
18
+ constructor() {
19
+ this.compiledPages = new Set();
20
+ this.buildLocks = new Map();
21
+ }
22
+
23
+ isCompiled(pageId) {
24
+ return this.compiledPages.has(pageId);
25
+ }
26
+
27
+ markCompiled(pageId) {
28
+ this.compiledPages.add(pageId);
29
+ }
30
+
31
+ async acquireBuildLock(pageId) {
32
+ // If page build already in progress, wait for it
33
+ if (this.buildLocks.has(pageId)) {
34
+ await this.buildLocks.get(pageId);
35
+ return false;
36
+ }
37
+
38
+ // Create new lock
39
+ let resolve;
40
+ const promise = new Promise((r) => {
41
+ resolve = r;
42
+ });
43
+ promise.resolve = resolve;
44
+ this.buildLocks.set(pageId, promise);
45
+ return true;
46
+ }
47
+
48
+ releaseBuildLock(pageId) {
49
+ const lock = this.buildLocks.get(pageId);
50
+ if (lock) {
51
+ lock.resolve();
52
+ this.buildLocks.delete(pageId);
53
+ }
54
+ }
55
+
56
+ invalidateAll() {
57
+ this.compiledPages.clear();
58
+ }
59
+ }
60
+
61
+ export default PageCache;
@@ -0,0 +1,80 @@
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
+ */
16
+
17
+ import PageCache from './pageCache.mjs';
18
+
19
+ test('isCompiled returns false for uncompiled page', () => {
20
+ const cache = new PageCache();
21
+ expect(cache.isCompiled('home')).toBe(false);
22
+ });
23
+
24
+ test('isCompiled returns true after markCompiled', () => {
25
+ const cache = new PageCache();
26
+ cache.markCompiled('home');
27
+ expect(cache.isCompiled('home')).toBe(true);
28
+ });
29
+
30
+ test('invalidateAll clears all compiled pages', () => {
31
+ const cache = new PageCache();
32
+ cache.markCompiled('home');
33
+ cache.markCompiled('dashboard');
34
+ cache.invalidateAll();
35
+ expect(cache.isCompiled('home')).toBe(false);
36
+ expect(cache.isCompiled('dashboard')).toBe(false);
37
+ });
38
+
39
+ test('acquireBuildLock returns true for first request', async () => {
40
+ const cache = new PageCache();
41
+ const shouldBuild = await cache.acquireBuildLock('home');
42
+ expect(shouldBuild).toBe(true);
43
+ cache.releaseBuildLock('home');
44
+ });
45
+
46
+ test('acquireBuildLock returns false for concurrent request (waits for first)', async () => {
47
+ const cache = new PageCache();
48
+
49
+ // First request acquires lock
50
+ const shouldBuild1 = await cache.acquireBuildLock('home');
51
+ expect(shouldBuild1).toBe(true);
52
+
53
+ // Second request waits for lock and returns false
54
+ const promise2 = cache.acquireBuildLock('home');
55
+
56
+ // Release first lock
57
+ cache.releaseBuildLock('home');
58
+
59
+ const shouldBuild2 = await promise2;
60
+ expect(shouldBuild2).toBe(false);
61
+ });
62
+
63
+ test('acquireBuildLock for different pages does not block', async () => {
64
+ const cache = new PageCache();
65
+
66
+ const shouldBuild1 = await cache.acquireBuildLock('home');
67
+ const shouldBuild2 = await cache.acquireBuildLock('dashboard');
68
+
69
+ expect(shouldBuild1).toBe(true);
70
+ expect(shouldBuild2).toBe(true);
71
+
72
+ cache.releaseBuildLock('home');
73
+ cache.releaseBuildLock('dashboard');
74
+ });
75
+
76
+ test('releaseBuildLock does nothing for non-existent lock', () => {
77
+ const cache = new PageCache();
78
+ // Should not throw
79
+ cache.releaseBuildLock('nonexistent');
80
+ });
@@ -1,5 +1,5 @@
1
1
  /*
2
- Copyright 2020-2024 Lowdefy, Inc
2
+ Copyright 2020-2026 Lowdefy, Inc
3
3
 
4
4
  Licensed under the Apache License, Version 2.0 (the "License");
5
5
  you may not use this file except in compliance with the License.
@@ -19,7 +19,9 @@ import path from 'path';
19
19
  import yargs from 'yargs';
20
20
  import { hideBin } from 'yargs/helpers';
21
21
 
22
- import createLogger from './utils/createLogger.mjs';
22
+ import pino from 'pino';
23
+ import { createNodeLogger } from '@lowdefy/logger/node';
24
+ import checkMockUserWarning from './processes/checkMockUserWarning.mjs';
23
25
  import initialBuild from './processes/initialBuild.mjs';
24
26
  import installPlugins from './processes/installPlugins.mjs';
25
27
  import lowdefyBuild from './processes/lowdefyBuild.mjs';
@@ -46,7 +48,12 @@ async function getContext() {
46
48
  config: path.resolve(argv.configDirectory ?? env.LOWDEFY_DIRECTORY_CONFIG ?? process.cwd()),
47
49
  server: process.cwd(),
48
50
  },
49
- logger: createLogger({ level: env.LOWDEFY_LOG_LEVEL }),
51
+ logger: createNodeLogger({
52
+ name: 'lowdefy build',
53
+ level: env.LOWDEFY_LOG_LEVEL ?? 'info',
54
+ base: { pid: undefined, hostname: undefined },
55
+ destination: pino.destination({ dest: 1, sync: true }),
56
+ }),
50
57
  options: {
51
58
  port: argv.port ?? env.PORT ?? 3000,
52
59
  refResolver: argv.refResolver ?? env.LOWDEFY_BUILD_REF_RESOLVER,
@@ -58,12 +65,27 @@ async function getContext() {
58
65
  : [],
59
66
  },
60
67
  version: env.npm_package_version,
68
+
69
+ // JIT build state
70
+ pageRegistry: null,
71
+ buildContext: null,
61
72
  };
62
73
 
63
74
  context.packageManagerCmd = process.platform === 'win32' ? 'pnpm.cmd' : 'pnpm';
75
+ context.checkMockUserWarning = checkMockUserWarning(context);
64
76
  context.initialBuild = initialBuild(context);
65
77
  context.installPlugins = installPlugins(context);
66
- context.lowdefyBuild = lowdefyBuild(context);
78
+
79
+ // Wrap lowdefyBuild to capture and store the shallow build result
80
+ const buildFn = lowdefyBuild(context);
81
+ context.lowdefyBuild = async () => {
82
+ const result = await buildFn();
83
+ if (result) {
84
+ context.pageRegistry = result.pageRegistry;
85
+ context.buildContext = result.context;
86
+ }
87
+ };
88
+
67
89
  context.nextBuild = nextBuild(context);
68
90
  context.readDotEnv = readDotEnv(context);
69
91
  context.reloadClients = reloadClients(context);