@lowdefy/server-dev 0.0.0-experimental-20260720072521 → 0.0.0-experimental-20260720135014

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 (196) hide show
  1. package/.eslintrc.yaml +1 -0
  2. package/lib/build/app.js +1 -8
  3. package/lib/build/appMeta.js +1 -8
  4. package/lib/build/auth.js +1 -8
  5. package/lib/build/config.js +1 -8
  6. package/lib/build/i18n.js +1 -8
  7. package/lib/build/logger.js +1 -8
  8. package/lib/build/theme.js +1 -8
  9. package/lib/client/App.js +80 -0
  10. package/{client/Page.jsx → lib/client/Page.js} +5 -5
  11. package/{client/Reload.jsx → lib/client/Reload.js} +7 -5
  12. package/lib/client/auth/{Auth.jsx → Auth.js} +5 -12
  13. package/lib/client/auth/AuthConfigured.js +61 -0
  14. package/lib/client/auth/AuthNotConfigured.js +2 -22
  15. package/lib/client/setPageId.js +5 -6
  16. package/lib/client/utils/useMutateCache.js +1 -14
  17. package/lib/client/utils/usePageConfig.js +11 -31
  18. package/lib/server/apiWrapper.js +116 -0
  19. package/lib/server/auth/{getStrategies.js → getAuthOptions.js} +9 -14
  20. package/lib/server/auth/{getMockUser.js → getMockSession.js} +19 -10
  21. package/{src/routes/docs/checkpointsRevert.js → lib/server/auth/getServerSession.js} +13 -11
  22. package/lib/server/compileCss.js +39 -0
  23. package/lib/server/jitPageBuilder.js +11 -36
  24. package/lib/server/log/logRequest.js +3 -1
  25. package/manager/getContext.mjs +7 -3
  26. package/manager/processes/compileCss.mjs +54 -0
  27. package/manager/processes/initialBuild.mjs +2 -0
  28. package/manager/processes/installPlugins.mjs +11 -19
  29. package/manager/processes/lowdefyBuild.mjs +7 -29
  30. package/manager/processes/nextBuild.mjs +50 -0
  31. package/manager/processes/readDotEnv.mjs +1 -13
  32. package/manager/processes/shutdownServer.mjs +6 -6
  33. package/manager/processes/startServer.mjs +7 -8
  34. package/manager/processes/startWatchers.mjs +2 -2
  35. package/manager/run.mjs +0 -41
  36. package/manager/utils/createCustomPluginMessagesMap.mjs +1 -8
  37. package/manager/utils/createCustomPluginTypesMap.mjs +2 -3
  38. package/manager/utils/{getViteBin.mjs → getNextBin.mjs} +10 -7
  39. package/manager/utils/updatePageTailwindCss.mjs +1 -7
  40. package/manager/watchers/lowdefyBuildWatcher.mjs +1 -0
  41. package/manager/watchers/moduleBuildWatcher.mjs +1 -0
  42. package/manager/watchers/{serverArtifactWatcher.mjs → nextBuildWatcher.mjs} +22 -18
  43. package/next.config.js +25 -0
  44. package/package.json +47 -63
  45. package/package.original.json +55 -66
  46. package/{src/routes/devTools.js → pages/404.js} +2 -4
  47. package/{src/routes/docs/appMap.js → pages/[[...pageId]].js} +2 -6
  48. package/{client/App.jsx → pages/_app.js} +33 -21
  49. package/pages/_document.js +107 -0
  50. package/pages/api/agent/[...path].js +86 -0
  51. package/pages/api/auth/[...nextauth].js +48 -0
  52. package/pages/api/client-error.js +45 -0
  53. package/{src/routes/docs/buildStatus.js → pages/api/dev-tools.js} +4 -5
  54. package/{lib/docs/clientErrorStore.js → pages/api/endpoints/[...endpointId].js} +11 -15
  55. package/{src/lib/serveBuildJs.js → pages/api/icons/dynamic.js} +12 -8
  56. package/{src/routes/jsEnv.js → pages/api/js/[env].js} +20 -8
  57. package/pages/api/page/[...pageId].js +74 -0
  58. package/{src/routes → pages/api}/ping.js +2 -4
  59. package/pages/api/reload.js +53 -0
  60. package/pages/api/request/[...path].js +41 -0
  61. package/{src/routes → pages/api}/root.js +4 -4
  62. package/client/Inspector.jsx +0 -206
  63. package/client/Routing.jsx +0 -106
  64. package/client/feedback/FeedbackMount.jsx +0 -83
  65. package/client/feedback/FeedbackOverlay.jsx +0 -901
  66. package/client/feedback/captureTabScreenshot.js +0 -84
  67. package/client/feedback/copyToClipboard.js +0 -45
  68. package/client/feedback/drawAnnotationsSvg.js +0 -119
  69. package/client/feedback/elementInspect.js +0 -99
  70. package/client/feedback/feedbackStyles.js +0 -301
  71. package/client/feedback/pinResponsiveImages.js +0 -59
  72. package/client/feedback/sendFeedback.js +0 -39
  73. package/client/feedback/useFeedbackToggle.js +0 -70
  74. package/client/main.jsx +0 -42
  75. package/client/openInEditor/OpenInEditorListener.jsx +0 -261
  76. package/client/openInEditor/findBlockLocation.js +0 -51
  77. package/lib/client/auth/AuthConfigured.jsx +0 -155
  78. package/lib/docs/captureAnnotatedScreenshot.js +0 -88
  79. package/lib/docs/checkpointPaths.js +0 -91
  80. package/lib/docs/checkpointStore.js +0 -234
  81. package/lib/docs/checkpointToMocks.js +0 -65
  82. package/lib/docs/configCheckpoints.test.mjs +0 -156
  83. package/lib/docs/createConfigCheckpoint.js +0 -92
  84. package/lib/docs/createDocsMcpServer.js +0 -506
  85. package/lib/docs/createDocsMcpServer.test.mjs +0 -123
  86. package/lib/docs/devMockRegistry.js +0 -68
  87. package/lib/docs/docs.test.mjs +0 -448
  88. package/lib/docs/enrichFeedback.js +0 -71
  89. package/lib/docs/evalOperator.js +0 -39
  90. package/lib/docs/evalOperator.test.mjs +0 -112
  91. package/lib/docs/evalOperatorHeadless.js +0 -87
  92. package/lib/docs/evalOperatorHeadless.test.mjs +0 -72
  93. package/lib/docs/evalOperatorInTab.js +0 -48
  94. package/lib/docs/findConfig.js +0 -260
  95. package/lib/docs/formatFeedback.js +0 -121
  96. package/lib/docs/formatFeedback.test.mjs +0 -204
  97. package/lib/docs/getAppMap.js +0 -180
  98. package/lib/docs/getBrowser.js +0 -115
  99. package/lib/docs/getBuildStatus.js +0 -37
  100. package/lib/docs/getCoreDoc.js +0 -63
  101. package/lib/docs/getDocsManifest.js +0 -40
  102. package/lib/docs/getExamples.js +0 -58
  103. package/lib/docs/getOverview.js +0 -115
  104. package/lib/docs/getPageConfig.js +0 -53
  105. package/lib/docs/getPluginDoc.js +0 -55
  106. package/lib/docs/getSchema.js +0 -65
  107. package/lib/docs/inspectState.js +0 -41
  108. package/lib/docs/inspectState.test.mjs +0 -91
  109. package/lib/docs/inspectStateFromTab.js +0 -40
  110. package/lib/docs/inspectStateHeadless.js +0 -84
  111. package/lib/docs/inspectStateHeadless.test.mjs +0 -60
  112. package/lib/docs/isWriteRequestsAllowed.js +0 -50
  113. package/lib/docs/listConfigCheckpoints.js +0 -55
  114. package/lib/docs/listPlugins.js +0 -80
  115. package/lib/docs/listTypes.js +0 -105
  116. package/lib/docs/loadState.js +0 -139
  117. package/lib/docs/mapPageBuildErrors.js +0 -28
  118. package/lib/docs/normalizeTypeKind.js +0 -40
  119. package/lib/docs/readBuildArtifact.js +0 -35
  120. package/lib/docs/readPageArtifact.js +0 -26
  121. package/lib/docs/resolvePluginDir.js +0 -49
  122. package/lib/docs/revertConfigCheckpoint.js +0 -73
  123. package/lib/docs/runRequest.js +0 -141
  124. package/lib/docs/saveAnnotatedScreenshot.js +0 -43
  125. package/lib/docs/scaffoldPage.js +0 -75
  126. package/lib/docs/scaffoldPage.test.mjs +0 -97
  127. package/lib/docs/screenshotPage.js +0 -109
  128. package/lib/docs/screenshotPage.test.mjs +0 -59
  129. package/lib/docs/screenshotPageClip.test.mjs +0 -102
  130. package/lib/docs/searchDocs.js +0 -67
  131. package/lib/docs/setupTestFixtures.mjs +0 -335
  132. package/lib/docs/snapshotState.js +0 -59
  133. package/lib/docs/stateCheckpoints.test.mjs +0 -348
  134. package/lib/docs/tabAvailable.js +0 -28
  135. package/lib/docs/tabChannel.js +0 -119
  136. package/lib/docs/tabChannel.test.mjs +0 -130
  137. package/lib/server/auth/createSystemContext.js +0 -70
  138. package/lib/server/auth/getAuth.js +0 -46
  139. package/lib/server/auth/getHeadlessUser.js +0 -39
  140. package/lib/server/auth/headlessUser.js +0 -30
  141. package/lib/server/createLowdefyContext.js +0 -133
  142. package/lib/server/loadDynamicJsMap.js +0 -47
  143. package/manager/processes/lowdefyBuild.test.mjs +0 -102
  144. package/manager/utils/formatNoticeBox.mjs +0 -30
  145. package/manager/utils/writeBuildStatus.mjs +0 -39
  146. package/manager/utils/writeBuildStatus.test.mjs +0 -76
  147. package/postcss.config.cjs +0 -1
  148. package/src/app.js +0 -188
  149. package/src/html/renderDevPage.js +0 -86
  150. package/src/lib/getPathSegments.js +0 -25
  151. package/src/lib/safeScriptJson.js +0 -38
  152. package/src/middleware/apiContext.js +0 -32
  153. package/src/middleware/errorHandler.js +0 -55
  154. package/src/routes/agent.js +0 -64
  155. package/src/routes/auth.js +0 -43
  156. package/src/routes/clientError.js +0 -57
  157. package/src/routes/cron.js +0 -44
  158. package/src/routes/detached.js +0 -51
  159. package/src/routes/devInspect.js +0 -94
  160. package/src/routes/docs/checkpointsCreate.js +0 -28
  161. package/src/routes/docs/checkpointsList.js +0 -23
  162. package/src/routes/docs/content.js +0 -31
  163. package/src/routes/docs/evalOperator.js +0 -46
  164. package/src/routes/docs/examples.js +0 -33
  165. package/src/routes/docs/find.js +0 -30
  166. package/src/routes/docs/index.js +0 -23
  167. package/src/routes/docs/inspectState.js +0 -34
  168. package/src/routes/docs/loadState.js +0 -38
  169. package/src/routes/docs/mcp.js +0 -30
  170. package/src/routes/docs/pageConfig.js +0 -36
  171. package/src/routes/docs/pluginDoc.js +0 -33
  172. package/src/routes/docs/plugins.js +0 -23
  173. package/src/routes/docs/runRequest.js +0 -34
  174. package/src/routes/docs/schema.js +0 -37
  175. package/src/routes/docs/screenshot.js +0 -66
  176. package/src/routes/docs/search.js +0 -27
  177. package/src/routes/docs/snapshotState.js +0 -36
  178. package/src/routes/docs/stateCheckpointsList.js +0 -26
  179. package/src/routes/docs/types.js +0 -33
  180. package/src/routes/endpoints.js +0 -76
  181. package/src/routes/feedback.js +0 -110
  182. package/src/routes/feedback.test.mjs +0 -204
  183. package/src/routes/iconsDynamic.js +0 -25
  184. package/src/routes/jitPage.js +0 -99
  185. package/src/routes/reload.js +0 -81
  186. package/src/routes/request.js +0 -66
  187. package/src/routes/usage.js +0 -46
  188. package/src/routes/user.js +0 -27
  189. package/src/routes/websocket.js +0 -33
  190. package/src/websocket/devWebSocket.js +0 -78
  191. package/vite.config.js +0 -111
  192. /package/lib/client/{BuildErrorPage.jsx → BuildErrorPage.js} +0 -0
  193. /package/lib/client/{BuildingPage.jsx → BuildingPage.js} +0 -0
  194. /package/lib/client/{ErrorBar.jsx → ErrorBar.js} +0 -0
  195. /package/lib/client/{InstallingPluginsPage.jsx → InstallingPluginsPage.js} +0 -0
  196. /package/lib/client/{RestartingPage.jsx → RestartingPage.js} +0 -0
@@ -1,71 +0,0 @@
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 findConfig from './findConfig.js';
20
-
21
- // Resolves each annotation's target.blockId to a config source location
22
- // (file:line) via findConfig, which force-builds the batch's page — so an
23
- // annotation drawn on a block the agent hasn't looked at yet still resolves.
24
- // Runs before the batch is queued (src/routes/feedback.js) so
25
- // formatFeedback.js can stay a pure string formatter with no build-artifact
26
- // access of its own.
27
- async function enrichFeedback({ batch }) {
28
- const annotations = await Promise.all(
29
- (batch.annotations ?? []).map((annotation) => enrichAnnotation({ annotation, batch }))
30
- );
31
- return { ...batch, annotations };
32
- }
33
-
34
- async function enrichAnnotation({ annotation, batch }) {
35
- const blockId = annotation.target?.blockId;
36
- if (type.isNone(blockId)) {
37
- return annotation;
38
- }
39
- try {
40
- // Dynamically generated blocks (Dynamic endpoints, list items) have ids
41
- // that exist only at runtime — walk up the ancestor chain until an id
42
- // resolves in config, so the annotation still points at the yaml that
43
- // produces the block.
44
- const candidates = [blockId, ...(annotation.target?.ancestorBlockIds ?? []).slice(1)];
45
- for (const candidateId of candidates) {
46
- const result = await findConfig({ id: candidateId, pageId: batch.pageId });
47
- const location = result.matches?.[0]?.location;
48
- if (location) {
49
- if (candidateId !== blockId) {
50
- return {
51
- ...annotation,
52
- location: { ...location, resolvedVia: candidateId },
53
- };
54
- }
55
- return { ...annotation, location };
56
- }
57
- }
58
- return {
59
- ...annotation,
60
- location: {
61
- note: `Block is generated at runtime — no configured ancestor found on page "${batch.pageId}".`,
62
- },
63
- };
64
- } catch (error) {
65
- // Never drop feedback because location resolution failed — the
66
- // developer's annotation is still useful without a file:line pointer.
67
- return { ...annotation, location: { note: `Failed to resolve location: ${error.message}` } };
68
- }
69
- }
70
-
71
- export default enrichFeedback;
@@ -1,39 +0,0 @@
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 evalOperatorInTab from './evalOperatorInTab.js';
18
- import evalOperatorHeadless from './evalOperatorHeadless.js';
19
- import tabAvailable from './tabAvailable.js';
20
-
21
- // Prefers a real, already-open browser tab over a fresh headless one - see
22
- // inspectState.js for the rationale. Falls back to headless whenever the tab
23
- // path isn't usable (no tab connected, or `source: 'tab'` was requested but
24
- // it errored) so agents always get an answer.
25
- async function evalOperator({ origin, pageId, expression, source }) {
26
- const shouldTryTab = source === 'tab' || (source === undefined && tabAvailable({ pageId }));
27
-
28
- if (shouldTryTab) {
29
- const result = await evalOperatorInTab({ pageId, expression });
30
- if (!result?.error) {
31
- return { ...result, source: 'tab' };
32
- }
33
- }
34
-
35
- const result = await evalOperatorHeadless({ origin, pageId, expression });
36
- return { ...result, source: 'headless' };
37
- }
38
-
39
- export default evalOperator;
@@ -1,112 +0,0 @@
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
- // evalOperator.js only chooses between the tab and headless sources - the
20
- // sources' own behavior is covered by evalOperatorHeadless.test.mjs and (once
21
- // written) evalOperatorInTab's own tests. Mock both, plus tabAvailable, so
22
- // only the fallback logic is under test here.
23
- const mockEvalOperatorInTab = jest.fn();
24
- const mockEvalOperatorHeadless = jest.fn();
25
- const mockTabAvailable = jest.fn();
26
-
27
- jest.unstable_mockModule('./evalOperatorInTab.js', () => ({
28
- default: mockEvalOperatorInTab,
29
- }));
30
- jest.unstable_mockModule('./evalOperatorHeadless.js', () => ({
31
- default: mockEvalOperatorHeadless,
32
- }));
33
- jest.unstable_mockModule('./tabAvailable.js', () => ({
34
- default: mockTabAvailable,
35
- }));
36
-
37
- const { default: evalOperator } = await import('./evalOperator.js');
38
-
39
- beforeEach(() => {
40
- jest.clearAllMocks();
41
- });
42
-
43
- const expression = { _state: 'name' };
44
-
45
- test('evalOperator uses the tab when source is "tab" and it succeeds', async () => {
46
- mockEvalOperatorInTab.mockResolvedValue({ value: 'Jane', errors: [] });
47
-
48
- const result = await evalOperator({
49
- origin: 'http://localhost:3001',
50
- pageId: 'home',
51
- expression,
52
- source: 'tab',
53
- });
54
-
55
- expect(result).toEqual({ value: 'Jane', errors: [], source: 'tab' });
56
- expect(mockEvalOperatorInTab).toHaveBeenCalledWith({ pageId: 'home', expression });
57
- expect(mockEvalOperatorHeadless).not.toHaveBeenCalled();
58
- });
59
-
60
- test('evalOperator falls back to headless when source is "tab" but the tab errors', async () => {
61
- mockEvalOperatorInTab.mockResolvedValue({ error: 'No browser tab connected.' });
62
- mockEvalOperatorHeadless.mockResolvedValue({ value: 'Jane', errors: [] });
63
-
64
- const result = await evalOperator({
65
- origin: 'http://localhost:3001',
66
- pageId: 'home',
67
- expression,
68
- source: 'tab',
69
- });
70
-
71
- expect(result).toEqual({ value: 'Jane', errors: [], source: 'headless' });
72
- expect(mockEvalOperatorHeadless).toHaveBeenCalledWith({
73
- origin: 'http://localhost:3001',
74
- pageId: 'home',
75
- expression,
76
- });
77
- });
78
-
79
- test('evalOperator goes straight to headless when source is "headless"', async () => {
80
- mockEvalOperatorHeadless.mockResolvedValue({ value: 'Jane', errors: [] });
81
-
82
- const result = await evalOperator({
83
- origin: 'http://localhost:3001',
84
- pageId: 'home',
85
- expression,
86
- source: 'headless',
87
- });
88
-
89
- expect(result).toEqual({ value: 'Jane', errors: [], source: 'headless' });
90
- expect(mockTabAvailable).not.toHaveBeenCalled();
91
- expect(mockEvalOperatorInTab).not.toHaveBeenCalled();
92
- });
93
-
94
- test('evalOperator prefers the tab when source is unset and a tab is available', async () => {
95
- mockTabAvailable.mockReturnValue(true);
96
- mockEvalOperatorInTab.mockResolvedValue({ value: 'Jane', errors: [] });
97
-
98
- const result = await evalOperator({ origin: 'http://localhost:3001', pageId: 'home', expression });
99
-
100
- expect(result).toEqual({ value: 'Jane', errors: [], source: 'tab' });
101
- expect(mockEvalOperatorHeadless).not.toHaveBeenCalled();
102
- });
103
-
104
- test('evalOperator goes straight to headless when source is unset and no tab is available', async () => {
105
- mockTabAvailable.mockReturnValue(false);
106
- mockEvalOperatorHeadless.mockResolvedValue({ value: 'Jane', errors: [] });
107
-
108
- const result = await evalOperator({ origin: 'http://localhost:3001', pageId: 'home', expression });
109
-
110
- expect(result).toEqual({ value: 'Jane', errors: [], source: 'headless' });
111
- expect(mockEvalOperatorInTab).not.toHaveBeenCalled();
112
- });
@@ -1,87 +0,0 @@
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 { getBrowser, openPage, buildPageUrl } from './getBrowser.js';
20
-
21
- // Evaluates an operator expression against the live client state of a
22
- // headless Chromium tab navigated to the page's own route, using the page's
23
- // own WebParser instance so results match runtime exactly. Mirrors
24
- // Inspector.jsx's evalExpression (the live-tab equivalent).
25
- async function evalOperatorHeadless({ origin, pageId, expression, timeout = 15000 }) {
26
- if (type.isNone(origin) || !type.isString(origin)) {
27
- return {
28
- error: `evalOperatorHeadless requires an "origin" string. Received ${JSON.stringify(origin)}.`,
29
- };
30
- }
31
- if (type.isNone(pageId) || !type.isString(pageId)) {
32
- return {
33
- error: `evalOperatorHeadless requires a "pageId" string. Received ${JSON.stringify(pageId)}.`,
34
- };
35
- }
36
- if (type.isNone(expression)) {
37
- return { error: 'evalOperatorHeadless requires an "expression".' };
38
- }
39
-
40
- let browser;
41
- try {
42
- browser = await getBrowser();
43
- } catch (error) {
44
- return {
45
- error: `No Chromium available. Run: npx playwright install chromium (${error.message})`,
46
- };
47
- }
48
-
49
- const url = buildPageUrl({ origin, pageId });
50
-
51
- let context;
52
- try {
53
- const opened = await openPage({ browser, origin, pageId, timeout });
54
- context = opened.context;
55
- // Passed through as JSON so the page-side parser always receives a plain
56
- // value, matching how it arrives at Inspector.jsx's eval-request handler.
57
- const expressionJson = JSON.stringify(expression);
58
- const result = await opened.page.evaluate(
59
- ({ id, exprJson }) => {
60
- const lowdefy = window.lowdefy;
61
- const pageContext = lowdefy?.contexts?.[`page:${id}`];
62
- if (!pageContext) {
63
- return { error: `No live context for page "${id}".` };
64
- }
65
- const input = JSON.parse(exprJson);
66
- const { output, errors } = pageContext._internal.parser.parse({
67
- input,
68
- location: 'agent_eval',
69
- });
70
- return {
71
- value: output === undefined ? undefined : JSON.parse(JSON.stringify(output)),
72
- errors: errors.map((error) => error.message),
73
- };
74
- },
75
- { id: pageId, exprJson: expressionJson }
76
- );
77
- return result;
78
- } catch (error) {
79
- return { error: `Failed to evaluate operator at "${url}": ${error.message}` };
80
- } finally {
81
- if (context) {
82
- await context.close();
83
- }
84
- }
85
- }
86
-
87
- export default evalOperatorHeadless;
@@ -1,72 +0,0 @@
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 os from 'os';
19
- import path from 'path';
20
- import { jest } from '@jest/globals';
21
-
22
- // A real headless evaluation needs an installed browser, which jest can't
23
- // rely on, so this only covers the error paths: bad input, and
24
- // chromium.launch failing (the "no browser installed" case). Mirrors
25
- // screenshotPage.test.mjs, which exercises the same getBrowser.js singleton.
26
- jest.unstable_mockModule('playwright-core', () => ({
27
- chromium: { launch: jest.fn().mockRejectedValue(new Error("Executable doesn't exist")) },
28
- }));
29
-
30
- // lib/build/config.js reads build/config.json from process.cwd() at import
31
- // time — chdir into a fixture that has one before getBrowser.js (which
32
- // imports it transitively) is loaded. Mirrors the pattern in
33
- // screenshotPage.test.mjs.
34
- const originalCwd = process.cwd();
35
- const fixtureDir = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-eval-operator-test-'));
36
- fs.mkdirSync(path.join(fixtureDir, 'build'), { recursive: true });
37
- fs.writeFileSync(path.join(fixtureDir, 'build', 'config.json'), JSON.stringify({ basePath: '' }));
38
- process.chdir(fixtureDir);
39
-
40
- const { default: evalOperatorHeadless } = await import('./evalOperatorHeadless.js');
41
-
42
- afterAll(() => {
43
- process.chdir(originalCwd);
44
- fs.rmSync(fixtureDir, { recursive: true, force: true });
45
- });
46
-
47
- test('evalOperatorHeadless returns an error when origin is missing', async () => {
48
- const result = await evalOperatorHeadless({ pageId: 'home', expression: { _state: 'x' } });
49
- expect(result.error).toMatch(/requires an "origin" string/);
50
- });
51
-
52
- test('evalOperatorHeadless returns an error when pageId is missing', async () => {
53
- const result = await evalOperatorHeadless({
54
- origin: 'http://localhost:3001',
55
- expression: { _state: 'x' },
56
- });
57
- expect(result.error).toMatch(/requires a "pageId" string/);
58
- });
59
-
60
- test('evalOperatorHeadless returns an error when expression is missing', async () => {
61
- const result = await evalOperatorHeadless({ origin: 'http://localhost:3001', pageId: 'home' });
62
- expect(result.error).toMatch(/requires an "expression"/);
63
- });
64
-
65
- test('evalOperatorHeadless returns an actionable error when no browser is available', async () => {
66
- const result = await evalOperatorHeadless({
67
- origin: 'http://localhost:3001',
68
- pageId: 'home',
69
- expression: { _state: 'x' },
70
- });
71
- expect(result.error).toMatch(/No Chromium available. Run: npx playwright install chromium/);
72
- });
@@ -1,48 +0,0 @@
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, serializer } from '@lowdefy/helpers';
18
-
19
- import { requestFromTab } from './tabChannel.js';
20
-
21
- // Evaluates an operator expression against the live client state of a page
22
- // open in a developer's real browser tab (e.g. `{ _state: 'myInput' }`),
23
- // using the page's own WebParser instance so results match runtime exactly.
24
- //
25
- // requestFromTab resolves directly with whatever the tab posted as `result`
26
- // (see tabChannel.js) — `{ value, errors }` on success, or an `{ error }`
27
- // object if no tab is connected, the request timed out, or Inspector.jsx
28
- // itself failed to evaluate the expression.
29
- async function evalOperatorInTab({ pageId, expression }) {
30
- if (type.isNone(expression)) {
31
- throw new Error('evalOperatorInTab requires an "expression".');
32
- }
33
- const response = await requestFromTab({
34
- pageId,
35
- event: 'eval-request',
36
- payload: { expression },
37
- });
38
- if (response?.error) {
39
- return { error: response.error };
40
- }
41
- const { value, errors } = response;
42
- return {
43
- value: type.isUndefined(value) ? undefined : serializer.deserializeFromString(value),
44
- errors,
45
- };
46
- }
47
-
48
- export default evalOperatorInTab;
@@ -1,260 +0,0 @@
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 path from 'node:path';
18
- import { resolveConfigLocation } from '@lowdefy/errors';
19
- import { type } from '@lowdefy/helpers';
20
-
21
- import buildPageIfNeeded from '../server/jitPageBuilder.js';
22
- import readBuildArtifact from './readBuildArtifact.js';
23
-
24
- const MAX_MATCHES = 20;
25
-
26
- function escapeRegExp(value) {
27
- return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
28
- }
29
-
30
- // Array segments in keyMap `key` paths look like `[0:home]` or
31
- // `[2:my_button:Button]` (see packages/build/src/build/addKeys.js recArray) —
32
- // match an id at either position.
33
- function buildIdPattern(id) {
34
- return new RegExp(`\\[\\d+:${escapeRegExp(id)}(:[^\\]]*)?\\]`);
35
- }
36
-
37
- // The JIT page build runs addKeys on the page object itself (see
38
- // packages/build/src/build/jit/buildPageJit.js), so a built page's keyMap
39
- // subtree is keyed `root`, `root.blocks[...]` — the key path carries NO page
40
- // segment, and every built page looks identical by prefix. The page a keyMap
41
- // entry belongs to is only recoverable structurally: walk ~k_parent up to the
42
- // subtree root and compare against the page's own root. The page's root is
43
- // found from its build artifact — block objects in pages/<pageId>.json keep
44
- // their ~k, and any of them chains up to the page root. Skeleton-style keys
45
- // (`root.pages[N:pageId]...`) carry the page segment inline, so a containment
46
- // pattern covers those.
47
- function buildPageScope({ pageId, keyMap }) {
48
- let rootKeyId = findPageRootKeyId({ pageId, keyMap });
49
- // A page built during the skeleton build (e.g. the default 404 page) chains
50
- // to the global config root, which every skeleton entry shares — useless as
51
- // a page discriminator. Those pages' keys carry the page segment inline, so
52
- // the pattern branch identifies them instead.
53
- if (!type.isNone(rootKeyId) && rootKeyId === findGlobalRootId(keyMap)) {
54
- rootKeyId = null;
55
- }
56
- return {
57
- rootKeyId,
58
- pagePattern: new RegExp(`\\bpages\\[\\d+:${escapeRegExp(pageId)}(:[^\\]]*)?\\]`),
59
- };
60
- }
61
-
62
- function findGlobalRootId(keyMap) {
63
- for (const [keyId, entry] of Object.entries(keyMap)) {
64
- if (entry?.key === 'root.pages') {
65
- return resolveChainRootId({ keyId, keyMap });
66
- }
67
- }
68
- return null;
69
- }
70
-
71
- function findPageRootKeyId({ pageId, keyMap }) {
72
- const artifact = readBuildArtifact({ name: `pages/${pageId}.json`, deserialize: true });
73
- const keyId = findFirstKeyId(artifact);
74
- if (type.isNone(keyId)) {
75
- return null;
76
- }
77
- return resolveChainRootId({ keyId, keyMap });
78
- }
79
-
80
- function findFirstKeyId(node) {
81
- if (type.isString(node?.['~k'])) {
82
- return node['~k'];
83
- }
84
- if (type.isArray(node)) {
85
- for (const item of node) {
86
- const keyId = findFirstKeyId(item);
87
- if (keyId) {
88
- return keyId;
89
- }
90
- }
91
- return null;
92
- }
93
- if (!type.isObject(node)) {
94
- return null;
95
- }
96
- for (const key of Object.keys(node)) {
97
- const keyId = findFirstKeyId(node[key]);
98
- if (keyId) {
99
- return keyId;
100
- }
101
- }
102
- return null;
103
- }
104
-
105
- // Walk ~k_parent to the top of the entry's subtree. addKeys assigns the tree
106
- // root a parent id that is never written to keyMap, so "parent not in keyMap"
107
- // is the root. The seen-set guards against a corrupt artifact cycling forever.
108
- function resolveChainRootId({ keyId, keyMap }) {
109
- let currentId = keyId;
110
- const seen = new Set();
111
- while (!seen.has(currentId)) {
112
- seen.add(currentId);
113
- const parentId = keyMap[currentId]?.['~k_parent'];
114
- if (type.isNone(parentId) || type.isUndefined(keyMap[parentId])) {
115
- return currentId;
116
- }
117
- currentId = parentId;
118
- }
119
- return currentId;
120
- }
121
-
122
- function belongsToPage({ keyId, entry, keyMap, pageScope }) {
123
- if (pageScope.pagePattern.test(entry.key)) {
124
- return true;
125
- }
126
- if (type.isNone(pageScope.rootKeyId)) {
127
- return false;
128
- }
129
- return resolveChainRootId({ keyId, keyMap }) === pageScope.rootKeyId;
130
- }
131
-
132
- function scanKeyMap({ id, keyMap, refMap, configDirectory, pageScope }) {
133
- const pattern = buildIdPattern(id);
134
- const matches = [];
135
- for (const [keyId, entry] of Object.entries(keyMap)) {
136
- if (matches.length >= MAX_MATCHES) {
137
- break;
138
- }
139
- if (!type.isString(entry?.key)) {
140
- continue;
141
- }
142
- if (!pattern.test(entry.key)) {
143
- continue;
144
- }
145
- if (pageScope && !belongsToPage({ keyId, entry, keyMap, pageScope })) {
146
- continue;
147
- }
148
- matches.push({
149
- keyPath: entry.key,
150
- location: resolveConfigLocation({ configKey: keyId, keyMap, refMap, configDirectory }),
151
- });
152
- }
153
- return matches;
154
- }
155
-
156
- // List item blocks render with array indices applied to their ids
157
- // (`my_list.0.name`) while config — and therefore keyMap — holds the `$`
158
- // placeholder form (`my_list.$.name`, see applyArrayIndices in
159
- // @lowdefy/helpers). Fold numeric segments back to `$` so a runtime id from
160
- // inside a list resolves to the yaml that defines the item block instead of
161
- // missing and falling back to an ancestor (usually the list itself).
162
- function deIndexId(id) {
163
- return id.replace(/\.\d+(?=\.|$)/g, () => '.$');
164
- }
165
-
166
- function scanKeyMapWithDeIndex({ id, keyMap, refMap, configDirectory, pageScope }) {
167
- const matches = scanKeyMap({ id, keyMap, refMap, configDirectory, pageScope });
168
- if (matches.length > 0) {
169
- return matches;
170
- }
171
- const deIndexedId = deIndexId(id);
172
- if (deIndexedId === id) {
173
- return matches;
174
- }
175
- return scanKeyMap({ id: deIndexedId, keyMap, refMap, configDirectory, pageScope });
176
- }
177
-
178
- // Locates a block/request/connection/etc by id, or a page by pageId. Block
179
- // content only exists in keyMap once its page has been JIT-built (see
180
- // lib/server/jitPageBuilder.js), so pass `pageId` to force-build and scan a
181
- // specific page. Never throws on not-found — agents should be able to treat
182
- // "no matches" as a normal result and try another id or pageId.
183
- async function findConfig({ id, pageId }) {
184
- if (type.isNone(id) || !type.isString(id)) {
185
- throw new Error(
186
- `findConfig requires an "id" string. Received ${JSON.stringify(id)}. ` +
187
- 'Use GET /lowdefy-docs/find/:id.'
188
- );
189
- }
190
-
191
- const pageRegistry = readBuildArtifact({ name: 'pageRegistry.json' }) ?? {};
192
- const configDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd();
193
-
194
- // A page's root block renders with blockId === pageId, so open-in-editor
195
- // and feedback enrichment hit this branch for it — they read
196
- // matches[0].location.source, so the page result must carry a match, not
197
- // just the file field the docs/MCP consumers use.
198
- if (pageRegistry[id]) {
199
- const refPath = pageRegistry[id].refPath;
200
- const matches = [];
201
- if (type.isString(refPath)) {
202
- matches.push({
203
- keyPath: `pages[${id}]`,
204
- location: { source: path.resolve(configDirectory, refPath) },
205
- });
206
- }
207
- return { kind: 'page', pageId: id, file: refPath, matches };
208
- }
209
-
210
- if (!type.isNone(pageId)) {
211
- if (!pageRegistry[pageId]) {
212
- return {
213
- matches: [],
214
- note:
215
- `Unknown pageId "${pageId}". See GET /lowdefy-docs/find/${id} without ?pageId= ` +
216
- 'to scan all already-built pages, or check the pageId spelling.',
217
- };
218
- }
219
-
220
- const buildDirectory = path.join(process.cwd(), 'build');
221
- try {
222
- await buildPageIfNeeded({ pageId, buildDirectory, configDirectory });
223
- } catch (error) {
224
- return {
225
- matches: [],
226
- note: `Page "${pageId}" failed to build, so its config could not be scanned: ${error.message}`,
227
- };
228
- }
229
-
230
- const keyMap = readBuildArtifact({ name: 'keyMap.json' }) ?? {};
231
- const refMap = readBuildArtifact({ name: 'refMap.json' }) ?? {};
232
- const matches = scanKeyMapWithDeIndex({
233
- id,
234
- keyMap,
235
- refMap,
236
- configDirectory,
237
- pageScope: buildPageScope({ pageId, keyMap }),
238
- });
239
- return matches.length > 0
240
- ? { matches }
241
- : {
242
- matches,
243
- note:
244
- `No config found with id "${id}" on page "${pageId}". Retry without ?pageId= to ` +
245
- 'scan all built pages and app-level config (connections, menus, api).',
246
- };
247
- }
248
-
249
- const keyMap = readBuildArtifact({ name: 'keyMap.json' }) ?? {};
250
- const refMap = readBuildArtifact({ name: 'refMap.json' }) ?? {};
251
- const matches = scanKeyMapWithDeIndex({ id, keyMap, refMap, configDirectory });
252
- return {
253
- matches,
254
- note:
255
- 'Scanned without a pageId — content on pages that have not been JIT-built yet is not ' +
256
- 'in keyMap and was not covered. Pass ?pageId= to force-build and scan a specific page.',
257
- };
258
- }
259
-
260
- export default findConfig;