@lowdefy/server-dev 5.6.0 → 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/{pages/_app.js → client/App.jsx} +21 -33
  2. package/client/DevStreamContext.js +30 -0
  3. package/client/Inspector.jsx +226 -0
  4. package/{lib/client/Page.js → client/Page.jsx} +4 -8
  5. package/client/Reload.jsx +85 -0
  6. package/client/Routing.jsx +117 -0
  7. package/client/feedback/FeedbackMount.jsx +83 -0
  8. package/client/feedback/FeedbackOverlay.jsx +901 -0
  9. package/client/feedback/captureTabScreenshot.js +84 -0
  10. package/client/feedback/copyToClipboard.js +45 -0
  11. package/client/feedback/drawAnnotationsSvg.js +119 -0
  12. package/client/feedback/elementInspect.js +99 -0
  13. package/client/feedback/feedbackStyles.js +301 -0
  14. package/client/feedback/pinResponsiveImages.js +59 -0
  15. package/client/feedback/sendFeedback.js +39 -0
  16. package/client/feedback/useFeedbackToggle.js +70 -0
  17. package/client/main.jsx +42 -0
  18. package/client/openInEditor/OpenInEditorListener.jsx +268 -0
  19. package/client/openInEditor/findBlockLocation.js +51 -0
  20. package/lib/build/app.js +8 -1
  21. package/lib/build/appMeta.js +8 -1
  22. package/lib/build/auth.js +8 -1
  23. package/lib/build/config.js +8 -1
  24. package/lib/build/i18n.js +8 -1
  25. package/lib/build/logger.js +8 -1
  26. package/lib/build/theme.js +8 -1
  27. package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
  28. package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +23 -10
  29. package/lib/client/setPageId.js +6 -5
  30. package/lib/client/utils/useMutateCache.js +14 -1
  31. package/lib/client/utils/usePageConfig.js +45 -43
  32. package/lib/client/utils/usePageConfig.test.mjs +62 -0
  33. package/lib/docs/captureAnnotatedScreenshot.js +88 -0
  34. package/lib/docs/checkpointPaths.js +91 -0
  35. package/lib/docs/checkpointStore.js +240 -0
  36. package/lib/docs/checkpointToMocks.js +65 -0
  37. package/lib/docs/clientErrorStore.js +36 -0
  38. package/lib/docs/configCheckpoints.test.mjs +156 -0
  39. package/lib/docs/createConfigCheckpoint.js +92 -0
  40. package/lib/docs/createDocsMcpServer.js +531 -0
  41. package/lib/docs/createDocsMcpServer.test.mjs +141 -0
  42. package/lib/docs/devMockRegistry.js +68 -0
  43. package/lib/docs/docs.test.mjs +448 -0
  44. package/lib/docs/enrichFeedback.js +71 -0
  45. package/lib/docs/evalOperator.js +47 -0
  46. package/lib/docs/evalOperator.test.mjs +157 -0
  47. package/lib/docs/evalOperatorHeadless.js +87 -0
  48. package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
  49. package/lib/docs/evalOperatorInTab.js +48 -0
  50. package/lib/docs/findConfig.js +260 -0
  51. package/lib/docs/formatFeedback.js +121 -0
  52. package/lib/docs/formatFeedback.test.mjs +204 -0
  53. package/lib/docs/getAppMap.js +180 -0
  54. package/lib/docs/getBrowser.js +127 -0
  55. package/lib/docs/getBrowser.test.mjs +122 -0
  56. package/lib/docs/getBuildStatus.js +37 -0
  57. package/lib/docs/getCoreDoc.js +63 -0
  58. package/lib/docs/getDocsManifest.js +40 -0
  59. package/lib/docs/getExamples.js +58 -0
  60. package/lib/docs/getOverview.js +115 -0
  61. package/lib/docs/getPageConfig.js +53 -0
  62. package/lib/docs/getPluginDoc.js +55 -0
  63. package/lib/docs/getSchema.js +65 -0
  64. package/lib/docs/inspectState.js +47 -0
  65. package/lib/docs/inspectState.test.mjs +140 -0
  66. package/lib/docs/inspectStateFromTab.js +40 -0
  67. package/lib/docs/inspectStateHeadless.js +84 -0
  68. package/lib/docs/inspectStateHeadless.test.mjs +60 -0
  69. package/lib/docs/isWriteRequestsAllowed.js +50 -0
  70. package/lib/docs/listConfigCheckpoints.js +55 -0
  71. package/lib/docs/listPlugins.js +80 -0
  72. package/lib/docs/listTypes.js +105 -0
  73. package/lib/docs/loadState.js +152 -0
  74. package/lib/docs/mapPageBuildErrors.js +28 -0
  75. package/lib/docs/normalizeTypeKind.js +40 -0
  76. package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
  77. package/lib/docs/readPageArtifact.js +26 -0
  78. package/lib/docs/resolvePluginDir.js +49 -0
  79. package/lib/docs/resolveSource.js +48 -0
  80. package/lib/docs/resolveSource.test.mjs +100 -0
  81. package/lib/docs/revertConfigCheckpoint.js +73 -0
  82. package/lib/docs/runRequest.js +141 -0
  83. package/lib/docs/saveAnnotatedScreenshot.js +43 -0
  84. package/lib/docs/scaffoldPage.js +75 -0
  85. package/lib/docs/scaffoldPage.test.mjs +97 -0
  86. package/lib/docs/screenshotPage.js +110 -0
  87. package/lib/docs/screenshotPage.test.mjs +59 -0
  88. package/lib/docs/screenshotPageClip.test.mjs +102 -0
  89. package/lib/docs/searchDocs.js +67 -0
  90. package/lib/docs/setupTestFixtures.mjs +335 -0
  91. package/lib/docs/snapshotState.js +59 -0
  92. package/lib/docs/stateCheckpoints.test.mjs +366 -0
  93. package/lib/docs/tabAvailable.js +28 -0
  94. package/lib/docs/tabChannel.js +119 -0
  95. package/lib/docs/tabChannel.test.mjs +130 -0
  96. package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +7 -4
  97. package/lib/server/auth/getDevSession.js +70 -0
  98. package/lib/server/auth/getDevSession.test.mjs +138 -0
  99. package/lib/server/auth/getHeadlessUser.js +40 -0
  100. package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
  101. package/lib/server/auth/headlessUser.js +29 -0
  102. package/lib/server/auth/resolveHeadlessUser.js +38 -0
  103. package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
  104. package/lib/server/auth/session.js +39 -0
  105. package/lib/server/auth/strategies.js +47 -0
  106. package/lib/server/createLowdefyContext.js +122 -0
  107. package/lib/server/getPageJitEnrichment.test.mjs +105 -0
  108. package/lib/server/jitPageBuilder.js +125 -12
  109. package/lib/server/log/createHandleError.test.mjs +126 -0
  110. package/manager/getContext.mjs +3 -7
  111. package/manager/processes/initialBuild.mjs +0 -2
  112. package/manager/processes/installPlugins.mjs +19 -11
  113. package/manager/processes/lowdefyBuild.mjs +29 -7
  114. package/manager/processes/lowdefyBuild.test.mjs +102 -0
  115. package/manager/processes/readDotEnv.mjs +13 -1
  116. package/manager/processes/shutdownServer.mjs +6 -6
  117. package/manager/processes/startProxy.mjs +160 -0
  118. package/manager/processes/startProxy.test.mjs +102 -0
  119. package/manager/processes/startServer.mjs +33 -16
  120. package/manager/processes/startWatchers.mjs +2 -2
  121. package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
  122. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
  123. package/manager/run.mjs +50 -0
  124. package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
  125. package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
  126. package/manager/utils/formatNoticeBox.mjs +30 -0
  127. package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
  128. package/manager/utils/updatePageTailwindCss.mjs +7 -1
  129. package/manager/utils/writeBuildStatus.mjs +39 -0
  130. package/manager/utils/writeBuildStatus.test.mjs +76 -0
  131. package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
  132. package/manager/watchers/moduleBuildWatcher.mjs +0 -1
  133. package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
  134. package/package.json +61 -47
  135. package/package.original.json +64 -50
  136. package/postcss.config.cjs +1 -0
  137. package/src/app.js +175 -0
  138. package/src/html/renderDevPage.js +86 -0
  139. package/src/lib/getPathSegments.js +25 -0
  140. package/src/lib/safeScriptJson.js +38 -0
  141. package/src/middleware/apiContext.js +32 -0
  142. package/src/middleware/errorHandler.js +48 -0
  143. package/src/middleware/errorHandler.test.mjs +235 -0
  144. package/src/routes/agent.js +64 -0
  145. package/src/routes/apiPage.js +37 -0
  146. package/src/routes/auth.js +49 -0
  147. package/src/routes/auth.test.mjs +84 -0
  148. package/src/routes/clientError.js +57 -0
  149. package/src/routes/cron.js +47 -0
  150. package/src/routes/cronForward.js +47 -0
  151. package/src/routes/detached.js +47 -0
  152. package/src/routes/devInspect.js +111 -0
  153. package/src/routes/devInspect.test.mjs +119 -0
  154. package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
  155. package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
  156. package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
  157. package/src/routes/docs/checkpointsCreate.js +28 -0
  158. package/src/routes/docs/checkpointsList.js +23 -0
  159. package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
  160. package/src/routes/docs/content.js +31 -0
  161. package/src/routes/docs/evalOperator.js +56 -0
  162. package/src/routes/docs/examples.js +33 -0
  163. package/src/routes/docs/find.js +30 -0
  164. package/src/routes/docs/index.js +23 -0
  165. package/src/routes/docs/inspectState.js +44 -0
  166. package/src/routes/docs/loadState.js +49 -0
  167. package/src/routes/docs/mcp.js +30 -0
  168. package/src/routes/docs/pageConfig.js +36 -0
  169. package/src/routes/docs/parseUserParam.js +52 -0
  170. package/src/routes/docs/parseUserParam.test.mjs +45 -0
  171. package/src/routes/docs/pluginDoc.js +33 -0
  172. package/src/routes/docs/plugins.js +23 -0
  173. package/src/routes/docs/runRequest.js +36 -0
  174. package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
  175. package/src/routes/docs/screenshot.js +72 -0
  176. package/src/routes/docs/search.js +27 -0
  177. package/src/routes/docs/snapshotState.js +36 -0
  178. package/src/routes/docs/stateCheckpointsList.js +26 -0
  179. package/src/routes/docs/types.js +33 -0
  180. package/src/routes/endpoints.js +71 -0
  181. package/src/routes/feedback.js +110 -0
  182. package/src/routes/feedback.test.mjs +204 -0
  183. package/src/routes/jitPage.js +86 -0
  184. package/src/routes/mcp.js +34 -0
  185. package/{pages/api → src/routes}/ping.js +4 -2
  186. package/src/routes/reload.js +79 -0
  187. package/src/routes/request.js +74 -0
  188. package/src/routes/request.test.mjs +124 -0
  189. package/{pages/api → src/routes}/root.js +4 -4
  190. package/src/routes/usage.js +46 -0
  191. package/src/routes/websocket.js +33 -0
  192. package/src/websocket/devWebSocket.js +78 -0
  193. package/vite.config.js +111 -0
  194. package/.eslintrc.yaml +0 -1
  195. package/lib/client/App.js +0 -80
  196. package/lib/client/Reload.js +0 -60
  197. package/lib/server/apiWrapper.js +0 -109
  198. package/lib/server/compileCss.js +0 -39
  199. package/manager/processes/compileCss.mjs +0 -54
  200. package/manager/processes/nextBuild.mjs +0 -50
  201. package/next.config.js +0 -25
  202. package/pages/_document.js +0 -109
  203. package/pages/api/agent/[...path].js +0 -86
  204. package/pages/api/auth/[...nextauth].js +0 -48
  205. package/pages/api/client-error.js +0 -45
  206. package/pages/api/js/[env].js +0 -42
  207. package/pages/api/page/[...pageId].js +0 -74
  208. package/pages/api/reload.js +0 -53
  209. package/pages/api/request/[...path].js +0 -41
  210. /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
  211. /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
  212. /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
  213. /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
  214. /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
@@ -0,0 +1,152 @@
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 { type } from '@lowdefy/helpers';
18
+
19
+ import lowdefyConfig from '../build/config.js';
20
+ import { getBrowser, openPage } from './getBrowser.js';
21
+ import { loadMocks } from './devMockRegistry.js';
22
+ import { readCheckpoint } from './checkpointStore.js';
23
+
24
+ const VERIFY_KEY_COUNT = 3;
25
+
26
+ // Loads a state checkpoint's recorded requests into devMockRegistry (shared
27
+ // by both modes below — src/routes/request.js consults it regardless of how
28
+ // the page was opened), then either:
29
+ // - 'headless': drives a headless page to the checkpoint's pageId/urlQuery,
30
+ // injects its recorded state directly into the live context, and verifies
31
+ // a few keys round-tripped. Good for an agent verifying its own change.
32
+ // Takes a `user` so a checkpoint captured on a role-gated page restores
33
+ // into a page that actually renders (see resolveHeadlessUser.js).
34
+ // - 'registry-only': just returns a URL a human can open in a real browser
35
+ // tab — client/Inspector.jsx's `?_checkpoint=` bootstrap does the state
36
+ // injection client-side once that tab loads.
37
+ async function loadState({ origin, name, mode = 'headless', user }) {
38
+ if (type.isNone(name) || !type.isString(name)) {
39
+ return { error: `loadState requires a "name" string. Received ${JSON.stringify(name)}.` };
40
+ }
41
+
42
+ // `user` only reaches a page this function opens itself. In 'registry-only'
43
+ // mode the developer opens the returned URL in their own browser, carrying
44
+ // their own session, so an injected caller would be silently dropped.
45
+ if (!type.isNone(user) && mode === 'registry-only') {
46
+ return {
47
+ error:
48
+ 'loadState cannot apply "user" in "registry-only" mode — the developer opens the returned URL in their own browser, carrying their own session. Use the default "headless" mode.',
49
+ invalidInput: true,
50
+ };
51
+ }
52
+
53
+ let checkpoint;
54
+ try {
55
+ checkpoint = readCheckpoint({ name });
56
+ } catch (error) {
57
+ return { error: error.message };
58
+ }
59
+
60
+ const pageId = checkpoint.checkpoint.pageId;
61
+ if (type.isNone(pageId)) {
62
+ return { error: `Checkpoint "${name}" has no recorded pageId.` };
63
+ }
64
+
65
+ loadMocks({ pageId, mocks: checkpoint.requests });
66
+
67
+ const basePath = lowdefyConfig.basePath ?? '';
68
+ const urlQuery = checkpoint.urlQuery ?? '';
69
+
70
+ if (mode === 'registry-only') {
71
+ const separator = urlQuery.includes('?') ? '&' : '?';
72
+ const url = `${origin}${basePath}/${pageId}${urlQuery}${separator}_checkpoint=${encodeURIComponent(
73
+ name
74
+ )}`;
75
+ return {
76
+ url,
77
+ instructions:
78
+ 'Open this URL in a browser — state is injected on load, requests serve recorded data.',
79
+ };
80
+ }
81
+
82
+ if (mode !== 'headless') {
83
+ return {
84
+ error: `loadState "mode" must be "headless" or "registry-only". Received ${JSON.stringify(
85
+ mode
86
+ )}.`,
87
+ };
88
+ }
89
+
90
+ if (type.isNone(origin) || !type.isString(origin)) {
91
+ return { error: `loadState requires an "origin" string. Received ${JSON.stringify(origin)}.` };
92
+ }
93
+
94
+ let browser;
95
+ try {
96
+ browser = await getBrowser();
97
+ } catch (error) {
98
+ return {
99
+ error: `No Chromium available. Run: npx playwright install chromium (${error.message})`,
100
+ };
101
+ }
102
+
103
+ let context;
104
+ try {
105
+ // buildPageUrl (used internally by openPage) joins origin/basePath/pageId
106
+ // as a plain string, so folding the query string into the pageId segment
107
+ // reproduces the checkpoint's exact URL without needing a query-aware
108
+ // variant of openPage.
109
+ const opened = await openPage({ browser, origin, pageId: `${pageId}${urlQuery}`, user });
110
+ context = opened.context;
111
+ const { page } = opened;
112
+
113
+ await page.waitForFunction((id) => Boolean(window.lowdefy?.contexts?.[`page:${id}`]), pageId, {
114
+ timeout: 15000,
115
+ });
116
+
117
+ const stateEntries = Object.entries(checkpoint.state ?? {});
118
+ await page.evaluate(
119
+ ({ id, entries }) => {
120
+ const pageContext = window.lowdefy.contexts[`page:${id}`];
121
+ entries.forEach(([key, value]) => {
122
+ pageContext._internal.State.set(key, value);
123
+ });
124
+ pageContext._internal.update();
125
+ },
126
+ { id: pageId, entries: stateEntries }
127
+ );
128
+
129
+ const verifyKeys = stateEntries.slice(0, VERIFY_KEY_COUNT).map(([key]) => key);
130
+ const verifiedKeys = await page.evaluate(
131
+ ({ id, keys }) => {
132
+ const pageContext = window.lowdefy.contexts[`page:${id}`];
133
+ const result = {};
134
+ keys.forEach((key) => {
135
+ result[key] = pageContext.state?.[key];
136
+ });
137
+ return result;
138
+ },
139
+ { id: pageId, keys: verifyKeys }
140
+ );
141
+
142
+ return { loaded: true, mode, url: opened.url, verifiedKeys };
143
+ } catch (error) {
144
+ return { error: `Failed to load checkpoint "${name}": ${error.message}` };
145
+ } finally {
146
+ if (context) {
147
+ await context.close();
148
+ }
149
+ }
150
+ }
151
+
152
+ export default loadState;
@@ -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
+ // Mirrors the error mapping in src/routes/jitPage.js so agent-facing page
18
+ // config responses carry the same shape as the dev client's JIT build errors.
19
+ function mapPageBuildErrors(error) {
20
+ const rawErrors = error.buildErrors ?? [error];
21
+ return rawErrors.map((err) => ({
22
+ type: err.name ?? 'Error',
23
+ message: err.message,
24
+ source: err.source ?? null,
25
+ }));
26
+ }
27
+
28
+ export default mapPageBuildErrors;
@@ -0,0 +1,40 @@
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
+ const KINDS = {
18
+ action: 'actions',
19
+ actions: 'actions',
20
+ agent: 'agents',
21
+ agents: 'agents',
22
+ block: 'blocks',
23
+ blocks: 'blocks',
24
+ connection: 'connections',
25
+ connections: 'connections',
26
+ notification: 'notifications',
27
+ notifications: 'notifications',
28
+ operator: 'operators',
29
+ operators: 'operators',
30
+ request: 'requests',
31
+ requests: 'requests',
32
+ websocket: 'websockets',
33
+ websockets: 'websockets',
34
+ };
35
+
36
+ function normalizeTypeKind({ kind }) {
37
+ return KINDS[String(kind ?? '').toLowerCase()] ?? null;
38
+ }
39
+
40
+ export default normalizeTypeKind;
@@ -14,19 +14,22 @@
14
14
  limitations under the License.
15
15
  */
16
16
 
17
- import { callEndpoint } from '@lowdefy/api';
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import { serializer } from '@lowdefy/helpers';
18
20
 
19
- import apiWrapper from '../../../lib/server/apiWrapper.js';
20
-
21
- async function handler({ context, req, res }) {
22
- if (req.method !== 'POST') {
23
- throw new Error('Only POST requests are supported.');
21
+ // Read fresh on every call — the dev build rewrites artifacts on config
22
+ // change and the docs routes must serve the current state.
23
+ function readBuildArtifact({ name, deserialize = false }) {
24
+ const filePath = path.join(process.cwd(), 'build', name);
25
+ if (!fs.existsSync(filePath)) {
26
+ return null;
27
+ }
28
+ const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
29
+ if (deserialize) {
30
+ return serializer.deserialize(raw);
24
31
  }
25
- const endpointId = req.query.endpointId.join('/');
26
- const { blockId, payload, pageId } = req.body;
27
- context.logger.info({ event: 'call_api_endpoint', blockId, endpointId, pageId });
28
- const response = await callEndpoint(context, { blockId, endpointId, pageId, payload });
29
- res.status(200).json(response);
32
+ return raw;
30
33
  }
31
34
 
32
- export default apiWrapper(handler);
35
+ export default readBuildArtifact;
@@ -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
+ import readBuildArtifact from './readBuildArtifact.js';
18
+
19
+ // Factored out of getPageConfig so the artifact-read side (deserializing
20
+ // build/pages/<pageId>.json) is testable without exercising the JIT build
21
+ // pipeline (buildPageIfNeeded) that getPageConfig also drives.
22
+ function readPageArtifact({ pageId }) {
23
+ return readBuildArtifact({ name: `pages/${pageId}.json`, deserialize: true });
24
+ }
25
+
26
+ export default readPageArtifact;
@@ -0,0 +1,49 @@
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 'node:fs';
18
+ import { createRequire } from 'node:module';
19
+ import path from 'node:path';
20
+
21
+ // Resolve from the server directory so installed plugins (including local
22
+ // workspace: packages) resolve exactly as the server imports them.
23
+ const require = createRequire(path.join(process.cwd(), 'package.json'));
24
+
25
+ const cache = new Map();
26
+
27
+ function resolvePluginDir({ packageName }) {
28
+ if (cache.has(packageName)) {
29
+ return cache.get(packageName);
30
+ }
31
+ let dir = null;
32
+ // Installed plugins are direct deps of the server, so node_modules holds a
33
+ // link to each. Plugin exports maps rarely expose ./package.json, so
34
+ // require.resolve on it fails — check the directory first.
35
+ const linked = path.join(process.cwd(), 'node_modules', packageName);
36
+ if (fs.existsSync(path.join(linked, 'package.json'))) {
37
+ dir = fs.realpathSync(linked);
38
+ } else {
39
+ try {
40
+ dir = path.dirname(require.resolve(`${packageName}/package.json`));
41
+ } catch {
42
+ // Package not resolvable — degrade gracefully.
43
+ }
44
+ }
45
+ cache.set(packageName, dir);
46
+ return dir;
47
+ }
48
+
49
+ export default resolvePluginDir;
@@ -0,0 +1,48 @@
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 { type } from '@lowdefy/helpers';
18
+
19
+ import tabAvailable from './tabAvailable.js';
20
+
21
+ // The single tab-vs-headless decision, shared by inspectState and evalOperator
22
+ // so a change to the rule lands in both. Prefers a real, already-open browser
23
+ // tab over a fresh headless one — a live tab reflects whatever a developer is
24
+ // actually looking at (their own interactions, in-flight requests), while
25
+ // headless always starts a clean navigation.
26
+ //
27
+ // `user` is headless-only — the developer's tab carries their real session,
28
+ // which the dev server cannot re-identify — so it selects the headless source,
29
+ // and contradicts an explicit `source: 'tab'`.
30
+ //
31
+ // Returns { tryTab }, or { error, invalidInput } for a contradictory call.
32
+ // `invalidInput` marks the caller's own mistake rather than a failed render, so
33
+ // the HTTP routes can answer 400 instead of a 502 that reads as "the renderer
34
+ // broke, retry".
35
+ function resolveSource({ name, pageId, source, user }) {
36
+ const hasUser = !type.isNone(user);
37
+ if (hasUser && source === 'tab') {
38
+ return {
39
+ error: `${name} cannot apply "user" to the developer's live tab — it carries their real session. Omit "source", or use "headless".`,
40
+ invalidInput: true,
41
+ };
42
+ }
43
+ return {
44
+ tryTab: source === 'tab' || (source === undefined && !hasUser && tabAvailable({ pageId })),
45
+ };
46
+ }
47
+
48
+ export default resolveSource;
@@ -0,0 +1,100 @@
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 mockTabAvailable = jest.fn();
20
+
21
+ jest.unstable_mockModule('./tabAvailable.js', () => ({
22
+ default: mockTabAvailable,
23
+ }));
24
+
25
+ const { default: resolveSource } = await import('./resolveSource.js');
26
+
27
+ beforeEach(() => {
28
+ jest.clearAllMocks();
29
+ });
30
+
31
+ test('resolveSource tries the tab when source is "tab"', () => {
32
+ expect(resolveSource({ name: 'inspectState', pageId: 'home', source: 'tab' })).toEqual({
33
+ tryTab: true,
34
+ });
35
+ expect(mockTabAvailable).not.toHaveBeenCalled();
36
+ });
37
+
38
+ test('resolveSource does not try the tab when source is "headless"', () => {
39
+ expect(resolveSource({ name: 'inspectState', pageId: 'home', source: 'headless' })).toEqual({
40
+ tryTab: false,
41
+ });
42
+ expect(mockTabAvailable).not.toHaveBeenCalled();
43
+ });
44
+
45
+ test('resolveSource tries the tab when source is unset and a tab is available', () => {
46
+ mockTabAvailable.mockReturnValue(true);
47
+
48
+ expect(resolveSource({ name: 'inspectState', pageId: 'home' })).toEqual({ tryTab: true });
49
+ expect(mockTabAvailable).toHaveBeenCalledWith({ pageId: 'home' });
50
+ });
51
+
52
+ test('resolveSource does not try the tab when source is unset and no tab is available', () => {
53
+ mockTabAvailable.mockReturnValue(false);
54
+
55
+ expect(resolveSource({ name: 'inspectState', pageId: 'home' })).toEqual({ tryTab: false });
56
+ });
57
+
58
+ test('resolveSource selects headless over an available tab when a user is given', () => {
59
+ mockTabAvailable.mockReturnValue(true);
60
+
61
+ expect(
62
+ resolveSource({ name: 'inspectState', pageId: 'home', user: { roles: ['admin'] } })
63
+ ).toEqual({ tryTab: false });
64
+ });
65
+
66
+ test('resolveSource marks a user combined with source "tab" as invalid input', () => {
67
+ const result = resolveSource({
68
+ name: 'inspectState',
69
+ pageId: 'home',
70
+ source: 'tab',
71
+ user: { roles: ['admin'] },
72
+ });
73
+
74
+ expect(result.invalidInput).toBe(true);
75
+ expect(result.error).toBe(
76
+ 'inspectState cannot apply "user" to the developer\'s live tab — it carries their real session. Omit "source", or use "headless".'
77
+ );
78
+ expect(result.tryTab).toBeUndefined();
79
+ });
80
+
81
+ test('resolveSource names the calling function in the invalid-input error', () => {
82
+ const result = resolveSource({
83
+ name: 'evalOperator',
84
+ pageId: 'home',
85
+ source: 'tab',
86
+ user: { roles: ['admin'] },
87
+ });
88
+
89
+ expect(result.error).toMatch(/^evalOperator cannot apply "user"/);
90
+ });
91
+
92
+ test('resolveSource treats a null user as no user', () => {
93
+ mockTabAvailable.mockReturnValue(true);
94
+
95
+ expect(
96
+ resolveSource({ name: 'inspectState', pageId: 'home', source: 'tab', user: null })
97
+ ).toEqual({
98
+ tryTab: true,
99
+ });
100
+ });
@@ -0,0 +1,73 @@
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 { type } from '@lowdefy/helpers';
20
+
21
+ import {
22
+ getCheckpointsRoot,
23
+ getConfigDirectory,
24
+ listConfigFiles,
25
+ MANIFEST_FILE_NAME,
26
+ } from './checkpointPaths.js';
27
+
28
+ function copyFromSnapshot({ snapshotDirectory, configDirectory, relativeFilePath }) {
29
+ const source = path.join(snapshotDirectory, relativeFilePath);
30
+ const destination = path.join(configDirectory, relativeFilePath);
31
+ fs.mkdirSync(path.dirname(destination), { recursive: true });
32
+ fs.copyFileSync(source, destination);
33
+ }
34
+
35
+ // Restores the config directory to exactly the state captured by checkpoint
36
+ // `id`: every file the manifest lists is copied back (covering both
37
+ // modified files and files deleted after the checkpoint was taken), and any
38
+ // config-relevant file that exists now but isn't in the manifest is deleted
39
+ // (covering files added after the checkpoint). The manifest — not a diff
40
+ // against the live directory — is the single source of truth for what
41
+ // "restored" means, so reverts are exact and repeatable.
42
+ function revertConfigCheckpoint({ id }) {
43
+ if (type.isNone(id) || !type.isString(id)) {
44
+ throw new Error(
45
+ `revertConfigCheckpoint requires an "id" string. Received ${JSON.stringify(id)}.`
46
+ );
47
+ }
48
+
49
+ const checkpointsRoot = getCheckpointsRoot();
50
+ const snapshotDirectory = path.join(checkpointsRoot, id);
51
+ const manifestPath = path.join(snapshotDirectory, MANIFEST_FILE_NAME);
52
+ if (!fs.existsSync(manifestPath)) {
53
+ throw new Error(`Config checkpoint "${id}" not found.`);
54
+ }
55
+
56
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
57
+ const configDirectory = getConfigDirectory();
58
+
59
+ manifest.files.forEach((relativeFilePath) => {
60
+ copyFromSnapshot({ snapshotDirectory, configDirectory, relativeFilePath });
61
+ });
62
+
63
+ const manifestFiles = new Set(manifest.files);
64
+ const currentFiles = listConfigFiles({ configDirectory });
65
+ const deleted = currentFiles.filter((relativeFilePath) => !manifestFiles.has(relativeFilePath));
66
+ deleted.forEach((relativeFilePath) => {
67
+ fs.rmSync(path.join(configDirectory, relativeFilePath));
68
+ });
69
+
70
+ return { restored: manifest.files, deleted };
71
+ }
72
+
73
+ export default revertConfigCheckpoint;