@lowdefy/server-e2e 6.0.0 → 7.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 (44) hide show
  1. package/client/App.jsx +24 -14
  2. package/client/Page.jsx +53 -16
  3. package/client/getShownPage.js +28 -0
  4. package/client/getShownPage.test.mjs +52 -0
  5. package/lib/client/auth/Auth.jsx +30 -4
  6. package/lib/server/auth/{session.js → getUser.js} +8 -7
  7. package/lib/server/log/createLogger.js +3 -0
  8. package/lib/server/log/logRequest.js +1 -3
  9. package/lib/server/scrubSecrets.js +25 -0
  10. package/lowdefy/build.mjs +1 -0
  11. package/lowdefy/build.test.mjs +65 -0
  12. package/lowdefy/createCustomPluginTypesMap.mjs +3 -2
  13. package/lowdefy/createCustomPluginTypesMap.test.mjs +5 -0
  14. package/package.json +38 -39
  15. package/package.original.json +38 -39
  16. package/src/app.js +12 -5
  17. package/src/html/getAssets.js +3 -1
  18. package/src/html/getHomePath.js +29 -0
  19. package/src/html/renderPage.js +29 -17
  20. package/src/html/renderPage.test.mjs +162 -0
  21. package/src/html/template.js +16 -6
  22. package/src/index.js +23 -0
  23. package/src/lib/getRequestPath.js +29 -0
  24. package/src/middleware/apiContext.js +22 -3
  25. package/src/middleware/apiContext.test.mjs +120 -0
  26. package/src/middleware/errorHandler.js +63 -4
  27. package/src/middleware/errorHandler.test.mjs +402 -0
  28. package/src/routes/apiPage.js +14 -9
  29. package/src/routes/apiPage.test.mjs +92 -0
  30. package/src/routes/clientError.js +6 -12
  31. package/src/routes/e2eIdentity.js +28 -0
  32. package/src/routes/e2eIdentity.test.mjs +43 -0
  33. package/src/routes/endpoints.js +5 -3
  34. package/src/routes/mountPageRoutes.js +29 -0
  35. package/src/routes/mountPageRoutes.test.mjs +56 -0
  36. package/src/routes/request.js +5 -3
  37. package/src/routes/requestBody.test.mjs +69 -0
  38. package/src/routes/sessionMock.js +4 -1
  39. package/src/routes/usage.js +6 -2
  40. package/src/routes/user.js +27 -0
  41. package/src/routes/websocket.js +23 -3
  42. package/src/routes/websocket.test.mjs +73 -0
  43. package/vite.config.js +57 -1
  44. package/lib/server/apiWrapper.js +0 -73
package/client/App.jsx CHANGED
@@ -17,11 +17,13 @@
17
17
  import React, { useCallback, useRef } from 'react';
18
18
 
19
19
  import { ErrorBoundary } from '@lowdefy/block-utils';
20
- import { useDarkMode, useLocale } from '@lowdefy/client';
20
+ import { IconProvider, useDarkMode, useLocale } from '@lowdefy/client';
21
21
  import { StyleProvider } from '@ant-design/cssinjs';
22
22
  import { App as AntdApp, theme as antdTheme } from 'antd';
23
23
  import { XProvider } from '@ant-design/x';
24
24
 
25
+ import { appStorageKey } from '@lowdefy/helpers';
26
+
25
27
  import antdLocaleLoaders from '../build/i18n/antdLocales.js';
26
28
  import antdXLocaleLoaders from '../build/i18n/antdXLocales.js';
27
29
  import dayjsLocaleMap from '../build/i18n/dayjsLocales.js';
@@ -39,7 +41,7 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
39
41
  }
40
42
 
41
43
  function App({ config }) {
42
- const { rootConfig, session } = config;
44
+ const { rootConfig, user } = config;
43
45
 
44
46
  const usageDataRef = useRef({});
45
47
  const lowdefyRef = useRef({ eventCallback: createLogUsage({ usageDataRef }) });
@@ -47,9 +49,12 @@ function App({ config }) {
47
49
  lowdefyRef.current.theme = rootConfig.theme;
48
50
  }
49
51
 
52
+ // Device preferences, kept per app (the same key the HTML template's dark mode script reads).
53
+ const storageScope = { basePath: config.basePath ?? '', slug: rootConfig.lowdefyApp?.slug };
50
54
  const { algorithm, token, components } = useDarkMode({
51
55
  antd: lowdefyRef.current.theme?.antd,
52
56
  configDarkMode: lowdefyRef.current.theme?.darkMode,
57
+ storageKey: appStorageKey({ ...storageScope, name: 'lowdefy_darkMode' }),
53
58
  });
54
59
 
55
60
  const {
@@ -61,6 +66,7 @@ function App({ config }) {
61
66
  antdLocaleLoaders,
62
67
  antdXLocaleLoaders,
63
68
  dayjsLocaleMap,
69
+ storageKey: appStorageKey({ ...storageScope, name: 'lowdefy_locale' }),
64
70
  });
65
71
 
66
72
  if (rootConfig?.i18n?.defaultLocale) {
@@ -108,18 +114,22 @@ function App({ config }) {
108
114
  algorithm,
109
115
  }}
110
116
  >
111
- <AntdApp>
112
- <ThemeTokenResolver lowdefyRef={lowdefyRef}>
113
- <ErrorBoundary fullPage onError={handleError}>
114
- <Auth session={session}>
115
- {(auth) => {
116
- usageDataRef.current.user = auth.session?.hashed_id;
117
- return <Page auth={auth} config={config} lowdefy={lowdefyRef.current} />;
118
- }}
119
- </Auth>
120
- </ErrorBoundary>
121
- </ThemeTokenResolver>
122
- </AntdApp>
117
+ {/* Message, Notification and ConfirmModal render in the App.useApp()
118
+ holders here, above the Client's icon provider. */}
119
+ <IconProvider icons={rootConfig.theme.icons}>
120
+ <AntdApp>
121
+ <ThemeTokenResolver lowdefyRef={lowdefyRef}>
122
+ <ErrorBoundary fullPage onError={handleError}>
123
+ <Auth user={user}>
124
+ {(auth) => {
125
+ usageDataRef.current.user = auth.user?.id;
126
+ return <Page auth={auth} config={config} lowdefy={lowdefyRef.current} />;
127
+ }}
128
+ </Auth>
129
+ </ErrorBoundary>
130
+ </ThemeTokenResolver>
131
+ </AntdApp>
132
+ </IconProvider>
123
133
  </XProvider>
124
134
  </StyleProvider>
125
135
  );
package/client/Page.jsx CHANGED
@@ -14,13 +14,14 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import React, { useEffect, useRef, useState } from 'react';
17
+ import React, { useEffect, useReducer, useRef, useState } from 'react';
18
18
 
19
19
  import Client from '@lowdefy/client';
20
20
  import createRouter from '@lowdefy/client/adapters/createRouter.js';
21
21
  import createLinkComponent from '@lowdefy/client/adapters/Link.js';
22
22
  import { createUrl } from '@lowdefy/client/adapters/url.js';
23
23
  import Head from '@lowdefy/client/adapters/Head.js';
24
+ import { buildPagePath } from '@lowdefy/helpers';
24
25
 
25
26
  import actions from '../build/plugins/actions.js';
26
27
  import blockMetas from '../build/plugins/blockMetas.json';
@@ -29,10 +30,39 @@ import icons from '../build/plugins/icons.js';
29
30
  import operators from '../build/plugins/operators/client.js';
30
31
  import jsMap from '../build/plugins/operators/clientJsMap.js';
31
32
 
33
+ import getShownPage from './getShownPage.js';
34
+
35
+ // The path to fetch for a navigation. The app root shows the home page, and
36
+ // /api/page/ with an empty path matches no page, so it fetches the home page's
37
+ // own path.
38
+ function getFetchPath({ path, rootConfig }) {
39
+ if (path !== '') {
40
+ return path;
41
+ }
42
+ const { home, pagePaths } = rootConfig;
43
+ return buildPagePath({
44
+ pageId: home.pageId,
45
+ path: pagePaths[home.pageId],
46
+ pathParams: home.pathParams,
47
+ });
48
+ }
49
+
32
50
  // Replaces lib/client/Page.js. The first page renders from the config
33
- // embedded in the HTML; SPA navigations fetch /api/page/* and swap pageConfig.
51
+ // embedded in the HTML; SPA navigations fetch /api/page/<path> and swap the
52
+ // page. The server matches the path and answers with the page it shows, the
53
+ // values it carries and the path it matched.
34
54
  function Page({ auth, config, lowdefy }) {
35
- const [pageConfig, setPageConfig] = useState(config.pageConfig);
55
+ const [page, setPage] = useState({
56
+ matchedPath: config.matchedPath,
57
+ pageConfig: config.pageConfig,
58
+ pathParams: config.pathParams,
59
+ });
60
+ // The navigation listener is subscribed once, so it reads the shown page
61
+ // through a ref rather than the state its closure captured.
62
+ const pageRef = useRef(page);
63
+ // A re-render with the same config is how the page context picks up a new
64
+ // URL (getContext updates a memoized context on every render).
65
+ const [, rerender] = useReducer((count) => count + 1, 0);
36
66
 
37
67
  const routerRef = useRef(null);
38
68
  if (!routerRef.current) {
@@ -50,33 +80,38 @@ function Page({ auth, config, lowdefy }) {
50
80
  const latestNavRef = useRef(0);
51
81
 
52
82
  useEffect(() => {
53
- const unsubscribe = router.subscribe(async ({ pageId }) => {
83
+ const unsubscribe = router.subscribe(async ({ path, search }) => {
54
84
  const token = ++latestNavRef.current;
55
- const targetPageId = pageId ?? config.rootConfig.home.pageId;
85
+ // The path the shown instance was matched on shows the same page and
86
+ // values whatever the query, so it needs no fetch. Another spelling of
87
+ // the same values fetches and lands on the same instance. Dynamic pages
88
+ // re-resolve per navigation.
89
+ if (path === pageRef.current.matchedPath && pageRef.current.pageConfig.dynamic !== true) {
90
+ rerender();
91
+ return;
92
+ }
93
+ const fetchPath = getFetchPath({ path, rootConfig: config.rootConfig });
56
94
  try {
57
95
  // Forward the current query string so server-side Dynamic block
58
96
  // resolution sees the same urlQuery as an initial HTML load.
59
- const res = await fetch(
60
- `${router.basePath}/api/page/${targetPageId}${window.location.search}`
61
- );
97
+ const res = await fetch(`${router.basePath}/api/page/${fetchPath}${search}`);
62
98
  if (!res.ok) {
63
99
  if (token !== latestNavRef.current) return;
64
- if (targetPageId !== '404') {
100
+ if (path !== '404') {
65
101
  router.replace({ pathname: '/404' });
66
102
  }
67
103
  return;
68
104
  }
69
- const { pageConfig: nextPageConfig } = await res.json();
105
+ const response = await res.json();
70
106
  if (token !== latestNavRef.current) return;
71
- setPageConfig(nextPageConfig);
107
+ const nextPage = getShownPage({ path, response });
108
+ pageRef.current = nextPage;
109
+ setPage(nextPage);
72
110
  } catch (error) {
73
111
  // Network failure on SPA navigation — fall back to a full page load.
74
112
  if (token !== latestNavRef.current) return;
75
113
  window.location.assign(
76
- createUrl({
77
- basePath: router.basePath,
78
- pathname: targetPageId === config.rootConfig.home.pageId ? '/' : `/${targetPageId}`,
79
- })
114
+ createUrl({ basePath: router.basePath, pathname: `/${path}`, query: search.slice(1) })
80
115
  );
81
116
  }
82
117
  });
@@ -88,11 +123,13 @@ function Page({ auth, config, lowdefy }) {
88
123
  auth={auth}
89
124
  Components={{ Head, Link }}
90
125
  config={{
91
- pageConfig,
126
+ pageConfig: page.pageConfig,
92
127
  rootConfig: config.rootConfig,
93
128
  }}
94
129
  jsMap={jsMap}
95
130
  lowdefy={lowdefy}
131
+ matchedPath={page.matchedPath}
132
+ pathParams={page.pathParams}
96
133
  router={router}
97
134
  stage="e2e"
98
135
  types={{
@@ -0,0 +1,28 @@
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
+ // The page a navigation to `path` shows, from the page response. At the app root the client
18
+ // fetches the home page's own path, but the shown instance is matched on the root, as on a first
19
+ // load there, so a later navigation to the root (a query change, the home link) needs no fetch.
20
+ function getShownPage({ path, response }) {
21
+ const { pageConfig, pathParams } = response;
22
+ if (path === '') {
23
+ return { matchedPath: '', pageConfig, pathParams };
24
+ }
25
+ return { matchedPath: response.matchedPath, pageConfig, pathParams };
26
+ }
27
+
28
+ export default getShownPage;
@@ -0,0 +1,52 @@
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 getShownPage from './getShownPage.js';
18
+
19
+ const pageConfig = { pageId: 'ticket', path: 'tickets/{space}/{ticket_id}' };
20
+
21
+ test('getShownPage keeps the path the server matched', () => {
22
+ expect(
23
+ getShownPage({
24
+ path: 'tickets/a%2Bb/1',
25
+ response: {
26
+ pageId: 'ticket',
27
+ pathParams: { space: 'a+b', ticket_id: '1' },
28
+ matchedPath: 'tickets/a%2Bb/1',
29
+ pageConfig,
30
+ },
31
+ })
32
+ ).toEqual({
33
+ matchedPath: 'tickets/a%2Bb/1',
34
+ pageConfig,
35
+ pathParams: { space: 'a+b', ticket_id: '1' },
36
+ });
37
+ });
38
+
39
+ test('getShownPage matches the home page fetched for the app root on the root', () => {
40
+ const homeConfig = { pageId: 'users', path: 'admin/users' };
41
+ expect(
42
+ getShownPage({
43
+ path: '',
44
+ response: {
45
+ pageId: 'users',
46
+ pathParams: {},
47
+ matchedPath: 'admin/users',
48
+ pageConfig: homeConfig,
49
+ },
50
+ })
51
+ ).toEqual({ matchedPath: '', pageConfig: homeConfig, pathParams: {} });
52
+ });
@@ -24,19 +24,45 @@ function e2eNotSupported() {
24
24
  throw new Error('Sign-in and sign-out are not supported in e2e testing.');
25
25
  }
26
26
 
27
- function Auth({ children, session }) {
27
+ function Auth({ children, user }) {
28
28
  const auth = {
29
29
  authConfig,
30
- session,
30
+ user,
31
+ // Matches the BetterAuth client contract: { data, error }.
31
32
  getSession: async () => {
32
33
  const res = await fetch('/api/auth/session');
34
+ if (res.ok) {
35
+ return { data: await res.json(), error: null };
36
+ }
37
+ return { data: null, error: null };
38
+ },
39
+ getResolvedUser: async () => {
40
+ const res = await fetch('/api/user');
33
41
  if (res.ok) {
34
42
  return res.json();
35
43
  }
36
- return null;
44
+ return { user: null };
37
45
  },
38
- signIn: e2eNotSupported,
46
+ acceptInvitation: e2eNotSupported,
47
+ addPasskey: e2eNotSupported,
48
+ changePassword: e2eNotSupported,
49
+ deletePasskey: e2eNotSupported,
50
+ updatePasskey: e2eNotSupported,
51
+ requestPasswordReset: e2eNotSupported,
52
+ resetPassword: e2eNotSupported,
53
+ revokeOtherSessions: e2eNotSupported,
54
+ sendVerificationEmail: e2eNotSupported,
55
+ setActiveOrganization: e2eNotSupported,
56
+ signInEmail: e2eNotSupported,
57
+ signInMagicLink: e2eNotSupported,
58
+ signInSocial: e2eNotSupported,
39
59
  signOut: e2eNotSupported,
60
+ signUpEmail: e2eNotSupported,
61
+ twoFactorDisable: e2eNotSupported,
62
+ twoFactorEnable: e2eNotSupported,
63
+ twoFactorGenerateBackupCodes: e2eNotSupported,
64
+ twoFactorVerifyBackupCode: e2eNotSupported,
65
+ twoFactorVerifyTotp: e2eNotSupported,
40
66
  };
41
67
  return children(auth);
42
68
  }
@@ -14,22 +14,23 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- // E2E test-user injection: the session is read from the lowdefy_e2e_user
17
+ // E2E test-user injection: the caller is read from the lowdefy_e2e_user
18
18
  // cookie (base64-encoded JSON user object) instead of a real auth engine.
19
- function getSession(c) {
19
+ // The cookie user is pre-resolved - it substitutes for resolveAuthentication
20
+ // and its roles are authoritative.
21
+ function getUser(c) {
20
22
  const cookieHeader = c.req.header('cookie') ?? '';
21
23
  const match = cookieHeader.match(/lowdefy_e2e_user=([^;]+)/);
22
24
  if (!match) {
23
- return undefined;
25
+ return null;
24
26
  }
25
27
 
26
28
  try {
27
29
  const decoded = Buffer.from(decodeURIComponent(match[1]), 'base64').toString();
28
- const user = JSON.parse(decoded);
29
- return { user };
30
+ return JSON.parse(decoded);
30
31
  } catch {
31
- return undefined;
32
+ return null;
32
33
  }
33
34
  }
34
35
 
35
- export default getSession;
36
+ export default getUser;
@@ -15,11 +15,14 @@
15
15
  */
16
16
 
17
17
  import { createNodeLogger } from '@lowdefy/logger/node';
18
+ import { scrubCredentials } from '@lowdefy/node-utils';
18
19
 
19
20
  const logger = createNodeLogger({
20
21
  name: 'lowdefy_server_e2e',
21
22
  level: process.env.LOWDEFY_LOG_LEVEL ?? 'info',
22
23
  base: { pid: undefined, hostname: undefined },
24
+ // Secrets stay in these logs, but a value a request marked with _credential does not.
25
+ hooks: { streamWrite: scrubCredentials },
23
26
  });
24
27
 
25
28
  function createLogger(metadata = {}) {
@@ -15,13 +15,11 @@
15
15
  */
16
16
 
17
17
  function logRequest({ context }) {
18
- const { user = {} } = context.session ?? {};
18
+ const user = context.user ?? {};
19
19
  context.logger.debug({
20
20
  user: {
21
21
  id: user.id,
22
22
  roles: user.roles,
23
- sub: user.sub,
24
- session_id: user.session_id,
25
23
  },
26
24
  url: context.req.url,
27
25
  method: context.req.method,
@@ -0,0 +1,25 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import { createSecretScrubber } from '@lowdefy/node-utils';
18
+
19
+ import getE2eSecrets from './getE2eSecrets.js';
20
+
21
+ // E2E logs stay unscrubbed, but _error reads this off the context like every
22
+ // other server package, so a thrown message behaves identically in every mode.
23
+ const scrubSecrets = createSecretScrubber({ secrets: getE2eSecrets() });
24
+
25
+ export default scrubSecrets;
package/lowdefy/build.mjs CHANGED
@@ -51,6 +51,7 @@ async function run() {
51
51
  directories,
52
52
  logger,
53
53
  refResolver: argv.refResolver || process.env.LOWDEFY_BUILD_REF_RESOLVER,
54
+ skipPluginChecks: process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS === 'true',
54
55
  });
55
56
  }
56
57
 
@@ -0,0 +1,65 @@
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 { jest } from '@jest/globals';
18
+
19
+ const mockBuild = jest.fn();
20
+
21
+ jest.unstable_mockModule('@lowdefy/build', () => ({ default: mockBuild }));
22
+ jest.unstable_mockModule('@lowdefy/logger/node', () => ({
23
+ createNodeLogger: jest.fn(() => 'logger'),
24
+ }));
25
+ jest.unstable_mockModule('./createCustomPluginTypesMap.mjs', () => ({
26
+ default: jest.fn().mockResolvedValue({}),
27
+ }));
28
+
29
+ const savedSkip = process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS;
30
+ let loadCount = 0;
31
+
32
+ // build.mjs runs the build when imported, so each case loads a fresh module
33
+ // instance and waits for the build call.
34
+ async function runScript() {
35
+ loadCount += 1;
36
+ await import(`./build.mjs?case=${loadCount}`);
37
+ await new Promise((resolve) => setImmediate(resolve));
38
+ }
39
+
40
+ beforeEach(() => {
41
+ delete process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS;
42
+ mockBuild.mockReset();
43
+ mockBuild.mockResolvedValue();
44
+ });
45
+
46
+ afterAll(() => {
47
+ if (savedSkip === undefined) {
48
+ delete process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS;
49
+ } else {
50
+ process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS = savedSkip;
51
+ }
52
+ });
53
+
54
+ test('build passes skipPluginChecks true when LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS is true', async () => {
55
+ process.env.LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS = 'true';
56
+ await runScript();
57
+ expect(mockBuild).toHaveBeenCalledTimes(1);
58
+ expect(mockBuild.mock.calls[0][0].skipPluginChecks).toBe(true);
59
+ });
60
+
61
+ test('build passes skipPluginChecks false when LOWDEFY_BUILD_SKIP_PLUGIN_CHECKS is unset', async () => {
62
+ await runScript();
63
+ expect(mockBuild).toHaveBeenCalledTimes(1);
64
+ expect(mockBuild.mock.calls[0][0].skipPluginChecks).toBe(false);
65
+ });
@@ -42,14 +42,15 @@ async function createCustomPluginTypesMap({ directories }) {
42
42
  agents: {},
43
43
  auth: {
44
44
  adapters: {},
45
- callbacks: {},
46
- events: {},
47
45
  providers: {},
46
+ strategies: {},
48
47
  },
49
48
  blockMetas: {},
49
+ connectionMetas: {},
50
50
  blocks: {},
51
51
  connections: {},
52
52
  icons: {},
53
+ iconSets: {},
53
54
  operators: {
54
55
  client: {},
55
56
  server: {},
@@ -41,7 +41,12 @@ const pluginTypesEsm = {
41
41
  },
42
42
  };
43
43
 
44
+ // The module under test imports @lowdefy/build, which links against every
45
+ // export it uses, so the real module passes through and only file access is
46
+ // replaced.
47
+ const realNodeUtils = await import('@lowdefy/node-utils');
44
48
  jest.unstable_mockModule('@lowdefy/node-utils', () => ({
49
+ ...realNodeUtils,
45
50
  cleanDirectory: jest.fn(),
46
51
  copyFileOrDirectory: jest.fn(),
47
52
  getFileExtension: jest.fn(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lowdefy/server-e2e",
3
- "version": "6.0.0",
3
+ "version": "7.0.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Lowdefy e2e testing server with cookie-based user injection",
6
6
  "homepage": "https://lowdefy.com",
@@ -41,58 +41,57 @@
41
41
  ],
42
42
  "dependencies": {
43
43
  "@ant-design/cssinjs": "2.1.2",
44
- "@ant-design/x": "2.7.0",
45
- "@hono/node-server": "2.0.4",
46
- "@lowdefy/actions-core": "6.0.0",
47
- "@lowdefy/api": "6.0.0",
48
- "@lowdefy/block-utils": "6.0.0",
49
- "@lowdefy/blocks-aggrid": "6.0.0",
50
- "@lowdefy/blocks-antd": "6.0.0",
51
- "@lowdefy/blocks-antd-x": "6.0.0",
52
- "@lowdefy/blocks-basic": "6.0.0",
53
- "@lowdefy/blocks-echarts": "6.0.0",
54
- "@lowdefy/blocks-loaders": "6.0.0",
55
- "@lowdefy/blocks-markdown": "6.0.0",
56
- "@lowdefy/blocks-tiptap": "6.0.0",
57
- "@lowdefy/client": "6.0.0",
58
- "@lowdefy/connection-axios-http": "6.0.0",
59
- "@lowdefy/connection-mongodb": "6.0.0",
60
- "@lowdefy/errors": "6.0.0",
61
- "@lowdefy/helpers": "6.0.0",
62
- "@lowdefy/layout": "6.0.0",
63
- "@lowdefy/logger": "6.0.0",
64
- "@lowdefy/node-utils": "6.0.0",
65
- "@lowdefy/operators-js": "6.0.0",
66
- "@lowdefy/operators-nunjucks": "6.0.0",
67
- "@lowdefy/operators-uuid": "6.0.0",
68
- "@lowdefy/websockets-core": "6.0.0",
69
- "@tailwindcss/postcss": "4.3.1",
44
+ "@ant-design/x": "2.9.0",
45
+ "@hono/node-server": "2.0.10",
46
+ "@lowdefy/actions-core": "7.0.0",
47
+ "@lowdefy/api": "7.0.0",
48
+ "@lowdefy/block-utils": "7.0.0",
49
+ "@lowdefy/blocks-aggrid": "7.0.0",
50
+ "@lowdefy/blocks-antd": "7.0.0",
51
+ "@lowdefy/blocks-antd-x": "7.0.0",
52
+ "@lowdefy/blocks-basic": "7.0.0",
53
+ "@lowdefy/blocks-captcha": "7.0.0",
54
+ "@lowdefy/blocks-echarts": "7.0.0",
55
+ "@lowdefy/blocks-loaders": "7.0.0",
56
+ "@lowdefy/blocks-markdown": "7.0.0",
57
+ "@lowdefy/blocks-table": "7.0.0",
58
+ "@lowdefy/blocks-tiptap": "7.0.0",
59
+ "@lowdefy/client": "7.0.0",
60
+ "@lowdefy/connection-axios-http": "7.0.0",
61
+ "@lowdefy/connection-mongodb": "7.0.0",
62
+ "@lowdefy/errors": "7.0.0",
63
+ "@lowdefy/helpers": "7.0.0",
64
+ "@lowdefy/layout": "7.0.0",
65
+ "@lowdefy/logger": "7.0.0",
66
+ "@lowdefy/node-utils": "7.0.0",
67
+ "@lowdefy/operators-js": "7.0.0",
68
+ "@lowdefy/operators-nunjucks": "7.0.0",
69
+ "@lowdefy/operators-uuid": "7.0.0",
70
+ "@lowdefy/websockets-core": "7.0.0",
71
+ "@tailwindcss/postcss": "4.3.3",
70
72
  "@vitejs/plugin-react": "6.0.2",
71
- "antd": "6.3.1",
73
+ "antd": "6.6.5",
72
74
  "dayjs": "1.11.20",
73
- "hono": "4.12.25",
74
- "pino": "10.3.1",
75
+ "hono": "4.13.7",
76
+ "lightningcss": "1.32.0",
75
77
  "react": "18.2.0",
76
78
  "react-dom": "18.2.0",
77
- "react-icons": "5.6.0",
78
- "tailwindcss": "4.3.1",
79
+ "tailwindcss": "4.3.3",
79
80
  "uuid": "14.0.0",
80
81
  "vite": "8.0.16",
81
- "ws": "8.18.0"
82
+ "ws": "8.21.0"
82
83
  },
83
84
  "devDependencies": {
84
85
  "@jest/globals": "28.1.3",
85
- "@lowdefy/build": "6.0.0",
86
- "@next/eslint-plugin-next": "16.1.6",
87
- "@tailwindcss/postcss": "4.3.1",
86
+ "@lowdefy/build": "7.0.0",
87
+ "@tailwindcss/postcss": "4.3.3",
88
88
  "jest": "28.1.3",
89
- "tailwindcss": "4.3.1",
90
- "webpack": "5.94.0",
89
+ "tailwindcss": "4.3.3",
91
90
  "yaml": "2.9.0",
92
91
  "yargs": "18.0.0"
93
92
  },
94
93
  "engines": {
95
- "node": ">=22"
94
+ "node": ">=24"
96
95
  },
97
96
  "publishConfig": {
98
97
  "access": "public"