@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,121 +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
- // Turns one or more enriched feedback batches (after enrichFeedback.js has
20
- // attached `annotation.location`) into a single agent-readable text block.
21
- // The overlay copies this text to the developer's clipboard — they paste it
22
- // into their agent session. Kept as a pure string formatter — no
23
- // build-artifact access.
24
- function formatFeedback({ items }) {
25
- if (type.isNone(items) || items.length === 0) {
26
- return 'No pending feedback. The developer has not submitted any annotations yet.';
27
- }
28
- return items.map(formatBatch).join('\n\n---\n\n');
29
- }
30
-
31
- function formatBatch(batch) {
32
- const viewport = batch.viewport ?? {};
33
- const lines = [
34
- `Feedback: ${batch.annotations?.length ?? 0} annotation(s) on page "${batch.pageId}" ` +
35
- `(${batch.url ?? 'unknown url'}) — viewport ${viewport.width}x${viewport.height} ` +
36
- `@${viewport.dpr ?? 1}x, scrollY ${viewport.scrollY ?? 0}`,
37
- '',
38
- ];
39
-
40
- (batch.annotations ?? []).forEach((annotation, index) => {
41
- lines.push(formatAnnotation({ annotation, index }));
42
- });
43
-
44
- if (batch.screenshotPath) {
45
- lines.push('');
46
- lines.push(`Annotated screenshot: ${batch.screenshotPath} (read this image to see the drawings)`);
47
- }
48
-
49
- lines.push('');
50
- lines.push(
51
- `For the page's live state call lowdefy_inspect_state({ pageId: "${batch.pageId}" }).`
52
- );
53
-
54
- return lines.join('\n');
55
- }
56
-
57
- function formatAnnotation({ annotation, index }) {
58
- const lines = [];
59
- const num = index + 1;
60
- const locationText = formatLocation(annotation.location);
61
-
62
- if (annotation.kind === 'element' && annotation.target) {
63
- const header = locationText
64
- ? `${num}. Element "${annotation.target.blockId}" (${locationText})`
65
- : `${num}. Element "${annotation.target.blockId}"`;
66
- lines.push(header);
67
- if (annotation.target.ancestorBlockIds?.length > 0) {
68
- lines.push(` Ancestors: ${annotation.target.ancestorBlockIds.join(' > ')}`);
69
- }
70
- if (annotation.target.tag) {
71
- const text = annotation.target.text ? ` "${annotation.target.text}"` : '';
72
- lines.push(` Tag: ${annotation.target.tag}${text}`);
73
- }
74
- } else {
75
- lines.push(`${num}. Region`);
76
- }
77
-
78
- lines.push(` Comment: ${annotation.comment}`);
79
-
80
- const shapesSummary = summarizeShapes({
81
- shapes: annotation.geometry?.shapes,
82
- hasElementRect: !type.isNone(annotation.geometry?.elementRect),
83
- });
84
- if (shapesSummary) {
85
- lines.push(` Shapes: ${shapesSummary}`);
86
- }
87
-
88
- return lines.join('\n');
89
- }
90
-
91
- function formatLocation(location) {
92
- if (type.isNone(location)) {
93
- return null;
94
- }
95
- if (location.source && location.resolvedVia) {
96
- return `generated at runtime — defined via ancestor "${location.resolvedVia}": ${location.source}`;
97
- }
98
- return location.source ?? location.note ?? null;
99
- }
100
-
101
- // Counts shapes by type (rect/arrow/freehand) preserving first-seen order,
102
- // e.g. "1 rect around the element, 1 arrow" — the first counted type is
103
- // attributed to the targeted element when the annotation carries an
104
- // elementRect, since that is the shape drawn to select it.
105
- function summarizeShapes({ shapes, hasElementRect }) {
106
- if (!shapes || shapes.length === 0) {
107
- return null;
108
- }
109
- const counts = new Map();
110
- shapes.forEach((shape) => {
111
- counts.set(shape.type, (counts.get(shape.type) ?? 0) + 1);
112
- });
113
- return Array.from(counts.entries())
114
- .map(([shapeType, shapeCount], index) => {
115
- const label = `${shapeCount} ${shapeType}${shapeCount > 1 ? 's' : ''}`;
116
- return index === 0 && hasElementRect ? `${label} around the element` : label;
117
- })
118
- .join(', ');
119
- }
120
-
121
- export default formatFeedback;
@@ -1,204 +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
- const mockFindConfig = jest.fn();
20
- jest.unstable_mockModule('./findConfig.js', () => ({
21
- default: mockFindConfig,
22
- }));
23
-
24
- const { default: enrichFeedback } = await import('./enrichFeedback.js');
25
- const { default: formatFeedback } = await import('./formatFeedback.js');
26
-
27
- beforeEach(() => {
28
- mockFindConfig.mockReset();
29
- });
30
-
31
- // --- enrichFeedback ---
32
-
33
- test('enrichFeedback attaches a location to annotations with a target blockId', async () => {
34
- mockFindConfig.mockResolvedValue({
35
- matches: [
36
- {
37
- keyPath: 'root.pages[0:login].blocks[2:submit_button:Button]',
38
- location: {
39
- source: 'pages/login.yaml:12',
40
- config: 'root.pages[0:login].blocks[2:submit_button:Button]',
41
- },
42
- },
43
- ],
44
- });
45
- const batch = {
46
- pageId: 'login',
47
- annotations: [
48
- {
49
- id: '1',
50
- kind: 'element',
51
- comment: 'too small',
52
- target: {
53
- blockId: 'submit_button',
54
- ancestorBlockIds: ['submit_button', 'login_form'],
55
- tag: 'BUTTON',
56
- text: 'Submit',
57
- },
58
- geometry: { elementRect: { x: 220, y: 480, width: 96, height: 40 }, shapes: [] },
59
- },
60
- ],
61
- };
62
-
63
- const enriched = await enrichFeedback({ batch });
64
-
65
- expect(mockFindConfig).toHaveBeenCalledWith({ id: 'submit_button', pageId: 'login' });
66
- expect(enriched.annotations[0].location).toEqual({
67
- source: 'pages/login.yaml:12',
68
- config: 'root.pages[0:login].blocks[2:submit_button:Button]',
69
- });
70
- });
71
-
72
- test('enrichFeedback attaches a note when findConfig finds no matches', async () => {
73
- mockFindConfig.mockResolvedValue({
74
- matches: [],
75
- note: 'Block is generated at runtime — no configured ancestor found on page "login".',
76
- });
77
- const batch = {
78
- pageId: 'login',
79
- annotations: [{ id: '1', kind: 'element', comment: 'x', target: { blockId: 'missing' } }],
80
- };
81
-
82
- const enriched = await enrichFeedback({ batch });
83
-
84
- expect(enriched.annotations[0].location).toEqual({
85
- note: 'Block is generated at runtime — no configured ancestor found on page "login".',
86
- });
87
- });
88
-
89
- test('enrichFeedback leaves annotations without a target blockId unchanged', async () => {
90
- const batch = {
91
- pageId: 'login',
92
- annotations: [{ id: '1', kind: 'region', comment: 'x', target: null }],
93
- };
94
-
95
- const enriched = await enrichFeedback({ batch });
96
-
97
- expect(mockFindConfig).not.toHaveBeenCalled();
98
- expect(enriched.annotations[0]).toEqual({ id: '1', kind: 'region', comment: 'x', target: null });
99
- });
100
-
101
- test('enrichFeedback still returns the batch when findConfig throws', async () => {
102
- mockFindConfig.mockRejectedValue(new Error('boom'));
103
- const batch = {
104
- pageId: 'login',
105
- annotations: [{ id: '1', kind: 'element', comment: 'x', target: { blockId: 'submit_button' } }],
106
- };
107
-
108
- const enriched = await enrichFeedback({ batch });
109
-
110
- expect(enriched.annotations).toHaveLength(1);
111
- expect(enriched.annotations[0].location.note).toMatch(/Failed to resolve location: boom/);
112
- });
113
-
114
- // --- formatFeedback ---
115
-
116
- test('formatFeedback returns a message when there is no pending feedback', () => {
117
- expect(formatFeedback({ items: [] })).toMatch(/No pending feedback/);
118
- });
119
-
120
- test('formatFeedback formats an element annotation with location, ancestors, comment, shapes, and screenshot', () => {
121
- const items = [
122
- {
123
- pageId: 'login',
124
- url: '/login?x=1',
125
- viewport: { width: 1280, height: 720, scrollX: 0, scrollY: 340, dpr: 2 },
126
- annotations: [
127
- {
128
- id: '1',
129
- kind: 'element',
130
- comment: 'Button is too small on mobile',
131
- target: {
132
- blockId: 'submit_button',
133
- ancestorBlockIds: ['submit_button', 'login_form'],
134
- tag: 'BUTTON',
135
- text: 'Submit',
136
- },
137
- geometry: {
138
- elementRect: { x: 220, y: 480, width: 96, height: 40 },
139
- shapes: [
140
- { type: 'rect', points: [] },
141
- { type: 'arrow', points: [] },
142
- ],
143
- },
144
- location: {
145
- source: 'pages/login.yaml:12',
146
- config: 'root.pages[0:login].blocks[2:submit_button:Button]',
147
- },
148
- },
149
- ],
150
- screenshotPath: '.lowdefy/annotations/login-test.png',
151
- },
152
- ];
153
-
154
- const text = formatFeedback({ items });
155
-
156
- expect(text).toContain('1 annotation(s) on page "login"');
157
- expect(text).toContain('viewport 1280x720 @2x, scrollY 340');
158
- expect(text).toContain('Element "submit_button" (pages/login.yaml:12)');
159
- expect(text).toContain('Ancestors: submit_button > login_form');
160
- expect(text).toContain('Comment: Button is too small on mobile');
161
- expect(text).toContain('1 rect around the element, 1 arrow');
162
- expect(text).toContain('Annotated screenshot: .lowdefy/annotations/login-test.png');
163
- expect(text).toContain('lowdefy_inspect_state({ pageId: "login" })');
164
- expect(text).not.toContain('Console');
165
- });
166
-
167
- test('formatFeedback formats a region annotation without a target', () => {
168
- const items = [
169
- {
170
- pageId: 'home',
171
- url: '/home',
172
- viewport: { width: 1280, height: 720 },
173
- annotations: [
174
- {
175
- id: '1',
176
- kind: 'region',
177
- comment: 'Layout looks off here',
178
- target: null,
179
- geometry: { elementRect: null, shapes: [{ type: 'freehand', points: [] }] },
180
- },
181
- ],
182
- },
183
- ];
184
-
185
- const text = formatFeedback({ items });
186
-
187
- expect(text).toContain('1. Region');
188
- expect(text).toContain('Comment: Layout looks off here');
189
- expect(text).toContain('1 freehand');
190
- });
191
-
192
- test('formatFeedback multiple batches are joined and each keeps its own pageId in instructions', () => {
193
- const items = [
194
- { pageId: 'login', url: '/login', viewport: { width: 100, height: 100 }, annotations: [] },
195
- { pageId: 'home', url: '/home', viewport: { width: 100, height: 100 }, annotations: [] },
196
- ];
197
-
198
- const text = formatFeedback({ items });
199
-
200
- expect(text).toContain('page "login"');
201
- expect(text).toContain('page "home"');
202
- expect(text).toContain('lowdefy_inspect_state({ pageId: "login" })');
203
- expect(text).toContain('lowdefy_inspect_state({ pageId: "home" })');
204
- });
@@ -1,180 +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 'node:fs';
18
- import path from 'node:path';
19
- import { type } from '@lowdefy/helpers';
20
-
21
- import readBuildArtifact from './readBuildArtifact.js';
22
-
23
- // Recursively counts blocks on a page, including blocks nested in slots
24
- // (packages/build/src/build/buildPages/buildBlock/moveAreasToSlots.js moves
25
- // "areas" to "slots" during build, so built page artifacts only ever have
26
- // "slots").
27
- function countBlocks(blocks, types) {
28
- let count = 0;
29
- (blocks ?? []).forEach((block) => {
30
- count += 1;
31
- if (block.type) {
32
- types.add(block.type);
33
- }
34
- Object.values(block.slots ?? {}).forEach((slot) => {
35
- count += countBlocks(slot?.blocks, types);
36
- });
37
- });
38
- return count;
39
- }
40
-
41
- function summarizeBlocks(blocks) {
42
- const types = new Set();
43
- const blockCount = countBlocks(blocks, types);
44
- return { blockCount, blockTypes: [...types] };
45
- }
46
-
47
- // The request's "type" is stripped from the page artifact by the build
48
- // (packages/build/src/build/full/writeRequests.js deletes
49
- // type/connectionId/properties/auth after writing the per-request file), so
50
- // it is read from the dedicated per-request artifact instead.
51
- function summarizeRequests({ pageId, requests }) {
52
- return (requests ?? []).map((request) => {
53
- const requestConfig = readBuildArtifact({
54
- name: `pages/${pageId}/requests/${request.requestId}.json`,
55
- deserialize: true,
56
- });
57
- return { id: request.requestId, type: requestConfig?.type ?? null };
58
- });
59
- }
60
-
61
- function getPages() {
62
- const registry = readBuildArtifact({ name: 'pageRegistry.json' }) ?? {};
63
- const pages = [];
64
- let unbuiltCount = 0;
65
-
66
- Object.entries(registry).forEach(([pageId, entry]) => {
67
- const built = readBuildArtifact({ name: `pages/${pageId}.json`, deserialize: true });
68
- const page = {
69
- pageId,
70
- file: entry.refPath,
71
- auth: entry.auth,
72
- built: !type.isNone(built),
73
- };
74
- if (type.isNone(built)) {
75
- unbuiltCount += 1;
76
- } else {
77
- Object.assign(page, summarizeBlocks(built.blocks));
78
- page.requests = summarizeRequests({ pageId, requests: built.requests });
79
- }
80
- pages.push(page);
81
- });
82
-
83
- return { pages, unbuiltCount };
84
- }
85
-
86
- // Menu items can nest further links (e.g. MenuGroup), so this recurses; kept
87
- // to id/type/pageId/title only — no properties/auth noise.
88
- function summarizeMenuItem(menuItem) {
89
- const summary = {
90
- menuItemId: menuItem.menuItemId ?? menuItem.id,
91
- type: menuItem.type,
92
- };
93
- if (!type.isNone(menuItem.pageId)) {
94
- summary.pageId = menuItem.pageId;
95
- }
96
- if (!type.isNone(menuItem.properties?.title)) {
97
- summary.title = menuItem.properties.title;
98
- }
99
- if (type.isArray(menuItem.links)) {
100
- summary.links = menuItem.links.map(summarizeMenuItem);
101
- }
102
- return summary;
103
- }
104
-
105
- function getMenus() {
106
- const menus = readBuildArtifact({ name: 'menus.json', deserialize: true }) ?? [];
107
- return menus.map((menu) => ({
108
- menuId: menu.menuId ?? menu.id,
109
- links: (menu.links ?? []).map(summarizeMenuItem),
110
- }));
111
- }
112
-
113
- function getConnections() {
114
- const ids = readBuildArtifact({ name: 'connectionIds.json' }) ?? [];
115
- return ids.map((id) => {
116
- const connection = readBuildArtifact({ name: `connections/${id}.json`, deserialize: true });
117
- return { id, type: connection?.type ?? null };
118
- });
119
- }
120
-
121
- // api/ and agents/ have one artifact file per id and no id manifest (unlike
122
- // connectionIds.json / websocketIds.json), so their ids come from a
123
- // directory listing instead of readBuildArtifact.
124
- function listArtifactIds({ buildDirectory, dirName }) {
125
- const dirPath = path.join(buildDirectory, dirName);
126
- if (!fs.existsSync(dirPath)) {
127
- return [];
128
- }
129
- return fs
130
- .readdirSync(dirPath)
131
- .filter((filename) => filename.endsWith('.json'))
132
- .map((filename) => filename.slice(0, -'.json'.length));
133
- }
134
-
135
- function getEndpoints({ buildDirectory }) {
136
- return listArtifactIds({ buildDirectory, dirName: 'api' }).map((id) => {
137
- const endpoint = readBuildArtifact({ name: `api/${id}.json`, deserialize: true });
138
- return { id, type: endpoint?.type ?? null };
139
- });
140
- }
141
-
142
- function getAgents({ buildDirectory }) {
143
- return listArtifactIds({ buildDirectory, dirName: 'agents' }).map((id) => {
144
- const agent = readBuildArtifact({ name: `agents/${id}.json`, deserialize: true });
145
- return { id, type: agent?.type ?? null };
146
- });
147
- }
148
-
149
- function getWebsockets() {
150
- return readBuildArtifact({ name: 'websocketIds.json' }) ?? [];
151
- }
152
-
153
- // Compact snapshot of the whole app for an agent to orient itself: every
154
- // page (with block/request detail only for pages that have a built
155
- // artifact — pages are JIT-built on first visit, see lib/docs/getPageConfig.js),
156
- // menus, connections, api endpoints, agents and websockets.
157
- function getAppMap() {
158
- const buildDirectory = path.join(process.cwd(), 'build');
159
- const { pages, unbuiltCount } = getPages();
160
-
161
- const map = {
162
- pages,
163
- menus: getMenus(),
164
- connections: getConnections(),
165
- endpoints: getEndpoints({ buildDirectory }),
166
- agents: getAgents({ buildDirectory }),
167
- websockets: getWebsockets(),
168
- };
169
-
170
- if (unbuiltCount > 0) {
171
- map.note =
172
- `${unbuiltCount} page(s) have not been built yet, so only "file" and "auth" are shown ` +
173
- 'for them. Visit the page in the browser, or GET /lowdefy-docs/page/{pageId}, to trigger ' +
174
- 'a build and see their blocks and requests.';
175
- }
176
-
177
- return map;
178
- }
179
-
180
- export default getAppMap;
@@ -1,115 +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 { chromium } from 'playwright-core';
18
- import { type } from '@lowdefy/helpers';
19
-
20
- import lowdefyConfig from '../build/config.js';
21
- import { HEADLESS_USER_COOKIE, headlessUser } from '../server/auth/headlessUser.js';
22
-
23
- // playwright-core does not bundle a browser (unlike @playwright/test) — it
24
- // only drives one that is already installed. `channel: 'chrome'` picks up a
25
- // system Chrome install first since that's more likely to already be
26
- // present than a Playwright-managed Chromium in a dev environment.
27
- async function launchBrowser() {
28
- try {
29
- return await chromium.launch({ channel: 'chrome' });
30
- } catch {
31
- return await chromium.launch();
32
- }
33
- }
34
-
35
- // Module-level singleton — a browser process is expensive to start, so it is
36
- // launched once and reused across every headless caller (screenshots, state
37
- // inspection, operator evaluation). Cached as a promise so concurrent calls
38
- // awaiting startup share the same launch instead of racing.
39
- let browserPromise = null;
40
-
41
- async function getBrowser() {
42
- if (type.isNone(browserPromise)) {
43
- // Clear the cache on failure so the next call retries the launch
44
- // instead of replaying a cached rejection forever.
45
- browserPromise = launchBrowser().catch((error) => {
46
- browserPromise = null;
47
- throw error;
48
- });
49
- }
50
- let browser = await browserPromise;
51
- if (!browser.isConnected()) {
52
- browserPromise = launchBrowser().catch((error) => {
53
- browserPromise = null;
54
- throw error;
55
- });
56
- browser = await browserPromise;
57
- }
58
- return browser;
59
- }
60
-
61
- // `origin` should already include any configured basePath prefix that the
62
- // caller can't derive itself (e.g. from a request URL) — here it's read from
63
- // build/config.json (the same source app.js uses to mount the app) so
64
- // callers only need to pass the bare origin.
65
- function buildPageUrl({ origin, pageId }) {
66
- const basePath = lowdefyConfig.basePath ?? '';
67
- return `${origin}${basePath}/${pageId}`;
68
- }
69
-
70
- // Opens a fresh browser context + page at the app's pageId route. Callers
71
- // obtain `browser` via getBrowser() themselves so they can map a launch
72
- // failure to their own "no browser available" error message, separate from
73
- // navigation failures.
74
- async function openPage({ browser, origin, pageId, width = 1280, height = 800, timeout = 15000 }) {
75
- const url = buildPageUrl({ origin, pageId });
76
- const context = await browser.newContext({ viewport: { width, height } });
77
- // Inject an authenticated user so auth-protected pages don't 404 for the
78
- // cookieless headless context. Mirrors the e2e user-cookie pattern; scoped to
79
- // `origin` so it rides along on the same-origin /api/* fetches.
80
- await context.addCookies([
81
- {
82
- name: HEADLESS_USER_COOKIE,
83
- value: Buffer.from(JSON.stringify(headlessUser)).toString('base64'),
84
- url: origin,
85
- },
86
- ]);
87
- const page = await context.newPage();
88
- try {
89
- await page.goto(url, { waitUntil: 'networkidle', timeout });
90
- } catch {
91
- // Pages with long-polling/SSE connections (reload, websockets) never
92
- // go network-idle — fall back to 'load' rather than failing outright.
93
- await page.goto(url, { waitUntil: 'load', timeout });
94
- }
95
- // The engine builds the page context (and runs onInit + initial requests)
96
- // after the bundle loads — 'load'/'networkidle' fire before that. Every
97
- // caller (screenshot, inspect, eval, checkpoint load) needs the app
98
- // actually mounted, so wait for the context and for initial requests to
99
- // settle. Tolerant: on timeout proceed and let the caller surface what it
100
- // finds — a screenshot of a hung page is still useful signal.
101
- await page
102
- .waitForFunction(
103
- (id) => {
104
- const pageContext = window.lowdefy?.contexts?.[`page:${id}`];
105
- if (!pageContext) return false;
106
- return !Object.values(pageContext.requests ?? {}).some((calls) => calls?.[0]?.loading);
107
- },
108
- pageId,
109
- { timeout }
110
- )
111
- .catch(() => {});
112
- return { context, page, url };
113
- }
114
-
115
- export { getBrowser, openPage, buildPageUrl };
@@ -1,37 +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 clientErrorStore from './clientErrorStore.js';
18
- import readBuildArtifact from './readBuildArtifact.js';
19
-
20
- // Feedback loop for agents: build status (written by the build manager to
21
- // build/buildStatus.json) plus recent browser errors reported via
22
- // POST /api/client-error. Lets an agent check "did my last edit work?"
23
- // without tailing terminal logs.
24
- function getBuildStatus() {
25
- const build = readBuildArtifact({ name: 'buildStatus.json' }) ?? {
26
- status: 'unknown',
27
- message:
28
- 'No build status yet — the build manager has not written build/buildStatus.json. ' +
29
- 'This is expected before the first build completes.',
30
- };
31
- return {
32
- build,
33
- clientErrors: clientErrorStore.list(),
34
- };
35
- }
36
-
37
- export default getBuildStatus;