@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,59 +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
- // 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;
@@ -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
- // 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;
@@ -1,70 +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 { 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;
package/client/main.jsx DELETED
@@ -1,42 +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
- // 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} />);
@@ -1,261 +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 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 Cmd/Ctrl+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
- // Dev-only: Cmd/Ctrl+click any element to open the YAML that defines its
61
- // block in VS Code (vscode://file deep link, at the exact line). While the
62
- // modifier is held, the hovered block shows the same blue highlight box and
63
- // blockId chip as the annotation overlay's picking mode, and the cursor
64
- // switches to a pointer — so the developer sees exactly what a click will
65
- // open. Mounted always-on from Routing.jsx alongside Inspector and
66
- // FeedbackMount, and follows their resilience contract — every handler is
67
- // try/caught so this can never take the app down with it.
68
- //
69
- // The click resolves the nearest block and, when that id is generated at
70
- // runtime (list items, Dynamic endpoints), falls back through the ancestor
71
- // block chain until an id resolves in config — the same strategy the
72
- // feedback pipeline uses in enrichFeedback.js.
73
- function OpenInEditorListener({ basePath, pageId }) {
74
- const [hoverBlock, setHoverBlock] = useState(null); // { blockId, rect }
75
- // Refs keep the freshest values without re-binding the capture listeners
76
- // on every SPA navigation or hover change.
77
- const pageIdRef = useRef(pageId);
78
- pageIdRef.current = pageId;
79
- const hoverRef = useRef(hoverBlock);
80
- hoverRef.current = hoverBlock;
81
- const lastPointerRef = useRef(null); // { x, y }
82
-
83
- // Style tag lifecycle — the listener is always mounted, so this lives for
84
- // the app's lifetime and only the body class toggles per hover.
85
- useEffect(() => {
86
- injectStyleTag();
87
- return () => removeStyleTag();
88
- }, []);
89
-
90
- // Pointer cursor only while a block is highlighted — over non-block areas
91
- // the cursor stays native, signalling the click would do nothing.
92
- useEffect(() => {
93
- try {
94
- document.body.classList.toggle(BODY_CLASS, Boolean(hoverBlock));
95
- } catch {
96
- // no-op
97
- }
98
- return () => {
99
- try {
100
- document.body.classList.remove(BODY_CLASS);
101
- } catch {
102
- // no-op
103
- }
104
- };
105
- }, [hoverBlock]);
106
-
107
- useEffect(() => {
108
- function clearHover() {
109
- if (hoverRef.current) {
110
- setHoverBlock(null);
111
- }
112
- }
113
-
114
- function resolveHover() {
115
- try {
116
- const point = lastPointerRef.current;
117
- if (!point || feedbackOverlayOpen()) {
118
- clearHover();
119
- return;
120
- }
121
- const el = document.elementFromPoint(point.x, point.y);
122
- const blockId = nearestBlock(el);
123
- const blockEl = blockId ? document.getElementById(`bl-${blockId}`) : null;
124
- if (!blockEl) {
125
- clearHover();
126
- return;
127
- }
128
- const rect = blockEl.getBoundingClientRect();
129
- setHoverBlock({
130
- blockId,
131
- rect: { top: rect.top, left: rect.left, width: rect.width, height: rect.height },
132
- });
133
- } catch {
134
- clearHover();
135
- }
136
- }
137
-
138
- function onPointerMove(event) {
139
- try {
140
- lastPointerRef.current = { x: event.clientX, y: event.clientY };
141
- if (event.metaKey || event.ctrlKey) {
142
- resolveHover();
143
- } else {
144
- clearHover();
145
- }
146
- } catch {
147
- // Best-effort hover tracking.
148
- }
149
- }
150
-
151
- // Modifier pressed/released while the mouse is stationary — pointermove
152
- // alone would leave the highlight stale.
153
- function onKeyDown(event) {
154
- if (event.key === 'Meta' || event.key === 'Control') {
155
- resolveHover();
156
- }
157
- }
158
-
159
- function onKeyUp(event) {
160
- if (event.key === 'Meta' || event.key === 'Control') {
161
- clearHover();
162
- }
163
- }
164
-
165
- // Cmd+Tab away releases the modifier without a keyup reaching the page.
166
- function onBlur() {
167
- clearHover();
168
- }
169
-
170
- // Keep the highlight glued to the block while the app scrolls under a
171
- // stationary cursor.
172
- function onScroll() {
173
- if (hoverRef.current) {
174
- resolveHover();
175
- }
176
- }
177
-
178
- async function openBlockInEditor({ candidates }) {
179
- for (const blockId of candidates) {
180
- const location = await findBlockLocation({
181
- basePath,
182
- blockId,
183
- pageId: pageIdRef.current,
184
- });
185
- if (location) {
186
- window.location.assign(`vscode://file${location.file}:${location.line}`);
187
- return;
188
- }
189
- }
190
- try {
191
- // eslint-disable-next-line no-console
192
- console.info(
193
- `Lowdefy: no config location found for "${candidates[0]}" — the block is generated at runtime.`
194
- );
195
- } catch {
196
- // no-op
197
- }
198
- }
199
-
200
- function onClickCapture(event) {
201
- try {
202
- if (!(event.metaKey || event.ctrlKey) || event.shiftKey || event.altKey) {
203
- return;
204
- }
205
- if (feedbackOverlayOpen()) {
206
- return;
207
- }
208
- const candidates = blockAncestorChain(event.target);
209
- if (candidates.length === 0) {
210
- return;
211
- }
212
- // Only claim the click once a block is under the cursor — plain
213
- // Cmd/Ctrl+clicks elsewhere keep their default behaviour.
214
- event.preventDefault();
215
- event.stopPropagation();
216
- void openBlockInEditor({ candidates });
217
- } catch {
218
- // Never let the shortcut break the developer's app.
219
- }
220
- }
221
-
222
- window.addEventListener('pointermove', onPointerMove);
223
- window.addEventListener('keydown', onKeyDown);
224
- window.addEventListener('keyup', onKeyUp);
225
- window.addEventListener('blur', onBlur);
226
- window.addEventListener('scroll', onScroll, true);
227
- window.addEventListener('click', onClickCapture, { capture: true });
228
- return () => {
229
- window.removeEventListener('pointermove', onPointerMove);
230
- window.removeEventListener('keydown', onKeyDown);
231
- window.removeEventListener('keyup', onKeyUp);
232
- window.removeEventListener('blur', onBlur);
233
- window.removeEventListener('scroll', onScroll, true);
234
- window.removeEventListener('click', onClickCapture, { capture: true });
235
- };
236
- }, [basePath]);
237
-
238
- if (!hoverBlock || typeof document === 'undefined' || !document.body) {
239
- return null;
240
- }
241
- // data-lowdefy-feedback keeps this out of the annotation overlay's element
242
- // picking and out of tab-captured screenshots (both exclude that root).
243
- return createPortal(
244
- <div data-lowdefy-feedback data-lowdefy-open-in-editor style={overlayContainer}>
245
- <div
246
- style={{
247
- ...highlightBox,
248
- top: hoverBlock.rect.top,
249
- left: hoverBlock.rect.left,
250
- width: hoverBlock.rect.width,
251
- height: hoverBlock.rect.height,
252
- }}
253
- >
254
- <span style={labelChip}>{hoverBlock.blockId}</span>
255
- </div>
256
- </div>,
257
- document.body
258
- );
259
- }
260
-
261
- export default OpenInEditorListener;
@@ -1,51 +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
- // 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;
@@ -1,155 +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 React, { useEffect, useRef } from 'react';
18
- import { createAuthClient } from 'better-auth/react';
19
- import {
20
- adminClient,
21
- genericOAuthClient,
22
- magicLinkClient,
23
- organizationClient,
24
- phoneNumberClient,
25
- twoFactorClient,
26
- } from 'better-auth/client/plugins';
27
- import { passkeyClient } from '@better-auth/passkey/client';
28
-
29
- import { serializer } from '@lowdefy/helpers';
30
-
31
- import rawLowdefyConfig from '../../../build/config.json';
32
-
33
- const lowdefyConfig = serializer.deserialize(rawLowdefyConfig);
34
-
35
- const authClient = createAuthClient({
36
- baseURL: `${window.location.origin}${lowdefyConfig.basePath ?? ''}/api/auth`,
37
- plugins: [
38
- adminClient(),
39
- genericOAuthClient(),
40
- magicLinkClient(),
41
- organizationClient(),
42
- passkeyClient(),
43
- phoneNumberClient(),
44
- twoFactorClient(),
45
- ],
46
- });
47
-
48
- // The server resolves the caller per request and embeds it in the page
49
- // config, so the first render never flashes unauthenticated. The BetterAuth
50
- // client store takes over once its session fetch settles.
51
- //
52
- // Roles and merged attributes resolve server-side from the active member row
53
- // - the base session carries neither. The last server-resolved caller is
54
- // kept in a ref: while the session user is unchanged its roles and
55
- // attributes stay authoritative, and UpdateSession refreshes the ref from
56
- // /api/user after a change (e.g. SetActiveOrganization).
57
- function Session({ children, reloadSuppressedRef, serverUser }) {
58
- const { data: session, isPending } = authClient.useSession();
59
- const resolvedUserRef = useRef(serverUser);
60
- const wasAuthenticated = useRef(Boolean(serverUser));
61
-
62
- useEffect(() => {
63
- if (session) {
64
- wasAuthenticated.current = true;
65
- }
66
- // Reload after sign-out (or session revocation) so the server can apply
67
- // the page auth fork - a protected page redirects to the login page.
68
- // A logout with a callbackUrl suppresses the reload - its own navigation
69
- // would otherwise race this one.
70
- if (wasAuthenticated.current && !isPending && !session && !reloadSuppressedRef.current) {
71
- window.location.reload();
72
- }
73
- }, [session, isPending]);
74
-
75
- if (isPending) {
76
- return children(resolvedUserRef.current, resolvedUserRef);
77
- }
78
- if (!session?.user) {
79
- return children(null, resolvedUserRef);
80
- }
81
- const resolved = resolvedUserRef.current;
82
- const user =
83
- resolved && resolved.id === session.user.id
84
- ? { ...session.user, roles: resolved.roles, attributes: resolved.attributes }
85
- : { roles: [], ...session.user };
86
- return children(user, resolvedUserRef);
87
- }
88
-
89
- function AuthConfigured({ authConfig, children, serverUser }) {
90
- const reloadSuppressedRef = useRef(false);
91
- const auth = {
92
- authConfig,
93
- getSession: ({ disableCookieCache } = {}) =>
94
- authClient.getSession(disableCookieCache ? { query: { disableCookieCache: true } } : {}),
95
- // The server-resolved caller - roles from the active member row and the
96
- // merged attributes bag - for re-syncing after session changes.
97
- getResolvedUser: async () => {
98
- const response = await fetch(`${lowdefyConfig.basePath ?? ''}/api/user`, {
99
- credentials: 'same-origin',
100
- });
101
- if (!response.ok) {
102
- // Throw instead of parsing an error page - a silent { user: undefined }
103
- // would read as a client-side logout in updateSession.
104
- throw new Error(`Failed to fetch the resolved user (HTTP ${response.status}).`);
105
- }
106
- return response.json();
107
- },
108
- suppressSignOutReload: () => {
109
- reloadSuppressedRef.current = true;
110
- },
111
- acceptInvitation: (params) => authClient.organization.acceptInvitation(params),
112
- // addPasskey runs the WebAuthn browser ceremony itself - options fetch,
113
- // authenticator prompt, verification.
114
- addPasskey: (params) => authClient.passkey.addPasskey(params),
115
- changePassword: (params) => authClient.changePassword(params),
116
- deletePasskey: (params) => authClient.passkey.deletePasskey(params),
117
- impersonateUser: (params) => authClient.admin.impersonateUser(params),
118
- phoneNumberRequestPasswordReset: (params) =>
119
- authClient.phoneNumber.requestPasswordReset(params),
120
- phoneNumberResetPassword: (params) => authClient.phoneNumber.resetPassword(params),
121
- phoneNumberSendOtp: (params) => authClient.phoneNumber.sendOtp(params),
122
- phoneNumberVerify: (params) => authClient.phoneNumber.verify(params),
123
- requestPasswordReset: (params) => authClient.requestPasswordReset(params),
124
- resetPassword: (params) => authClient.resetPassword(params),
125
- revokeOtherSessions: () => authClient.revokeOtherSessions(),
126
- sendVerificationEmail: (params) => authClient.sendVerificationEmail(params),
127
- setActiveOrganization: (params) => authClient.organization.setActive(params),
128
- signInEmail: (params) => authClient.signIn.email(params),
129
- signInMagicLink: (params) => authClient.signIn.magicLink(params),
130
- signInOauth2: (params) => authClient.signIn.oauth2(params),
131
- signInPasskey: (params) => authClient.signIn.passkey(params),
132
- signInPhoneNumber: (params) => authClient.signIn.phoneNumber(params),
133
- signInSocial: (params) => authClient.signIn.social(params),
134
- signOut: () => authClient.signOut(),
135
- signUpEmail: (params) => authClient.signUp.email(params),
136
- stopImpersonating: () => authClient.admin.stopImpersonating(),
137
- twoFactorDisable: (params) => authClient.twoFactor.disable(params),
138
- twoFactorEnable: (params) => authClient.twoFactor.enable(params),
139
- twoFactorVerifyBackupCode: (params) => authClient.twoFactor.verifyBackupCode(params),
140
- twoFactorVerifyTotp: (params) => authClient.twoFactor.verifyTotp(params),
141
- };
142
- return (
143
- <Session reloadSuppressedRef={reloadSuppressedRef} serverUser={serverUser}>
144
- {(user, resolvedUserRef) => {
145
- auth.user = user;
146
- auth.updateResolvedUser = (resolved) => {
147
- resolvedUserRef.current = resolved;
148
- };
149
- return children(auth);
150
- }}
151
- </Session>
152
- );
153
- }
154
-
155
- export default AuthConfigured;