@lowdefy/server-dev 0.0.0-experimental-20260220142815 → 0.0.0-experimental-20260220143146

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 (37) hide show
  1. package/lib/client/BuildErrorPage.js +4 -3
  2. package/lib/client/InstallingPluginsPage.js +50 -0
  3. package/lib/client/Page.js +4 -0
  4. package/lib/client/RestartingPage.js +2 -0
  5. package/lib/client/utils/request.js +1 -3
  6. package/lib/client/utils/usePageConfig.js +3 -0
  7. package/lib/server/apiWrapper.js +14 -10
  8. package/lib/server/auth/getMockSession.js +1 -1
  9. package/lib/server/jitPageBuilder.js +17 -1
  10. package/lib/server/log/createHandleError.js +47 -0
  11. package/lib/server/log/createLogger.js +2 -2
  12. package/lib/server/pageCache.mjs +0 -13
  13. package/lib/server/pageCache.test.mjs +0 -39
  14. package/manager/getContext.mjs +8 -4
  15. package/manager/processes/installPlugins.mjs +2 -2
  16. package/manager/processes/lowdefyBuild.mjs +2 -2
  17. package/manager/processes/nextBuild.mjs +4 -6
  18. package/manager/processes/restartServer.mjs +2 -2
  19. package/manager/processes/shutdownServer.mjs +1 -1
  20. package/manager/utils/checkPortAvailable.mjs +5 -4
  21. package/manager/utils/getLowdefyVersion.mjs +6 -12
  22. package/manager/watchers/lowdefyBuildWatcher.mjs +4 -24
  23. package/manager/watchers/nextBuildWatcher.mjs +12 -2
  24. package/next.config.js +1 -10
  25. package/package.json +30 -30
  26. package/package.original.json +30 -30
  27. package/pages/_app.js +3 -17
  28. package/pages/api/client-error.js +15 -10
  29. package/pages/api/page/[pageId].js +11 -3
  30. package/pages/api/request/[pageId]/[requestId].js +4 -1
  31. package/lib/client/sentry/captureSentryError.js +0 -43
  32. package/lib/client/sentry/initSentryClient.js +0 -55
  33. package/lib/client/sentry/setSentryUser.js +0 -41
  34. package/lib/server/log/logError.js +0 -56
  35. package/lib/server/sentry/captureSentryError.js +0 -57
  36. package/lib/server/sentry/initSentry.js +0 -44
  37. package/lib/server/sentry/setSentryUser.js +0 -46
@@ -21,7 +21,7 @@ const typeColors = {
21
21
  ConfigWarning: '#d48806',
22
22
  PluginError: '#531dab',
23
23
  ServiceError: '#096dd9',
24
- LowdefyError: '#cf1322',
24
+ LowdefyInternalError: '#cf1322',
25
25
  };
26
26
 
27
27
  function getTypeColor(type) {
@@ -49,7 +49,7 @@ const ErrorItem = ({ type, message, source }) => {
49
49
  >
50
50
  {type}
51
51
  </span>
52
- <p style={{ fontSize: 14, margin: '4px 0' }}>{message}</p>
52
+ <p style={{ fontSize: 14, margin: '4px 0', fontFamily: 'monospace' }}>{message}</p>
53
53
  {source && <p style={{ fontSize: 13, color: '#8c8c8c', margin: 0 }}>{source}</p>}
54
54
  </div>
55
55
  );
@@ -67,7 +67,8 @@ const BuildErrorPage = ({ errors, message, source }) => {
67
67
  flexDirection: 'column',
68
68
  alignItems: 'center',
69
69
  justifyContent: 'center',
70
- fontFamily: 'monospace',
70
+ fontFamily:
71
+ "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
71
72
  padding: '0 24px',
72
73
  }}
73
74
  >
@@ -0,0 +1,50 @@
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 React from 'react';
18
+
19
+ const InstallingPluginsPage = ({ packages }) => {
20
+ return (
21
+ <div
22
+ style={{
23
+ height: '100vh',
24
+ textAlign: 'center',
25
+ display: 'flex',
26
+ flexDirection: 'column',
27
+ alignItems: 'center',
28
+ justifyContent: 'center',
29
+ fontFamily:
30
+ "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
31
+ }}
32
+ >
33
+ <div
34
+ style={{
35
+ verticalAlign: 'middle',
36
+ display: 'inline-block',
37
+ }}
38
+ >
39
+ <h3>Installing Plugin Packages</h3>
40
+ <p>
41
+ This page requires packages that are not yet installed:{' '}
42
+ <strong>{(packages ?? []).join(', ')}</strong>.
43
+ </p>
44
+ <p>The server will restart automatically. The page will reload when ready.</p>
45
+ </div>
46
+ </div>
47
+ );
48
+ };
49
+
50
+ export default InstallingPluginsPage;
@@ -18,6 +18,7 @@ import React from 'react';
18
18
  import Client from '@lowdefy/client';
19
19
 
20
20
  import BuildErrorPage from './BuildErrorPage.js';
21
+ import InstallingPluginsPage from './InstallingPluginsPage.js';
21
22
  import RestartingPage from './RestartingPage.js';
22
23
  import usePageConfig from './utils/usePageConfig.js';
23
24
 
@@ -47,6 +48,9 @@ const Page = ({
47
48
  />
48
49
  );
49
50
  }
51
+ if (pageConfig.installing) {
52
+ return <InstallingPluginsPage packages={pageConfig.packages} />;
53
+ }
50
54
  if (resetContext.restarting) {
51
55
  return <RestartingPage />;
52
56
  }
@@ -26,6 +26,8 @@ const RestartingPage = () => {
26
26
  flexDirection: 'column',
27
27
  alignItems: 'center',
28
28
  justifyContent: 'center',
29
+ fontFamily:
30
+ "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif",
29
31
  }}
30
32
  >
31
33
  <div
@@ -27,9 +27,7 @@ async function request({ url, method = 'GET', body }) {
27
27
  }
28
28
  if (!res.ok) {
29
29
  const body = await res.json();
30
- console.log(res);
31
- console.log(body);
32
- throw new Error(body.message || 'Request error');
30
+ throw new Error(body?.['~e']?.message ?? body?.message ?? 'Request error');
33
31
  }
34
32
  return res.json();
35
33
  }
@@ -43,6 +43,9 @@ async function fetchPageConfig(url) {
43
43
  if (data?.buildError) {
44
44
  return data;
45
45
  }
46
+ if (data?.installing) {
47
+ return data;
48
+ }
46
49
  if (!res.ok) {
47
50
  throw new Error(data.message || 'Request error');
48
51
  }
@@ -17,6 +17,7 @@ import fs from 'fs';
17
17
  import path from 'path';
18
18
  import { createApiContext } from '@lowdefy/api';
19
19
  import { getSecretsFromEnv } from '@lowdefy/node-utils';
20
+ import { serializer } from '@lowdefy/helpers';
20
21
  import { v4 as uuid } from 'uuid';
21
22
 
22
23
  import config from '../build/config.js';
@@ -24,13 +25,11 @@ import connections from '../../build/plugins/connections.js';
24
25
  import createLogger from './log/createLogger.js';
25
26
  import fileCache from './fileCache.js';
26
27
  import getServerSession from './auth/getServerSession.js';
27
- import logError from './log/logError.js';
28
+ import createHandleError from './log/createHandleError.js';
28
29
  import logRequest from './log/logRequest.js';
29
30
  import operators from '../../build/plugins/operators/server.js';
30
31
  import staticJsMap from '../../build/plugins/operators/serverJsMap.js';
31
32
  import getAuthOptions from './auth/getAuthOptions.js';
32
- import loggerConfig from '../build/logger.js';
33
- import setSentryUser from './sentry/setSentryUser.js';
34
33
 
35
34
  const secrets = getSecretsFromEnv();
36
35
 
@@ -73,6 +72,9 @@ function apiWrapper(handler) {
73
72
  fileCache,
74
73
  headers: req?.headers,
75
74
  jsMap,
75
+ handleError: async (err) => {
76
+ console.error(err);
77
+ },
76
78
  logger: console,
77
79
  operators,
78
80
  req,
@@ -81,14 +83,10 @@ function apiWrapper(handler) {
81
83
  };
82
84
  try {
83
85
  context.logger = createLogger({ rid: context.rid });
86
+ context.handleError = createHandleError({ context });
84
87
  context.authOptions = getAuthOptions(context);
85
88
  if (!req.url.startsWith('/api/auth')) {
86
89
  context.session = await getServerSession(context);
87
- // Set Sentry user context for authenticated requests
88
- setSentryUser({
89
- user: context.session?.user,
90
- sentryConfig: loggerConfig.sentry,
91
- });
92
90
  }
93
91
  createApiContext(context);
94
92
  logRequest({ context });
@@ -97,8 +95,14 @@ function apiWrapper(handler) {
97
95
  // TODO: Log response time?
98
96
  return response;
99
97
  } catch (error) {
100
- await logError({ error, context });
101
- 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);
102
106
  }
103
107
  };
104
108
  }
@@ -28,7 +28,7 @@ async function getMockSession() {
28
28
  try {
29
29
  mockUser = JSON.parse(mockUserJson);
30
30
  } catch (error) {
31
- throw new Error(`Invalid JSON in LOWDEFY_DEV_USER environment variable: ${error.message}`);
31
+ throw new Error('Invalid JSON in LOWDEFY_DEV_USER environment variable.', { cause: error });
32
32
  }
33
33
  } else {
34
34
  mockUser = authJson.dev?.mockUser;
@@ -85,10 +85,14 @@ function getBuildContext(buildDirectory, configDirectory) {
85
85
  const jsMap = readJsonFile(path.join(buildDirectory, 'jsMap.json')) ?? { client: {}, server: {} };
86
86
  const connectionIds = readJsonFile(path.join(buildDirectory, 'connectionIds.json')) ?? [];
87
87
 
88
+ const customTypesMap = readJsonFile(path.join(buildDirectory, 'customTypesMap.json')) ?? {};
89
+
88
90
  cachedBuildContext = createContext({
91
+ customTypesMap,
89
92
  directories: {
90
93
  build: buildDirectory,
91
94
  config: configDirectory,
95
+ server: path.resolve(buildDirectory, '..'),
92
96
  },
93
97
  logger: jitLogger,
94
98
  stage: 'dev',
@@ -103,6 +107,11 @@ function getBuildContext(buildDirectory, configDirectory) {
103
107
  cachedBuildContext.connectionIds.add(id);
104
108
  }
105
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
+
106
115
  return cachedBuildContext;
107
116
  }
108
117
 
@@ -126,11 +135,18 @@ async function buildPageIfNeeded({ pageId, buildDirectory, configDirectory }) {
126
135
  try {
127
136
  const context = getBuildContext(buildDirectory, configDirectory);
128
137
  const startTime = Date.now();
129
- await buildPageJit({
138
+ const result = await buildPageJit({
130
139
  pageId,
131
140
  pageRegistry: registry,
132
141
  context,
133
142
  });
143
+ if (result && result.installing) {
144
+ jitLogger.info(
145
+ `Installing plugin packages for page "${pageId}": ${result.packages.join(', ')}. ` +
146
+ 'The page will be available after the server restarts.'
147
+ );
148
+ return result;
149
+ }
134
150
  pageCache.markCompiled(pageId);
135
151
  jitLogger.info(`Built page "${pageId}" in ${Date.now() - startTime}ms.`);
136
152
  return true;
@@ -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;
@@ -14,7 +14,7 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import { createNodeLogger, wrapErrorLogger } from '@lowdefy/logger/node';
17
+ import { createNodeLogger } from '@lowdefy/logger/node';
18
18
 
19
19
  const logger = createNodeLogger({
20
20
  name: 'lowdefy_server',
@@ -23,7 +23,7 @@ const logger = createNodeLogger({
23
23
  });
24
24
 
25
25
  function createLogger(metadata = {}) {
26
- return wrapErrorLogger(logger.child(metadata));
26
+ return logger.child(metadata);
27
27
  }
28
28
 
29
29
  export default createLogger;
@@ -86,19 +86,6 @@ class PageCache {
86
86
  }
87
87
  }
88
88
 
89
- invalidateByFiles(changedFiles, fileDependencyMap) {
90
- const affectedPages = new Set();
91
- for (const filePath of changedFiles) {
92
- const pageIds = fileDependencyMap.get(filePath);
93
- if (pageIds) {
94
- for (const pageId of pageIds) {
95
- affectedPages.add(pageId);
96
- }
97
- }
98
- }
99
- this.invalidatePages(affectedPages);
100
- return affectedPages;
101
- }
102
89
  }
103
90
 
104
91
  export default PageCache;
@@ -47,45 +47,6 @@ test('invalidatePages removes specific pages', () => {
47
47
  expect(cache.isCompiled('settings')).toBe(false);
48
48
  });
49
49
 
50
- test('invalidateByFiles returns affected pages and invalidates them', () => {
51
- const cache = new PageCache();
52
- cache.markCompiled('home');
53
- cache.markCompiled('dashboard');
54
-
55
- const fileDependencyMap = new Map([
56
- ['pages/home/blocks.yaml', new Set(['home'])],
57
- ['shared/layout.yaml', new Set(['home', 'dashboard'])],
58
- ]);
59
-
60
- const affected = cache.invalidateByFiles(['pages/home/blocks.yaml'], fileDependencyMap);
61
- expect(affected).toEqual(new Set(['home']));
62
- expect(cache.isCompiled('home')).toBe(false);
63
- expect(cache.isCompiled('dashboard')).toBe(true);
64
- });
65
-
66
- test('invalidateByFiles handles shared files affecting multiple pages', () => {
67
- const cache = new PageCache();
68
- cache.markCompiled('home');
69
- cache.markCompiled('dashboard');
70
-
71
- const fileDependencyMap = new Map([['shared/layout.yaml', new Set(['home', 'dashboard'])]]);
72
-
73
- const affected = cache.invalidateByFiles(['shared/layout.yaml'], fileDependencyMap);
74
- expect(affected).toEqual(new Set(['home', 'dashboard']));
75
- expect(cache.isCompiled('home')).toBe(false);
76
- expect(cache.isCompiled('dashboard')).toBe(false);
77
- });
78
-
79
- test('invalidateByFiles returns empty set for unknown files', () => {
80
- const cache = new PageCache();
81
- cache.markCompiled('home');
82
-
83
- const fileDependencyMap = new Map();
84
- const affected = cache.invalidateByFiles(['unknown.yaml'], fileDependencyMap);
85
- expect(affected).toEqual(new Set());
86
- expect(cache.isCompiled('home')).toBe(true);
87
- });
88
-
89
50
  test('acquireBuildLock returns true for first request', async () => {
90
51
  const cache = new PageCache();
91
52
  const shouldBuild = await cache.acquireBuildLock('home');
@@ -19,7 +19,8 @@ import path from 'path';
19
19
  import yargs from 'yargs';
20
20
  import { hideBin } from 'yargs/helpers';
21
21
 
22
- import { createDevLogger as createLogger } from '@lowdefy/logger/dev';
22
+ import pino from 'pino';
23
+ import { createNodeLogger } from '@lowdefy/logger/node';
23
24
  import checkMockUserWarning from './processes/checkMockUserWarning.mjs';
24
25
  import initialBuild from './processes/initialBuild.mjs';
25
26
  import installPlugins from './processes/installPlugins.mjs';
@@ -48,7 +49,12 @@ async function getContext() {
48
49
  config: path.resolve(argv.configDirectory ?? env.LOWDEFY_DIRECTORY_CONFIG ?? process.cwd()),
49
50
  server: process.cwd(),
50
51
  },
51
- logger: createLogger({ level: env.LOWDEFY_LOG_LEVEL }),
52
+ logger: createNodeLogger({
53
+ name: 'lowdefy build',
54
+ level: env.LOWDEFY_LOG_LEVEL ?? 'info',
55
+ base: { pid: undefined, hostname: undefined },
56
+ destination: pino.destination({ dest: 1, sync: true }),
57
+ }),
52
58
  options: {
53
59
  port: argv.port ?? env.PORT ?? 3000,
54
60
  refResolver: argv.refResolver ?? env.LOWDEFY_BUILD_REF_RESOLVER,
@@ -64,7 +70,6 @@ async function getContext() {
64
70
  // JIT build state
65
71
  pageCache: new PageCache(),
66
72
  pageRegistry: null,
67
- fileDependencyMap: null,
68
73
  buildContext: null,
69
74
  };
70
75
 
@@ -79,7 +84,6 @@ async function getContext() {
79
84
  const result = await buildFn();
80
85
  if (result) {
81
86
  context.pageRegistry = result.pageRegistry;
82
- context.fileDependencyMap = result.fileDependencyMap;
83
87
  context.buildContext = result.context;
84
88
  }
85
89
  };
@@ -18,7 +18,7 @@ import { spawnProcess } from '@lowdefy/node-utils';
18
18
 
19
19
  function installPlugins({ logger, packageManagerCmd }) {
20
20
  return async () => {
21
- logger.ui.spin('Installing plugins...');
21
+ logger.info({ spin: true }, 'Installing plugins...');
22
22
  await spawnProcess({
23
23
  processOptions: {
24
24
  // https://nodejs.org/en/blog/vulnerability/april-2024-security-releases-2#command-injection-via-args-parameter-of-child_processspawn-without-shell-option-enabled-on-windows-cve-2024-27980---high
@@ -28,7 +28,7 @@ function installPlugins({ logger, packageManagerCmd }) {
28
28
  args: ['install', '--no-frozen-lockfile'],
29
29
  stdOutLineHandler: (line) => logger.debug(line),
30
30
  });
31
- logger.ui.log('Installed plugins.');
31
+ logger.info('Installed plugins.');
32
32
  };
33
33
  }
34
34
 
@@ -19,7 +19,7 @@ import createCustomPluginTypesMap from '../utils/createCustomPluginTypesMap.mjs'
19
19
 
20
20
  function lowdefyBuild({ directories, logger, options, pageCache }) {
21
21
  return async () => {
22
- logger.ui.spin('Building config...');
22
+ logger.info({ spin: true }, 'Building config...');
23
23
  const customTypesMap = await createCustomPluginTypesMap({ directories, logger });
24
24
 
25
25
  if (pageCache) {
@@ -35,7 +35,7 @@ function lowdefyBuild({ directories, logger, options, pageCache }) {
35
35
  });
36
36
 
37
37
  // Return result so getContext can store registries
38
- logger.ui.log('Built config.');
38
+ logger.info('Built config.');
39
39
  return result;
40
40
  } finally {
41
41
  if (pageCache) {
@@ -14,11 +14,12 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { BuildError } from '@lowdefy/errors';
17
18
  import { spawnProcess } from '@lowdefy/node-utils';
18
19
 
19
20
  function nextBuild({ bin, logger }) {
20
21
  return async () => {
21
- logger.ui.spin('Building app...');
22
+ logger.info({ spin: true }, 'Building app...');
22
23
  const errorLines = [];
23
24
  try {
24
25
  await spawnProcess({
@@ -34,12 +35,9 @@ function nextBuild({ bin, logger }) {
34
35
  if (errorLines.length > 0) {
35
36
  errorLines.forEach((line) => logger.error(line));
36
37
  }
37
- const error = new Error('Next.js build failed. See above for details.');
38
- error.isFormatted = true;
39
- error.hideStack = true;
40
- throw error;
38
+ throw new BuildError('Next.js build failed. See above for details.');
41
39
  }
42
- logger.ui.log('Built app.');
40
+ logger.info('Built app.');
43
41
  };
44
42
  }
45
43
 
@@ -19,9 +19,9 @@ import startServer from './startServer.mjs';
19
19
  function restartServer(context) {
20
20
  return () => {
21
21
  context.shutdownServer();
22
- context.logger.ui.spin('Restarting server...');
22
+ context.logger.info({ spin: true }, 'Restarting server...');
23
23
  startServer(context);
24
- context.logger.ui.succeed('Restarted server.');
24
+ context.logger.info({ succeed: true }, 'Restarted server.');
25
25
  };
26
26
  }
27
27
 
@@ -21,7 +21,7 @@ function shutdownServer(context) {
21
21
  `Existing next server with pid ${context.nextServer.pid}, killed: ${context.nextServer.killed}`
22
22
  );
23
23
  if (!context.nextServer.killed) {
24
- context.logger.ui.spin('Shutting down server...');
24
+ context.logger.info({ spin: true }, 'Shutting down server...');
25
25
  context.nextServer.kill();
26
26
  context.logger.debug(
27
27
  `Killed next server with pid ${context.nextServer.pid}, killed: ${context.nextServer.killed}`
@@ -14,6 +14,7 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
+ import { BuildError } from '@lowdefy/errors';
17
18
  import net from 'net';
18
19
 
19
20
  function checkPortAvailable(port) {
@@ -21,11 +22,11 @@ function checkPortAvailable(port) {
21
22
  const server = net.createServer();
22
23
  server.once('error', (err) => {
23
24
  if (err.code === 'EADDRINUSE') {
24
- const error = new Error(
25
- `Port ${port} is already in use. Stop the other process or use a different port with --port.`
25
+ reject(
26
+ new BuildError(
27
+ `Port ${port} is already in use. Stop the other process or use a different port with --port.`
28
+ )
26
29
  );
27
- error.isFormatted = true;
28
- reject(error);
29
30
  } else {
30
31
  reject(err);
31
32
  }
@@ -17,7 +17,7 @@
17
17
  import path from 'path';
18
18
  import { type } from '@lowdefy/helpers';
19
19
  import { readFile } from '@lowdefy/node-utils';
20
- import { ConfigError } from '@lowdefy/errors/build';
20
+ import { ConfigError } from '@lowdefy/errors';
21
21
  import YAML from 'yaml';
22
22
 
23
23
  async function getLowdefyVersion(context) {
@@ -33,11 +33,7 @@ async function getLowdefyVersion(context) {
33
33
  try {
34
34
  lowdefy = YAML.parse(lowdefyYaml);
35
35
  } catch (error) {
36
- throw new ConfigError({
37
- error,
38
- filePath,
39
- configDirectory: context.directories.config,
40
- });
36
+ throw new ConfigError('Could not parse YAML.', { cause: error, filePath });
41
37
  }
42
38
  if (!lowdefy.lowdefy) {
43
39
  throw new Error(
@@ -45,12 +41,10 @@ async function getLowdefyVersion(context) {
45
41
  );
46
42
  }
47
43
  if (!type.isString(lowdefy.lowdefy)) {
48
- throw new ConfigError({
49
- message: 'Version number specified in "lowdefy.yaml" file should be a string.',
50
- received: lowdefy.lowdefy,
51
- filePath,
52
- configDirectory: context.directories.config,
53
- });
44
+ throw new ConfigError(
45
+ 'Version number specified in "lowdefy.yaml" file should be a string.',
46
+ { received: lowdefy.lowdefy, filePath }
47
+ );
54
48
  }
55
49
  return lowdefy.lowdefy;
56
50
  }
@@ -14,7 +14,6 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import fs from 'fs';
18
17
  import path from 'path';
19
18
  import getLowdefyVersion from '../utils/getLowdefyVersion.mjs';
20
19
  import setupWatcher from '../utils/setupWatcher.mjs';
@@ -41,35 +40,16 @@ function lowdefyBuildWatcher(context) {
41
40
  }
42
41
 
43
42
  try {
44
- // Check if only page-level files changed (targeted invalidation)
45
43
  const isSkeletonChange =
46
44
  lowdefyYamlModified ||
47
- changedFiles.some(
48
- (f) =>
49
- !context.fileDependencyMap?.has(f) && !f.startsWith('pages/') && !f.startsWith('./')
50
- );
45
+ changedFiles.some((f) => !f.startsWith('pages/') && !f.startsWith('./'));
51
46
 
52
47
  if (isSkeletonChange || !context.pageCache) {
53
- // Full skeleton rebuild
54
48
  await context.lowdefyBuild();
55
49
  } else {
56
- // Targeted invalidation: only clear affected pages
57
- const affectedPages = context.pageCache.invalidateByFiles(
58
- changedFiles,
59
- context.fileDependencyMap
60
- );
61
- if (affectedPages.size > 0) {
62
- // Write invalidated page IDs to a file so the Next.js server process
63
- // (which has its own PageCache) knows which pages to rebuild.
64
- const invalidationPath = path.join(context.directories.build, 'invalidatePages.json');
65
- fs.writeFileSync(invalidationPath, JSON.stringify([...affectedPages]));
66
- context.logger.ui.log(
67
- `Invalidated ${affectedPages.size} page(s): ${[...affectedPages].join(', ')}`
68
- );
69
- } else {
70
- // Unknown file changed - do full rebuild to be safe
71
- await context.lowdefyBuild();
72
- }
50
+ // Page-only changes: invalidate all pages, no skeleton rebuild needed
51
+ context.pageCache.invalidateAll();
52
+ context.logger.info('Page files changed, invalidated all pages.');
73
53
  }
74
54
  context.reloadClients();
75
55
  } catch (error) {
@@ -86,14 +86,24 @@ async function nextBuildWatcher(context) {
86
86
  })
87
87
  );
88
88
  if (!build) {
89
- context.logger.ui.succeed('Reloaded app.');
89
+ context.logger.info({ succeed: true }, 'Reloaded app.');
90
90
  return;
91
91
  }
92
92
 
93
93
  context.shutdownServer();
94
94
  if (install) {
95
- context.logger.ui.warn('Plugin dependencies have changed and will be reinstalled.');
95
+ context.logger.warn('Plugin dependencies have changed and will be reinstalled.');
96
96
  await context.installPlugins();
97
+ // Rebuild Lowdefy artifacts (blocks.js, icons.js, styles.less, etc.)
98
+ // so newly installed packages are included in the Next.js bundle.
99
+ await context.lowdefyBuild();
100
+ // Re-hash all tracked files to avoid detecting our own build output
101
+ // changes as new changes on the next watcher callback.
102
+ await Promise.all(
103
+ trackedFiles.map(async (filePath) => {
104
+ hashes[filePath] = await sha1(filePath);
105
+ })
106
+ );
97
107
  }
98
108
  await context.nextBuild();
99
109
  context.restartServer();