@lowdefy/server-e2e 5.6.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/{pages/_app.js → client/App.jsx} +43 -28
  2. package/client/Page.jsx +110 -0
  3. package/client/main.jsx +39 -0
  4. package/lib/build/app.js +8 -1
  5. package/lib/build/appMeta.js +8 -1
  6. package/lib/build/auth.js +8 -1
  7. package/lib/build/config.js +8 -1
  8. package/lib/build/i18n.js +26 -0
  9. package/lib/build/logger.js +8 -1
  10. package/lib/build/theme.js +8 -1
  11. package/lib/client/auth/{Auth.js → Auth.jsx} +5 -1
  12. package/lib/server/auth/{getServerSession.js → session.js} +5 -3
  13. package/lib/server/log/createHandleError.test.mjs +126 -0
  14. package/lib/server/scrubSecrets.js +25 -0
  15. package/lowdefy/createCustomPluginTypesMap.mjs +1 -0
  16. package/lowdefy/createCustomPluginTypesMap.test.mjs +2 -0
  17. package/package.json +49 -43
  18. package/package.original.json +52 -46
  19. package/postcss.config.cjs +1 -0
  20. package/src/app.js +78 -0
  21. package/src/html/getAssets.js +45 -0
  22. package/src/html/renderPage.js +79 -0
  23. package/src/html/template.js +95 -0
  24. package/src/index.js +40 -0
  25. package/src/lib/getPathSegments.js +25 -0
  26. package/src/lib/safeScriptJson.js +38 -0
  27. package/src/middleware/apiContext.js +84 -0
  28. package/src/middleware/apiContext.test.mjs +109 -0
  29. package/src/middleware/errorHandler.js +39 -0
  30. package/src/routes/apiPage.js +37 -0
  31. package/{pages/api/client-error.js → src/routes/clientError.js} +14 -18
  32. package/{pages/api/endpoints/[...endpointId].js → src/routes/endpoints.js} +8 -7
  33. package/{pages/api/request/[...path].js → src/routes/request.js} +10 -10
  34. package/{pages/api/auth/session.js → src/routes/sessionMock.js} +4 -5
  35. package/{pages/api → src/routes}/usage.js +11 -8
  36. package/src/routes/websocket.js +46 -0
  37. package/vite.config.js +45 -0
  38. package/.eslintrc.yaml +0 -1
  39. package/lib/client/Page.js +0 -57
  40. package/lib/server/apiWrapper.js +0 -73
  41. package/lib/server/serverSidePropsWrapper.js +0 -64
  42. package/next.config.js +0 -20
  43. package/pages/404.js +0 -49
  44. package/pages/[[...pageId]].js +0 -85
  45. package/pages/_document.js +0 -108
@@ -14,24 +14,20 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- // CSS layer order — MUST be the first CSS import. Next.js treats this as critical
18
- // CSS that loads before hydration, locking the cascade priority (antd > base/preflight)
19
- // before antd's StyleProvider injects @layer antd {} at runtime.
20
- import '../build/layer-order.css';
21
-
22
17
  import React, { useCallback, useRef } from 'react';
23
- import dynamic from 'next/dynamic';
24
18
 
25
19
  import { ErrorBoundary } from '@lowdefy/block-utils';
26
- import { useDarkMode } from '@lowdefy/client';
20
+ import { useDarkMode, useLocale } from '@lowdefy/client';
27
21
  import { StyleProvider } from '@ant-design/cssinjs';
28
- import { App as AntdApp, ConfigProvider, theme as antdTheme } from 'antd';
22
+ import { App as AntdApp, theme as antdTheme } from 'antd';
23
+ import { XProvider } from '@ant-design/x';
29
24
 
30
- import Auth from '../lib/client/auth/Auth.js';
25
+ import antdLocaleLoaders from '../build/i18n/antdLocales.js';
26
+ import antdXLocaleLoaders from '../build/i18n/antdXLocales.js';
27
+ import dayjsLocaleMap from '../build/i18n/dayjsLocales.js';
28
+ import Auth from '../lib/client/auth/Auth.jsx';
31
29
  import createLogUsage from '../lib/client/createLogUsage.js';
32
-
33
- // Must be in _app due to next specifications.
34
- import '../build/globals.css';
30
+ import Page from './Page.jsx';
35
31
 
36
32
  function ThemeTokenResolver({ lowdefyRef, children }) {
37
33
  const { token } = antdTheme.useToken();
@@ -42,7 +38,9 @@ function ThemeTokenResolver({ lowdefyRef, children }) {
42
38
  return children;
43
39
  }
44
40
 
45
- function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
41
+ function App({ config }) {
42
+ const { rootConfig, session } = config;
43
+
46
44
  const usageDataRef = useRef({});
47
45
  const lowdefyRef = useRef({ eventCallback: createLogUsage({ usageDataRef }) });
48
46
  if (rootConfig?.theme) {
@@ -54,6 +52,21 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
54
52
  configDarkMode: lowdefyRef.current.theme?.darkMode,
55
53
  });
56
54
 
55
+ const {
56
+ active: activeLocale,
57
+ antdLocale,
58
+ antdXLocale,
59
+ } = useLocale({
60
+ i18n: rootConfig?.i18n,
61
+ antdLocaleLoaders,
62
+ antdXLocaleLoaders,
63
+ dayjsLocaleMap,
64
+ });
65
+
66
+ if (rootConfig?.i18n?.defaultLocale) {
67
+ lowdefyRef.current.i18n = { ...rootConfig.i18n, active: activeLocale };
68
+ }
69
+
57
70
  const {
58
71
  lightToken: _lightToken,
59
72
  darkToken: _darkToken,
@@ -70,9 +83,22 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
70
83
  }
71
84
  }, []);
72
85
 
86
+ // XProvider extends antd's ConfigProvider; merging antd + antd-X locale packs
87
+ // gives X components (Bubble, Sender, Conversations, ...) their built-in strings
88
+ // alongside antd's. antd X ships only en_US + zh_CN; other locales fall back
89
+ // to en_US for X-native strings.
90
+ const mergedLocale =
91
+ antdLocale || antdXLocale ? { ...(antdLocale ?? {}), ...(antdXLocale ?? {}) } : undefined;
92
+
73
93
  return (
74
94
  <StyleProvider layer>
75
- <ConfigProvider
95
+ <XProvider
96
+ locale={mergedLocale}
97
+ form={
98
+ antdLocale?.Form?.defaultValidateMessages
99
+ ? { validateMessages: antdLocale.Form.defaultValidateMessages }
100
+ : undefined
101
+ }
76
102
  theme={{
77
103
  ...antdConfig,
78
104
  token,
@@ -88,26 +114,15 @@ function App({ Component, pageProps: { session, rootConfig, pageConfig } }) {
88
114
  <Auth session={session}>
89
115
  {(auth) => {
90
116
  usageDataRef.current.user = auth.session?.hashed_id;
91
- return (
92
- <Component
93
- auth={auth}
94
- lowdefy={lowdefyRef.current}
95
- rootConfig={rootConfig}
96
- pageConfig={pageConfig}
97
- />
98
- );
117
+ return <Page auth={auth} config={config} lowdefy={lowdefyRef.current} />;
99
118
  }}
100
119
  </Auth>
101
120
  </ErrorBoundary>
102
121
  </ThemeTokenResolver>
103
122
  </AntdApp>
104
- </ConfigProvider>
123
+ </XProvider>
105
124
  </StyleProvider>
106
125
  );
107
126
  }
108
127
 
109
- const DynamicApp = dynamic(() => Promise.resolve(App), {
110
- ssr: false,
111
- });
112
-
113
- export default DynamicApp;
128
+ export default App;
@@ -0,0 +1,110 @@
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, { useEffect, useRef, useState } from 'react';
18
+
19
+ import Client from '@lowdefy/client';
20
+ import createRouter from '@lowdefy/client/adapters/createRouter.js';
21
+ import createLinkComponent from '@lowdefy/client/adapters/Link.js';
22
+ import { createUrl } from '@lowdefy/client/adapters/url.js';
23
+ import Head from '@lowdefy/client/adapters/Head.js';
24
+
25
+ import actions from '../build/plugins/actions.js';
26
+ import blockMetas from '../build/plugins/blockMetas.json';
27
+ import blocks from '../build/plugins/blocks.js';
28
+ import icons from '../build/plugins/icons.js';
29
+ import operators from '../build/plugins/operators/client.js';
30
+ import jsMap from '../build/plugins/operators/clientJsMap.js';
31
+
32
+ // 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.
34
+ function Page({ auth, config, lowdefy }) {
35
+ const [pageConfig, setPageConfig] = useState(config.pageConfig);
36
+
37
+ const routerRef = useRef(null);
38
+ if (!routerRef.current) {
39
+ const router = createRouter({ basePath: config.basePath ?? '', window });
40
+ routerRef.current = {
41
+ router,
42
+ Link: createLinkComponent({ router }),
43
+ };
44
+ }
45
+ const { router, Link } = routerRef.current;
46
+
47
+ // Temporary sequence guard for the superseded-navigation race: a slow
48
+ // response for an abandoned navigation must not paint over a newer one.
49
+ // Remove when the loader-based lifecycle replaces these fetch paths.
50
+ const latestNavRef = useRef(0);
51
+
52
+ useEffect(() => {
53
+ const unsubscribe = router.subscribe(async ({ pageId }) => {
54
+ const token = ++latestNavRef.current;
55
+ const targetPageId = pageId ?? config.rootConfig.home.pageId;
56
+ try {
57
+ // Forward the current query string so server-side Dynamic block
58
+ // 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
+ );
62
+ if (!res.ok) {
63
+ if (token !== latestNavRef.current) return;
64
+ if (targetPageId !== '404') {
65
+ router.replace({ pathname: '/404' });
66
+ }
67
+ return;
68
+ }
69
+ const { pageConfig: nextPageConfig } = await res.json();
70
+ if (token !== latestNavRef.current) return;
71
+ setPageConfig(nextPageConfig);
72
+ } catch (error) {
73
+ // Network failure on SPA navigation — fall back to a full page load.
74
+ if (token !== latestNavRef.current) return;
75
+ window.location.assign(
76
+ createUrl({
77
+ basePath: router.basePath,
78
+ pathname: targetPageId === config.rootConfig.home.pageId ? '/' : `/${targetPageId}`,
79
+ })
80
+ );
81
+ }
82
+ });
83
+ return unsubscribe;
84
+ }, []);
85
+
86
+ return (
87
+ <Client
88
+ auth={auth}
89
+ Components={{ Head, Link }}
90
+ config={{
91
+ pageConfig,
92
+ rootConfig: config.rootConfig,
93
+ }}
94
+ jsMap={jsMap}
95
+ lowdefy={lowdefy}
96
+ router={router}
97
+ stage="e2e"
98
+ types={{
99
+ actions,
100
+ blockMetas,
101
+ blocks,
102
+ icons,
103
+ operators,
104
+ }}
105
+ window={window}
106
+ />
107
+ );
108
+ }
109
+
110
+ export default Page;
@@ -0,0 +1,39 @@
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
+ // CSS layer order — MUST be the first CSS import. This locks the cascade
18
+ // priority (antd > base/preflight) before antd's StyleProvider injects
19
+ // @layer antd {} at runtime.
20
+ import '../build/layer-order.css';
21
+
22
+ import React from 'react';
23
+ import { createRoot } from 'react-dom/client';
24
+
25
+ import App from './App.jsx';
26
+
27
+ import '../build/globals.css';
28
+
29
+ const config = JSON.parse(document.getElementById('__LOWDEFY_CONFIG__').textContent);
30
+ const container = document.getElementById('root');
31
+
32
+ // Keep one React root across Vite HMR updates — re-running createRoot on a
33
+ // container that already has a root is an error.
34
+ const root = import.meta.hot?.data.root ?? createRoot(container);
35
+ if (import.meta.hot) {
36
+ import.meta.hot.data.root = root;
37
+ }
38
+
39
+ root.render(<App config={config} />);
package/lib/build/app.js CHANGED
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/app.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/app.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/appMeta.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/appMeta.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
package/lib/build/auth.js CHANGED
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/auth.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/auth.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/config.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/config.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -0,0 +1,26 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
22
+ import { serializer } from '@lowdefy/helpers';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/i18n.json'), 'utf8'));
25
+
26
+ export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/logger.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/logger.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/theme.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/theme.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -14,7 +14,11 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import authConfig from '../../build/auth.js';
17
+ import { serializer } from '@lowdefy/helpers';
18
+
19
+ import rawAuthConfig from '../../../build/auth.json';
20
+
21
+ const authConfig = serializer.deserialize(rawAuthConfig);
18
22
 
19
23
  function e2eNotSupported() {
20
24
  throw new Error('Sign-in and sign-out are not supported in e2e testing.');
@@ -14,8 +14,10 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- function getServerSession({ req }) {
18
- const cookieHeader = req?.headers?.cookie ?? '';
17
+ // E2E test-user injection: the session is read from the lowdefy_e2e_user
18
+ // cookie (base64-encoded JSON user object) instead of a real auth engine.
19
+ function getSession(c) {
20
+ const cookieHeader = c.req.header('cookie') ?? '';
19
21
  const match = cookieHeader.match(/lowdefy_e2e_user=([^;]+)/);
20
22
  if (!match) {
21
23
  return undefined;
@@ -30,4 +32,4 @@ function getServerSession({ req }) {
30
32
  }
31
33
  }
32
34
 
33
- export default getServerSession;
35
+ export default getSession;
@@ -0,0 +1,126 @@
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
+ import { ConfigError, LowdefyInternalError } from '@lowdefy/errors';
19
+
20
+ import createHandleError from './createHandleError.js';
21
+
22
+ // This sink is the sole writer of `error.handled`. Two callers read the flag and
23
+ // neither can set it for itself: runRoutine's catch guards on `!error.handled` to
24
+ // avoid logging a nested error twice, and the browser's handleError skips the
25
+ // round-trip to /api/client-error when it is set. runRoutine deliberately gave up
26
+ // its own assignment on the strength of this function, so the tests below pin the
27
+ // assignment and — more importantly — its position ahead of everything that can
28
+ // throw.
29
+
30
+ const keyMap = { key_1: { key: 'root.pages[0:home]', '~r': 'ref_1', '~l': 5 } };
31
+ const refMap = { ref_1: { path: 'pages/home.yaml' } };
32
+
33
+ function testContext({ logger, readConfigFile, configDirectory } = {}) {
34
+ return {
35
+ configDirectory,
36
+ logger: logger ?? { error: jest.fn() },
37
+ readConfigFile:
38
+ readConfigFile ??
39
+ jest.fn(async (path) => {
40
+ if (path === 'keyMap.json') return keyMap;
41
+ if (path === 'refMap.json') return refMap;
42
+ return null;
43
+ }),
44
+ };
45
+ }
46
+
47
+ beforeEach(() => {
48
+ console.error = jest.fn();
49
+ });
50
+
51
+ test('handleError marks the error handled and resolves its location', async () => {
52
+ const context = testContext();
53
+ const handleError = createHandleError({ context });
54
+ const error = new ConfigError('Bad config.', { configKey: 'key_1' });
55
+
56
+ await handleError(error);
57
+
58
+ expect(error.handled).toBe(true);
59
+ expect(error.source).toBe('pages/home.yaml:5');
60
+ expect(error.config).toBe('root.pages[0:home]');
61
+ expect(context.logger.error).toHaveBeenCalledWith(error);
62
+ });
63
+
64
+ test('handleError marks a LowdefyInternalError handled even though it never gets a source', async () => {
65
+ // The Decision 7 case. Location resolution is deliberately skipped for an
66
+ // internal error, so `source` stays undefined - which is why the client's
67
+ // already-logged gate reads `handled` and not `source`. Keyed on source, this
68
+ // error was POSTed back to /api/client-error and logged a second time.
69
+ const context = testContext();
70
+ const handleError = createHandleError({ context });
71
+ const error = new LowdefyInternalError('Unexpected condition.');
72
+
73
+ await handleError(error);
74
+
75
+ expect(error.handled).toBe(true);
76
+ expect(error.source).toBeUndefined();
77
+ expect(context.readConfigFile).not.toHaveBeenCalled();
78
+ expect(context.logger.error).toHaveBeenCalledWith(error);
79
+ });
80
+
81
+ test('handleError marks the error handled before it logs, so a throwing logger cannot skip the flag', async () => {
82
+ const logger = {
83
+ error: jest.fn(() => {
84
+ throw new Error('Log drain unreachable.');
85
+ }),
86
+ };
87
+ const context = testContext({ logger });
88
+ const handleError = createHandleError({ context });
89
+ const error = new ConfigError('Bad config.', { configKey: 'key_1' });
90
+
91
+ await handleError(error);
92
+
93
+ // The sink swallows its own failure - reaching it is what makes the error
94
+ // already-logged, so the flag must survive the log call failing. Were it set
95
+ // after the log, this error would cross the wire unflagged and be logged a
96
+ // second time from the browser.
97
+ expect(error.handled).toBe(true);
98
+ expect(logger.error).toHaveBeenCalled();
99
+ expect(console.error).toHaveBeenCalled();
100
+ });
101
+
102
+ test('handleError marks the error handled when location resolution rejects', async () => {
103
+ const readConfigFile = jest.fn(async () => {
104
+ throw new Error('keyMap.json unreadable.');
105
+ });
106
+ const context = testContext({ readConfigFile });
107
+ const handleError = createHandleError({ context });
108
+ const error = new ConfigError('Bad config.', { configKey: 'key_1' });
109
+
110
+ await handleError(error);
111
+
112
+ expect(error.handled).toBe(true);
113
+ // ConfigError's constructor initialises source to null; resolution failing
114
+ // leaves it there rather than assigning.
115
+ expect(error.source).toBeNull();
116
+ });
117
+
118
+ test('handleError marks a plain Error handled', async () => {
119
+ const context = testContext();
120
+ const handleError = createHandleError({ context });
121
+ const error = new Error('Something broke.');
122
+
123
+ await handleError(error);
124
+
125
+ expect(error.handled).toBe(true);
126
+ });
@@ -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;
@@ -55,6 +55,7 @@ async function createCustomPluginTypesMap({ directories }) {
55
55
  server: {},
56
56
  },
57
57
  requests: {},
58
+ websockets: {},
58
59
  };
59
60
 
60
61
  const pluginDefinitions = await getPluginDefinitions({ directories });
@@ -50,6 +50,8 @@ jest.unstable_mockModule('@lowdefy/node-utils', () => ({
50
50
  spawnProcess: jest.fn(),
51
51
  readFile: jest.fn(async () => pluginYaml),
52
52
  writeFile: jest.fn(),
53
+ writeFileIfChanged: jest.fn(),
54
+ installIfPackageJsonChanged: jest.fn(),
53
55
  }));
54
56
 
55
57
  jest.unstable_mockModule('node:module', () => ({