@lowdefy/server-dev 5.5.1 → 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/{apiWrapper.js → createLowdefyContext.js} +63 -57
  107. package/lib/server/getPageJitEnrichment.test.mjs +105 -0
  108. package/lib/server/jitPageBuilder.js +125 -12
  109. package/lib/server/log/createHandleError.js +9 -0
  110. package/lib/server/log/createHandleError.test.mjs +126 -0
  111. package/manager/getContext.mjs +3 -7
  112. package/manager/processes/initialBuild.mjs +0 -2
  113. package/manager/processes/installPlugins.mjs +19 -11
  114. package/manager/processes/lowdefyBuild.mjs +29 -7
  115. package/manager/processes/lowdefyBuild.test.mjs +102 -0
  116. package/manager/processes/readDotEnv.mjs +13 -1
  117. package/manager/processes/shutdownServer.mjs +6 -6
  118. package/manager/processes/startProxy.mjs +160 -0
  119. package/manager/processes/startProxy.test.mjs +102 -0
  120. package/manager/processes/startServer.mjs +33 -16
  121. package/manager/processes/startWatchers.mjs +2 -2
  122. package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
  123. package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
  124. package/manager/run.mjs +50 -0
  125. package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
  126. package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
  127. package/manager/utils/formatNoticeBox.mjs +30 -0
  128. package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
  129. package/manager/utils/updatePageTailwindCss.mjs +7 -1
  130. package/manager/utils/writeBuildStatus.mjs +39 -0
  131. package/manager/utils/writeBuildStatus.test.mjs +76 -0
  132. package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
  133. package/manager/watchers/moduleBuildWatcher.mjs +0 -1
  134. package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
  135. package/package.json +61 -47
  136. package/package.original.json +64 -55
  137. package/postcss.config.cjs +1 -0
  138. package/src/app.js +175 -0
  139. package/src/html/renderDevPage.js +86 -0
  140. package/src/lib/getPathSegments.js +25 -0
  141. package/src/lib/safeScriptJson.js +38 -0
  142. package/src/middleware/apiContext.js +32 -0
  143. package/src/middleware/errorHandler.js +48 -0
  144. package/src/middleware/errorHandler.test.mjs +235 -0
  145. package/src/routes/agent.js +64 -0
  146. package/src/routes/apiPage.js +37 -0
  147. package/src/routes/auth.js +49 -0
  148. package/src/routes/auth.test.mjs +84 -0
  149. package/src/routes/clientError.js +57 -0
  150. package/src/routes/cron.js +47 -0
  151. package/src/routes/cronForward.js +47 -0
  152. package/src/routes/detached.js +47 -0
  153. package/src/routes/devInspect.js +111 -0
  154. package/src/routes/devInspect.test.mjs +119 -0
  155. package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
  156. package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
  157. package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
  158. package/src/routes/docs/checkpointsCreate.js +28 -0
  159. package/src/routes/docs/checkpointsList.js +23 -0
  160. package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
  161. package/src/routes/docs/content.js +31 -0
  162. package/src/routes/docs/evalOperator.js +56 -0
  163. package/src/routes/docs/examples.js +33 -0
  164. package/src/routes/docs/find.js +30 -0
  165. package/src/routes/docs/index.js +23 -0
  166. package/src/routes/docs/inspectState.js +44 -0
  167. package/src/routes/docs/loadState.js +49 -0
  168. package/src/routes/docs/mcp.js +30 -0
  169. package/src/routes/docs/pageConfig.js +36 -0
  170. package/src/routes/docs/parseUserParam.js +52 -0
  171. package/src/routes/docs/parseUserParam.test.mjs +45 -0
  172. package/src/routes/docs/pluginDoc.js +33 -0
  173. package/src/routes/docs/plugins.js +23 -0
  174. package/src/routes/docs/runRequest.js +36 -0
  175. package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
  176. package/src/routes/docs/screenshot.js +72 -0
  177. package/src/routes/docs/search.js +27 -0
  178. package/src/routes/docs/snapshotState.js +36 -0
  179. package/src/routes/docs/stateCheckpointsList.js +26 -0
  180. package/src/routes/docs/types.js +33 -0
  181. package/src/routes/endpoints.js +71 -0
  182. package/src/routes/feedback.js +110 -0
  183. package/src/routes/feedback.test.mjs +204 -0
  184. package/src/routes/jitPage.js +86 -0
  185. package/src/routes/mcp.js +34 -0
  186. package/{pages/api → src/routes}/ping.js +4 -2
  187. package/src/routes/reload.js +79 -0
  188. package/src/routes/request.js +74 -0
  189. package/src/routes/request.test.mjs +124 -0
  190. package/{pages/api → src/routes}/root.js +4 -4
  191. package/src/routes/usage.js +46 -0
  192. package/src/routes/websocket.js +33 -0
  193. package/src/websocket/devWebSocket.js +78 -0
  194. package/vite.config.js +111 -0
  195. package/.eslintrc.yaml +0 -1
  196. package/lib/client/App.js +0 -80
  197. package/lib/client/Reload.js +0 -60
  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 -107
  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,59 @@
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
+ // html-to-image embeds img.src and clones srcset/<source> attributes as-is:
18
+ // the selection the browser actually displays (img.currentSrc) is ignored, so
19
+ // a <picture> rasterizes its fallback src stretched into the displayed box
20
+ // (e.g. the PageHeaderMenu square logo warped into the wordmark's slot), and
21
+ // remote srcset candidates can't load inside the sandboxed SVG rasterization
22
+ // anyway. Pin every responsive image to its currentSrc for the capture — the
23
+ // URL the tab already displays, so nothing changes visually — and return a
24
+ // restore function for the caller's finally block.
25
+ function pinResponsiveImages() {
26
+ const restores = [];
27
+
28
+ document.querySelectorAll('img').forEach((img) => {
29
+ const isResponsive =
30
+ img.hasAttribute('srcset') || (img.currentSrc && img.currentSrc !== img.src);
31
+ if (!img.currentSrc || !isResponsive) return;
32
+ const src = img.getAttribute('src');
33
+ const srcset = img.getAttribute('srcset');
34
+ restores.push(() => {
35
+ if (src === null) {
36
+ img.removeAttribute('src');
37
+ } else {
38
+ img.setAttribute('src', src);
39
+ }
40
+ if (srcset !== null) {
41
+ img.setAttribute('srcset', srcset);
42
+ }
43
+ });
44
+ img.setAttribute('src', img.currentSrc);
45
+ img.removeAttribute('srcset');
46
+ });
47
+
48
+ document.querySelectorAll('picture > source[srcset]').forEach((source) => {
49
+ const srcset = source.getAttribute('srcset');
50
+ restores.push(() => source.setAttribute('srcset', srcset));
51
+ source.removeAttribute('srcset');
52
+ });
53
+
54
+ return function restore() {
55
+ restores.forEach((fn) => fn());
56
+ };
57
+ }
58
+
59
+ export default pinResponsiveImages;
@@ -0,0 +1,39 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // Sends the batch to the dev server for enrichment (blockId → yaml file:line
18
+ // via the build's keyMap) and gets back the formatted, agent-readable text.
19
+ // Best-effort, matching Inspector.jsx's postResult style — a dev-only feature
20
+ // must never throw into the app it rides along with. Returns the formatted
21
+ // string, or null on any failure.
22
+ async function sendFeedback({ basePath, batch }) {
23
+ try {
24
+ const response = await fetch(`${basePath}/lowdefy-feedback`, {
25
+ method: 'POST',
26
+ headers: { 'Content-Type': 'application/json' },
27
+ body: JSON.stringify(batch),
28
+ });
29
+ if (!response.ok) {
30
+ return null;
31
+ }
32
+ const data = await response.json();
33
+ return typeof data.formatted === 'string' ? data.formatted : null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export default sendFeedback;
@@ -0,0 +1,70 @@
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 { useEffect, useRef } from 'react';
18
+
19
+ import { tinykeys } from 'tinykeys';
20
+
21
+ // Binds the global Cmd/Ctrl+/ feedback toggle, matching the app's own
22
+ // shortcut convention (see createShortcutManager.js): tinykeys on window,
23
+ // "$mod" for the platform modifier. Registered at capture phase with
24
+ // stopImmediatePropagation so the app's own shortcut manager (or any block
25
+ // event bound to the same key) never also fires.
26
+ function useFeedbackToggle({ onToggle }) {
27
+ const onToggleRef = useRef(onToggle);
28
+ onToggleRef.current = onToggle;
29
+
30
+ useEffect(() => {
31
+ if (typeof window === 'undefined') {
32
+ return undefined;
33
+ }
34
+
35
+ let unsubscribe;
36
+ try {
37
+ unsubscribe = tinykeys(
38
+ window,
39
+ {
40
+ '$mod+Slash': (event) => {
41
+ try {
42
+ event.preventDefault();
43
+ event.stopImmediatePropagation();
44
+ if (onToggleRef.current) {
45
+ onToggleRef.current();
46
+ }
47
+ } catch {
48
+ // Never let the toggle handler crash the app.
49
+ }
50
+ },
51
+ },
52
+ { capture: true }
53
+ );
54
+ } catch {
55
+ // tinykeys unavailable or binding failed — the feature just won't toggle.
56
+ }
57
+
58
+ return () => {
59
+ try {
60
+ if (unsubscribe) {
61
+ unsubscribe();
62
+ }
63
+ } catch {
64
+ // no-op
65
+ }
66
+ };
67
+ }, []);
68
+ }
69
+
70
+ export default useFeedbackToggle;
@@ -0,0 +1,42 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ // CSS layer order — MUST be the first CSS import. This locks the cascade
18
+ // priority (antd > base/preflight) before antd's StyleProvider injects
19
+ // @layer antd {} at runtime. Vite's PostCSS pipeline compiles globals.css
20
+ // (Tailwind) on the fly and hot-replaces it — the old tailwind-jit.css
21
+ // side-channel is gone.
22
+ import '../build/layer-order.css';
23
+
24
+ import React from 'react';
25
+ import { createRoot } from 'react-dom/client';
26
+ import createRouter from '@lowdefy/client/adapters/createRouter.js';
27
+
28
+ import App from './App.jsx';
29
+
30
+ import '../build/globals.css';
31
+
32
+ const config = JSON.parse(document.getElementById('__LOWDEFY_CONFIG__').textContent);
33
+ const router = createRouter({ basePath: config.basePath ?? '', window });
34
+ const container = document.getElementById('root');
35
+
36
+ // Keep one React root across Vite HMR updates.
37
+ const root = import.meta.hot?.data.root ?? createRoot(container);
38
+ if (import.meta.hot) {
39
+ import.meta.hot.data.root = root;
40
+ }
41
+
42
+ root.render(<App config={config} router={router} />);
@@ -0,0 +1,268 @@
1
+ /*
2
+ Copyright 2020-2026 Lowdefy, Inc
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+ */
16
+
17
+ import React, { useEffect, useRef, useState } from 'react';
18
+ import { createPortal } from 'react-dom';
19
+
20
+ import { blockAncestorChain, nearestBlock } from '../feedback/elementInspect.js';
21
+ import { highlightBox, labelChip, overlayContainer } from '../feedback/feedbackStyles.js';
22
+ import findBlockLocation from './findBlockLocation.js';
23
+
24
+ const STYLE_TAG_ID = 'lowdefy-open-in-editor-style-tag';
25
+ const BODY_CLASS = 'lowdefy-open-in-editor-target';
26
+
27
+ // Cursor over arbitrary app descendants can't be forced from inline styles —
28
+ // same trick as feedbackStyles' picking/drawing classes.
29
+ function injectStyleTag() {
30
+ if (typeof document === 'undefined' || document.getElementById(STYLE_TAG_ID)) {
31
+ return;
32
+ }
33
+ const style = document.createElement('style');
34
+ style.id = STYLE_TAG_ID;
35
+ style.textContent = `
36
+ .${BODY_CLASS}, .${BODY_CLASS} * {
37
+ cursor: pointer !important;
38
+ }
39
+ `;
40
+ document.head.appendChild(style);
41
+ }
42
+
43
+ function removeStyleTag() {
44
+ if (typeof document === 'undefined') {
45
+ return;
46
+ }
47
+ document.getElementById(STYLE_TAG_ID)?.remove();
48
+ }
49
+
50
+ // The feedback overlay owns clicks while it is open (its picking mode claims
51
+ // them) — the Option/Alt+click shortcut stays out of the way until it closes.
52
+ // :not() skips this component's own highlight portal, which carries
53
+ // data-lowdefy-feedback purely for screenshot/picking exclusion.
54
+ function feedbackOverlayOpen() {
55
+ return Boolean(
56
+ document.querySelector('[data-lowdefy-feedback]:not([data-lowdefy-open-in-editor])')
57
+ );
58
+ }
59
+
60
+ // Option (macOS) / Alt (Windows, Linux) alone is the shortcut. Cmd/Ctrl are left
61
+ // to the browser (open link in new tab) and Shift to text selection, so any of
62
+ // them held alongside means the developer wants the browser's behaviour.
63
+ function isOpenModifier(event) {
64
+ return event.altKey && !event.metaKey && !event.ctrlKey && !event.shiftKey;
65
+ }
66
+
67
+ // Dev-only: Option/Alt+click any element to open the YAML that defines its
68
+ // block in VS Code (vscode://file deep link, at the exact line). While the
69
+ // modifier is held, the hovered block shows the same blue highlight box and
70
+ // blockId chip as the annotation overlay's picking mode, and the cursor
71
+ // switches to a pointer — so the developer sees exactly what a click will
72
+ // open. Mounted always-on from Routing.jsx alongside Inspector and
73
+ // FeedbackMount, and follows their resilience contract — every handler is
74
+ // try/caught so this can never take the app down with it.
75
+ //
76
+ // The click resolves the nearest block and, when that id is generated at
77
+ // runtime (list items, Dynamic endpoints), falls back through the ancestor
78
+ // block chain until an id resolves in config — the same strategy the
79
+ // feedback pipeline uses in enrichFeedback.js.
80
+ function OpenInEditorListener({ basePath, pageId }) {
81
+ const [hoverBlock, setHoverBlock] = useState(null); // { blockId, rect }
82
+ // Refs keep the freshest values without re-binding the capture listeners
83
+ // on every SPA navigation or hover change.
84
+ const pageIdRef = useRef(pageId);
85
+ pageIdRef.current = pageId;
86
+ const hoverRef = useRef(hoverBlock);
87
+ hoverRef.current = hoverBlock;
88
+ const lastPointerRef = useRef(null); // { x, y }
89
+
90
+ // Style tag lifecycle — the listener is always mounted, so this lives for
91
+ // the app's lifetime and only the body class toggles per hover.
92
+ useEffect(() => {
93
+ injectStyleTag();
94
+ return () => removeStyleTag();
95
+ }, []);
96
+
97
+ // Pointer cursor only while a block is highlighted — over non-block areas
98
+ // the cursor stays native, signalling the click would do nothing.
99
+ useEffect(() => {
100
+ try {
101
+ document.body.classList.toggle(BODY_CLASS, Boolean(hoverBlock));
102
+ } catch {
103
+ // no-op
104
+ }
105
+ return () => {
106
+ try {
107
+ document.body.classList.remove(BODY_CLASS);
108
+ } catch {
109
+ // no-op
110
+ }
111
+ };
112
+ }, [hoverBlock]);
113
+
114
+ useEffect(() => {
115
+ function clearHover() {
116
+ if (hoverRef.current) {
117
+ setHoverBlock(null);
118
+ }
119
+ }
120
+
121
+ function resolveHover() {
122
+ try {
123
+ const point = lastPointerRef.current;
124
+ if (!point || feedbackOverlayOpen()) {
125
+ clearHover();
126
+ return;
127
+ }
128
+ const el = document.elementFromPoint(point.x, point.y);
129
+ const blockId = nearestBlock(el);
130
+ const blockEl = blockId ? document.getElementById(`bl-${blockId}`) : null;
131
+ if (!blockEl) {
132
+ clearHover();
133
+ return;
134
+ }
135
+ const rect = blockEl.getBoundingClientRect();
136
+ setHoverBlock({
137
+ blockId,
138
+ rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
139
+ });
140
+ } catch {
141
+ clearHover();
142
+ }
143
+ }
144
+
145
+ function onPointerMove(event) {
146
+ try {
147
+ lastPointerRef.current = { x: event.clientX, y: event.clientY };
148
+ if (isOpenModifier(event)) {
149
+ resolveHover();
150
+ } else {
151
+ clearHover();
152
+ }
153
+ } catch {
154
+ // Best-effort hover tracking.
155
+ }
156
+ }
157
+
158
+ // Modifier pressed/released while the mouse is stationary — pointermove
159
+ // alone would leave the highlight stale.
160
+ function onKeyDown(event) {
161
+ if (event.key === 'Alt') {
162
+ resolveHover();
163
+ }
164
+ }
165
+
166
+ function onKeyUp(event) {
167
+ if (event.key === 'Alt') {
168
+ clearHover();
169
+ }
170
+ }
171
+
172
+ // Switching apps releases the modifier without a keyup reaching the page.
173
+ function onBlur() {
174
+ clearHover();
175
+ }
176
+
177
+ // Keep the highlight glued to the block while the app scrolls under a
178
+ // stationary cursor.
179
+ function onScroll() {
180
+ if (hoverRef.current) {
181
+ resolveHover();
182
+ }
183
+ }
184
+
185
+ async function openBlockInEditor({ candidates }) {
186
+ for (const blockId of candidates) {
187
+ const location = await findBlockLocation({
188
+ basePath,
189
+ blockId,
190
+ pageId: pageIdRef.current,
191
+ });
192
+ if (location) {
193
+ window.location.assign(`vscode://file${location.file}:${location.line}`);
194
+ return;
195
+ }
196
+ }
197
+ try {
198
+ // eslint-disable-next-line no-console
199
+ console.info(
200
+ `Lowdefy: no config location found for "${candidates[0]}" — the block is generated at runtime.`
201
+ );
202
+ } catch {
203
+ // no-op
204
+ }
205
+ }
206
+
207
+ function onClickCapture(event) {
208
+ try {
209
+ if (!isOpenModifier(event)) {
210
+ return;
211
+ }
212
+ if (feedbackOverlayOpen()) {
213
+ return;
214
+ }
215
+ const candidates = blockAncestorChain(event.target);
216
+ if (candidates.length === 0) {
217
+ return;
218
+ }
219
+ // Only claim the click once a block is under the cursor — plain
220
+ // Option/Alt+clicks elsewhere keep their default behaviour.
221
+ event.preventDefault();
222
+ event.stopPropagation();
223
+ void openBlockInEditor({ candidates });
224
+ } catch {
225
+ // Never let the shortcut break the developer's app.
226
+ }
227
+ }
228
+
229
+ window.addEventListener('pointermove', onPointerMove);
230
+ window.addEventListener('keydown', onKeyDown);
231
+ window.addEventListener('keyup', onKeyUp);
232
+ window.addEventListener('blur', onBlur);
233
+ window.addEventListener('scroll', onScroll, true);
234
+ window.addEventListener('click', onClickCapture, { capture: true });
235
+ return () => {
236
+ window.removeEventListener('pointermove', onPointerMove);
237
+ window.removeEventListener('keydown', onKeyDown);
238
+ window.removeEventListener('keyup', onKeyUp);
239
+ window.removeEventListener('blur', onBlur);
240
+ window.removeEventListener('scroll', onScroll, true);
241
+ window.removeEventListener('click', onClickCapture, { capture: true });
242
+ };
243
+ }, [basePath]);
244
+
245
+ if (!hoverBlock || typeof document === 'undefined' || !document.body) {
246
+ return null;
247
+ }
248
+ // data-lowdefy-feedback keeps this out of the annotation overlay's element
249
+ // picking and out of tab-captured screenshots (both exclude that root).
250
+ return createPortal(
251
+ <div data-lowdefy-feedback data-lowdefy-open-in-editor style={overlayContainer}>
252
+ <div
253
+ style={{
254
+ ...highlightBox,
255
+ top: hoverBlock.rect.top,
256
+ left: hoverBlock.rect.left,
257
+ width: hoverBlock.rect.width,
258
+ height: hoverBlock.rect.height,
259
+ }}
260
+ >
261
+ <span style={labelChip}>{hoverBlock.blockId}</span>
262
+ </div>
263
+ </div>,
264
+ document.body
265
+ );
266
+ }
267
+
268
+ export default OpenInEditorListener;
@@ -0,0 +1,51 @@
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
+ // Resolves a blockId to its YAML source via the dev server's existing
18
+ // GET /lowdefy-docs/find/{id}?pageId= route (which JIT-builds the page's
19
+ // keyMap on demand). Returns { file, line } with an absolute file path, or
20
+ // null when the id has no configured location. Best-effort, matching
21
+ // sendFeedback.js — a dev-only feature must never throw into the app.
22
+ async function findBlockLocation({ basePath, blockId, pageId }) {
23
+ try {
24
+ // Omit pageId when unknown — an empty ?pageId= reads as an unknown page
25
+ // on the server, while no param falls back to scanning all built pages.
26
+ const query = pageId ? `?pageId=${encodeURIComponent(pageId)}` : '';
27
+ const response = await fetch(
28
+ `${basePath}/lowdefy-docs/find/${encodeURIComponent(blockId)}${query}`
29
+ );
30
+ if (!response.ok) {
31
+ return null;
32
+ }
33
+ const data = await response.json();
34
+ const source = data.matches?.[0]?.location?.source;
35
+ if (typeof source !== 'string' || source.length === 0) {
36
+ return null;
37
+ }
38
+ // source is "<absolute path>" or "<absolute path>:<line>" — a numeric
39
+ // suffix after the last colon is the 1-based line number. Splitting on
40
+ // the last colon keeps Windows drive-letter colons intact.
41
+ const match = source.match(/^(.*):(\d+)$/);
42
+ if (match) {
43
+ return { file: match[1], line: Number(match[2]) };
44
+ }
45
+ return { file: source, line: 1 };
46
+ } catch {
47
+ return null;
48
+ }
49
+ }
50
+
51
+ export default findBlockLocation;
package/lib/build/app.js CHANGED
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/app.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/app.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/appMeta.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/appMeta.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
package/lib/build/auth.js CHANGED
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/auth.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/auth.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/config.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/config.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
package/lib/build/i18n.js CHANGED
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/i18n.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/i18n.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/logger.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/logger.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,7 +13,14 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
17
+ // Build artifacts are read from disk because the Hono server runs as
18
+ // unbundled Node.js ESM — JSON imports would need import attributes, and
19
+ // client code imports the build JSON directly through Vite instead.
20
+ import fs from 'node:fs';
21
+ import path from 'node:path';
16
22
  import { serializer } from '@lowdefy/helpers';
17
- import raw from '../../build/theme.json';
23
+
24
+ const raw = JSON.parse(fs.readFileSync(path.join(process.cwd(), 'build/theme.json'), 'utf8'));
18
25
 
19
26
  export default serializer.deserialize(raw);
@@ -13,13 +13,22 @@
13
13
  See the License for the specific language governing permissions and
14
14
  limitations under the License.
15
15
  */
16
+
16
17
  /* eslint-disable react/jsx-props-no-spreading */
17
18
 
18
19
  import React from 'react';
19
- import AuthConfigured from './AuthConfigured.js';
20
+ import AuthConfigured from './AuthConfigured.jsx';
20
21
  import AuthNotConfigured from './AuthNotConfigured.js';
21
22
 
22
- import authConfig from '../../build/auth.js';
23
+ import { serializer } from '@lowdefy/helpers';
24
+
25
+ // Client code imports the build JSON directly — Vite handles JSON imports;
26
+ // the lib/build/*.js wrappers are server-only (they read from disk).
27
+ // Deserialize to restore arrays from their ~arr build markers (providers
28
+ // must be a real array for single-provider inference in createAuthMethods).
29
+ import rawAuthConfig from '../../../build/auth.json';
30
+
31
+ const authConfig = serializer.deserialize(rawAuthConfig);
23
32
 
24
33
  function Auth({ children, session }) {
25
34
  if (authConfig.configured === true) {