@lowdefy/server-dev 5.6.0 → 6.1.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.
- package/{pages/_app.js → client/App.jsx} +21 -33
- package/client/DevStreamContext.js +30 -0
- package/client/Inspector.jsx +226 -0
- package/{lib/client/Page.js → client/Page.jsx} +4 -8
- package/client/Reload.jsx +85 -0
- package/client/Routing.jsx +117 -0
- package/client/clientBuildImports.test.mjs +61 -0
- package/client/feedback/FeedbackMount.jsx +83 -0
- package/client/feedback/FeedbackOverlay.jsx +901 -0
- package/client/feedback/captureTabScreenshot.js +84 -0
- package/client/feedback/copyToClipboard.js +45 -0
- package/client/feedback/drawAnnotationsSvg.js +119 -0
- package/client/feedback/elementInspect.js +99 -0
- package/client/feedback/feedbackStyles.js +301 -0
- package/client/feedback/pinResponsiveImages.js +59 -0
- package/client/feedback/sendFeedback.js +39 -0
- package/client/feedback/useFeedbackToggle.js +70 -0
- package/client/main.jsx +42 -0
- package/client/openInEditor/OpenInEditorListener.jsx +268 -0
- package/client/openInEditor/findBlockLocation.js +51 -0
- package/lib/build/app.js +8 -1
- package/lib/build/appMeta.js +8 -1
- package/lib/build/auth.js +8 -1
- package/lib/build/config.js +8 -1
- package/lib/build/i18n.js +8 -1
- package/lib/build/logger.js +8 -1
- package/lib/build/theme.js +8 -1
- package/lib/client/auth/{Auth.js → Auth.jsx} +11 -2
- package/lib/client/auth/{AuthConfigured.js → AuthConfigured.jsx} +22 -10
- package/lib/client/setPageId.js +6 -5
- package/lib/client/utils/useMutateCache.js +14 -1
- package/lib/client/utils/usePageConfig.js +45 -43
- package/lib/client/utils/usePageConfig.test.mjs +62 -0
- package/lib/docs/captureAnnotatedScreenshot.js +88 -0
- package/lib/docs/checkpointPaths.js +91 -0
- package/lib/docs/checkpointStore.js +240 -0
- package/lib/docs/checkpointToMocks.js +65 -0
- package/lib/docs/clientErrorStore.js +36 -0
- package/lib/docs/configCheckpoints.test.mjs +156 -0
- package/lib/docs/createConfigCheckpoint.js +92 -0
- package/lib/docs/createDocsMcpServer.js +531 -0
- package/lib/docs/createDocsMcpServer.test.mjs +141 -0
- package/lib/docs/devMockRegistry.js +68 -0
- package/lib/docs/docs.test.mjs +448 -0
- package/lib/docs/enrichFeedback.js +71 -0
- package/lib/docs/evalOperator.js +47 -0
- package/lib/docs/evalOperator.test.mjs +157 -0
- package/lib/docs/evalOperatorHeadless.js +87 -0
- package/lib/docs/evalOperatorHeadless.test.mjs +72 -0
- package/lib/docs/evalOperatorInTab.js +48 -0
- package/lib/docs/findConfig.js +260 -0
- package/lib/docs/formatFeedback.js +121 -0
- package/lib/docs/formatFeedback.test.mjs +204 -0
- package/lib/docs/getAppMap.js +180 -0
- package/lib/docs/getBrowser.js +127 -0
- package/lib/docs/getBrowser.test.mjs +122 -0
- package/lib/docs/getBuildStatus.js +37 -0
- package/lib/docs/getCoreDoc.js +63 -0
- package/lib/docs/getDocsManifest.js +40 -0
- package/lib/docs/getExamples.js +58 -0
- package/lib/docs/getOverview.js +115 -0
- package/lib/docs/getPageConfig.js +53 -0
- package/lib/docs/getPluginDoc.js +55 -0
- package/lib/docs/getSchema.js +65 -0
- package/lib/docs/inspectState.js +47 -0
- package/lib/docs/inspectState.test.mjs +140 -0
- package/lib/docs/inspectStateFromTab.js +40 -0
- package/lib/docs/inspectStateHeadless.js +84 -0
- package/lib/docs/inspectStateHeadless.test.mjs +60 -0
- package/lib/docs/isWriteRequestsAllowed.js +50 -0
- package/lib/docs/listConfigCheckpoints.js +55 -0
- package/lib/docs/listPlugins.js +80 -0
- package/lib/docs/listTypes.js +105 -0
- package/lib/docs/loadState.js +152 -0
- package/lib/docs/mapPageBuildErrors.js +28 -0
- package/lib/docs/normalizeTypeKind.js +40 -0
- package/{pages/api/endpoints/[...endpointId].js → lib/docs/readBuildArtifact.js} +15 -12
- package/lib/docs/readPageArtifact.js +26 -0
- package/lib/docs/resolvePluginDir.js +49 -0
- package/lib/docs/resolveSource.js +48 -0
- package/lib/docs/resolveSource.test.mjs +100 -0
- package/lib/docs/revertConfigCheckpoint.js +73 -0
- package/lib/docs/runRequest.js +141 -0
- package/lib/docs/saveAnnotatedScreenshot.js +43 -0
- package/lib/docs/scaffoldPage.js +75 -0
- package/lib/docs/scaffoldPage.test.mjs +97 -0
- package/lib/docs/screenshotPage.js +110 -0
- package/lib/docs/screenshotPage.test.mjs +59 -0
- package/lib/docs/screenshotPageClip.test.mjs +102 -0
- package/lib/docs/searchDocs.js +67 -0
- package/lib/docs/setupTestFixtures.mjs +335 -0
- package/lib/docs/snapshotState.js +59 -0
- package/lib/docs/stateCheckpoints.test.mjs +366 -0
- package/lib/docs/tabAvailable.js +28 -0
- package/lib/docs/tabChannel.js +119 -0
- package/lib/docs/tabChannel.test.mjs +130 -0
- package/lib/server/auth/{getAuthOptions.js → getAuthConfig.js} +9 -4
- package/lib/server/auth/getDevSession.js +70 -0
- package/lib/server/auth/getDevSession.test.mjs +138 -0
- package/lib/server/auth/getHeadlessUser.js +40 -0
- package/lib/server/auth/{getMockSession.js → getMockUser.js} +8 -21
- package/lib/server/auth/headlessUser.js +29 -0
- package/lib/server/auth/resolveHeadlessUser.js +38 -0
- package/lib/server/auth/resolveHeadlessUser.test.mjs +78 -0
- package/lib/server/auth/session.js +39 -0
- package/lib/server/auth/strategies.js +47 -0
- package/lib/server/createLowdefyContext.js +128 -0
- package/lib/server/createLowdefyContext.test.mjs +117 -0
- package/lib/server/getPageJitEnrichment.test.mjs +105 -0
- package/lib/server/jitPageBuilder.js +125 -12
- package/lib/server/log/createHandleError.test.mjs +126 -0
- package/lib/server/log/createLogger.test.mjs +62 -0
- package/lib/server/scrubSecrets.js +23 -0
- package/manager/getContext.mjs +3 -7
- package/manager/processes/initialBuild.mjs +0 -2
- package/manager/processes/installPlugins.mjs +19 -11
- package/manager/processes/lowdefyBuild.mjs +29 -7
- package/manager/processes/lowdefyBuild.test.mjs +102 -0
- package/manager/processes/readDotEnv.mjs +13 -1
- package/manager/processes/shutdownServer.mjs +6 -6
- package/manager/processes/startProxy.mjs +160 -0
- package/manager/processes/startProxy.test.mjs +102 -0
- package/manager/processes/startServer.mjs +33 -16
- package/manager/processes/startWatchers.mjs +2 -2
- package/manager/processes/warnAuthUrlPortMismatch.mjs +47 -0
- package/manager/processes/warnAuthUrlPortMismatch.test.mjs +93 -0
- package/manager/run.mjs +50 -0
- package/manager/utils/createCustomPluginMessagesMap.mjs +8 -1
- package/manager/utils/createCustomPluginTypesMap.mjs +1 -0
- package/manager/utils/formatNoticeBox.mjs +30 -0
- package/manager/utils/{getNextBin.mjs → getViteBin.mjs} +7 -10
- package/manager/utils/updatePageTailwindCss.mjs +7 -1
- package/manager/utils/writeBuildStatus.mjs +39 -0
- package/manager/utils/writeBuildStatus.test.mjs +76 -0
- package/manager/watchers/lowdefyBuildWatcher.mjs +0 -1
- package/manager/watchers/moduleBuildWatcher.mjs +0 -1
- package/manager/watchers/{nextBuildWatcher.mjs → serverArtifactWatcher.mjs} +18 -20
- package/package.json +62 -49
- package/package.original.json +65 -52
- package/postcss.config.cjs +1 -0
- package/src/app.js +175 -0
- package/src/html/renderDevPage.js +86 -0
- package/src/lib/getPathSegments.js +25 -0
- package/src/lib/safeScriptJson.js +38 -0
- package/src/middleware/apiContext.js +32 -0
- package/src/middleware/errorHandler.js +48 -0
- package/src/middleware/errorHandler.test.mjs +247 -0
- package/src/routes/agent.js +64 -0
- package/src/routes/apiPage.js +37 -0
- package/src/routes/auth.js +49 -0
- package/src/routes/auth.test.mjs +84 -0
- package/src/routes/clientError.js +57 -0
- package/src/routes/cron.js +47 -0
- package/src/routes/cronForward.js +47 -0
- package/src/routes/detached.js +47 -0
- package/src/routes/devInspect.js +111 -0
- package/src/routes/devInspect.test.mjs +119 -0
- package/{pages/[[...pageId]].js → src/routes/devTools.js} +4 -2
- package/{pages/404.js → src/routes/docs/appMap.js} +6 -2
- package/{pages/api/dev-tools.js → src/routes/docs/buildStatus.js} +5 -4
- package/src/routes/docs/checkpointsCreate.js +28 -0
- package/src/routes/docs/checkpointsList.js +23 -0
- package/{pages/api/icons/dynamic.js → src/routes/docs/checkpointsRevert.js} +11 -12
- package/src/routes/docs/content.js +31 -0
- package/src/routes/docs/evalOperator.js +56 -0
- package/src/routes/docs/examples.js +33 -0
- package/src/routes/docs/find.js +30 -0
- package/src/routes/docs/index.js +23 -0
- package/src/routes/docs/inspectState.js +44 -0
- package/src/routes/docs/loadState.js +49 -0
- package/src/routes/docs/mcp.js +30 -0
- package/src/routes/docs/pageConfig.js +36 -0
- package/src/routes/docs/parseUserParam.js +52 -0
- package/src/routes/docs/parseUserParam.test.mjs +45 -0
- package/src/routes/docs/pluginDoc.js +33 -0
- package/src/routes/docs/plugins.js +23 -0
- package/src/routes/docs/runRequest.js +36 -0
- package/{lib/server/auth/getServerSession.js → src/routes/docs/schema.js} +17 -15
- package/src/routes/docs/screenshot.js +72 -0
- package/src/routes/docs/search.js +27 -0
- package/src/routes/docs/snapshotState.js +36 -0
- package/src/routes/docs/stateCheckpointsList.js +26 -0
- package/src/routes/docs/types.js +33 -0
- package/src/routes/endpoints.js +71 -0
- package/src/routes/feedback.js +110 -0
- package/src/routes/feedback.test.mjs +204 -0
- package/src/routes/jitPage.js +86 -0
- package/src/routes/mcp.js +34 -0
- package/{pages/api → src/routes}/ping.js +4 -2
- package/src/routes/reload.js +79 -0
- package/src/routes/request.js +74 -0
- package/src/routes/request.test.mjs +124 -0
- package/{pages/api → src/routes}/root.js +4 -4
- package/src/routes/usage.js +46 -0
- package/src/routes/websocket.js +33 -0
- package/src/websocket/devWebSocket.js +78 -0
- package/vite.config.js +111 -0
- package/.eslintrc.yaml +0 -1
- package/lib/client/App.js +0 -80
- package/lib/client/Reload.js +0 -60
- package/lib/server/apiWrapper.js +0 -109
- package/lib/server/compileCss.js +0 -39
- package/manager/processes/compileCss.mjs +0 -54
- package/manager/processes/nextBuild.mjs +0 -50
- package/next.config.js +0 -25
- package/pages/_document.js +0 -109
- package/pages/api/agent/[...path].js +0 -86
- package/pages/api/auth/[...nextauth].js +0 -48
- package/pages/api/client-error.js +0 -45
- package/pages/api/js/[env].js +0 -42
- package/pages/api/page/[...pageId].js +0 -74
- package/pages/api/reload.js +0 -53
- package/pages/api/request/[...path].js +0 -41
- /package/lib/client/{BuildErrorPage.js → BuildErrorPage.jsx} +0 -0
- /package/lib/client/{BuildingPage.js → BuildingPage.jsx} +0 -0
- /package/lib/client/{ErrorBar.js → ErrorBar.jsx} +0 -0
- /package/lib/client/{InstallingPluginsPage.js → InstallingPluginsPage.jsx} +0 -0
- /package/lib/client/{RestartingPage.js → RestartingPage.jsx} +0 -0
|
@@ -13,12 +13,29 @@
|
|
|
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, { useEffect, useRef } from 'react';
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
authConfigManager,
|
|
22
|
+
getSession,
|
|
23
|
+
SessionProvider,
|
|
24
|
+
signIn,
|
|
25
|
+
signOut,
|
|
26
|
+
useSession,
|
|
27
|
+
} from '@hono/auth-js/react';
|
|
28
|
+
|
|
29
|
+
// The app basePath, from Vite's BASE_URL (`${config.basePath}/`, set in
|
|
30
|
+
// vite.config.js). build/config.json is server-only - it carries every
|
|
31
|
+
// deployment environment's settings - so it is never imported into the client.
|
|
32
|
+
const basePath = import.meta.env.BASE_URL.replace(/\/$/, '');
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
// @hono/auth-js/react configures its fetch paths through a module-level
|
|
35
|
+
// manager instead of SessionProvider props.
|
|
36
|
+
if (basePath) {
|
|
37
|
+
authConfigManager.setConfig({ basePath: `${basePath}/api/auth` });
|
|
38
|
+
}
|
|
22
39
|
|
|
23
40
|
function Session({ children }) {
|
|
24
41
|
const wasAuthenticated = useRef(false);
|
|
@@ -31,9 +48,8 @@ function Session({ children }) {
|
|
|
31
48
|
}
|
|
32
49
|
}, [status]);
|
|
33
50
|
|
|
34
|
-
// If session is passed to SessionProvider from
|
|
35
|
-
// we won't have a loading state here.
|
|
36
|
-
// But 404 uses getStaticProps so we have this for 404.
|
|
51
|
+
// If session is passed to SessionProvider from the server-rendered config
|
|
52
|
+
// we won't have a loading state here, but unauthenticated first loads do.
|
|
37
53
|
if (status === 'loading') {
|
|
38
54
|
return '';
|
|
39
55
|
}
|
|
@@ -42,12 +58,8 @@ function Session({ children }) {
|
|
|
42
58
|
|
|
43
59
|
function AuthConfigured({ authConfig, children, serverSession }) {
|
|
44
60
|
const auth = { authConfig, getSession, signIn, signOut };
|
|
45
|
-
let basePath = lowdefyConfig.basePath;
|
|
46
|
-
if (basePath) {
|
|
47
|
-
basePath = `${basePath}/api/auth`;
|
|
48
|
-
}
|
|
49
61
|
return (
|
|
50
|
-
<SessionProvider session={serverSession}
|
|
62
|
+
<SessionProvider session={serverSession}>
|
|
51
63
|
<Session>
|
|
52
64
|
{(session) => {
|
|
53
65
|
auth.session = session;
|
package/lib/client/setPageId.js
CHANGED
|
@@ -14,18 +14,19 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
// location comes from the custom router (@lowdefy/client/adapters):
|
|
18
|
+
// { pageId, pathname, search }. pageId is null at the root path.
|
|
19
|
+
function setPageId(location, rootConfig) {
|
|
20
|
+
if (location.pageId === '404') {
|
|
19
21
|
return { redirect: false, pageId: '404' };
|
|
20
22
|
}
|
|
21
|
-
|
|
22
|
-
if (!segments || segments.length === 0) {
|
|
23
|
+
if (!location.pageId) {
|
|
23
24
|
if (rootConfig.home.configured === false) {
|
|
24
25
|
return { redirect: true, pageId: rootConfig.home.pageId };
|
|
25
26
|
}
|
|
26
27
|
return { redirect: false, pageId: rootConfig.home.pageId };
|
|
27
28
|
}
|
|
28
|
-
return { redirect: false, pageId:
|
|
29
|
+
return { redirect: false, pageId: location.pageId };
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export default setPageId;
|
|
@@ -16,12 +16,25 @@
|
|
|
16
16
|
|
|
17
17
|
import { useSWRConfig } from 'swr';
|
|
18
18
|
|
|
19
|
+
// Module-level counters that bust usePageConfig SWR keys: reloadVersion moves
|
|
20
|
+
// on config reloads (all pages), navVersion on client-side navigations
|
|
21
|
+
// (dynamic pages only — server-resolved content must re-resolve per
|
|
22
|
+
// navigation, never serve from the SWR cache).
|
|
19
23
|
let reloadVersion = 0;
|
|
24
|
+
let navVersion = 0;
|
|
20
25
|
|
|
21
26
|
function getReloadVersion() {
|
|
22
27
|
return reloadVersion;
|
|
23
28
|
}
|
|
24
29
|
|
|
30
|
+
function bumpNavVersion() {
|
|
31
|
+
navVersion += 1;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getNavVersion() {
|
|
35
|
+
return navVersion;
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
function useMutateCache(basePath) {
|
|
26
39
|
const { mutate } = useSWRConfig();
|
|
27
40
|
return () => {
|
|
@@ -34,5 +47,5 @@ function useMutateCache(basePath) {
|
|
|
34
47
|
};
|
|
35
48
|
}
|
|
36
49
|
|
|
37
|
-
export { getReloadVersion };
|
|
50
|
+
export { bumpNavVersion, getNavVersion, getReloadVersion };
|
|
38
51
|
export default useMutateCache;
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
|
|
14
14
|
import useSWR from 'swr';
|
|
15
15
|
|
|
16
|
-
import { getReloadVersion } from './useMutateCache.js';
|
|
16
|
+
import { getNavVersion, getReloadVersion } from './useMutateCache.js';
|
|
17
|
+
|
|
18
|
+
// URLs whose config is server-resolved per request — learned from the fetched
|
|
19
|
+
// config's dynamic flag, so the first visit caches like a static page and
|
|
20
|
+
// every later navigation refetches.
|
|
21
|
+
const dynamicUrls = new Set();
|
|
17
22
|
|
|
18
23
|
function parseJsModule(text) {
|
|
19
24
|
const fn = new Function('exports', text.replace('export default', 'exports.default ='));
|
|
@@ -22,30 +27,24 @@ function parseJsModule(text) {
|
|
|
22
27
|
return mod.default ?? {};
|
|
23
28
|
}
|
|
24
29
|
|
|
25
|
-
async function
|
|
30
|
+
export async function fetchPageConfig(url) {
|
|
31
|
+
// A stalled request (server restart mid-request, exhausted sockets) must
|
|
32
|
+
// become a visible error, never an eternal Suspense fallback — the reload
|
|
33
|
+
// recovery path cannot fire while the page tree is suspended.
|
|
34
|
+
let res;
|
|
26
35
|
try {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
res = await fetch(url, {
|
|
37
|
+
headers: { 'Content-Type': 'application/json' },
|
|
38
|
+
signal: AbortSignal.timeout(30_000),
|
|
39
|
+
});
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (error.name === 'TimeoutError' || error.name === 'AbortError') {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Page config request "${url}" timed out - the dev server may be restarting. Reload the page.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
32
47
|
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function fetchDynamicIcons(basePath) {
|
|
36
|
-
try {
|
|
37
|
-
const res = await fetch(`${basePath}/api/icons/dynamic`);
|
|
38
|
-
if (!res.ok) return {};
|
|
39
|
-
return parseJsModule(await res.text());
|
|
40
|
-
} catch {
|
|
41
|
-
return {};
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function fetchPageConfig(url) {
|
|
46
|
-
const res = await fetch(url, {
|
|
47
|
-
headers: { 'Content-Type': 'application/json' },
|
|
48
|
-
});
|
|
49
48
|
if (res.status === 404) {
|
|
50
49
|
return null;
|
|
51
50
|
}
|
|
@@ -60,34 +59,37 @@ async function fetchPageConfig(url) {
|
|
|
60
59
|
throw new Error(data.message || 'Request error');
|
|
61
60
|
}
|
|
62
61
|
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
fetchJsEntries(basePath),
|
|
69
|
-
fetchDynamicIcons(basePath),
|
|
70
|
-
]);
|
|
71
|
-
data._jsEntries = jsEntries;
|
|
72
|
-
data._dynamicIcons = dynamicIcons;
|
|
62
|
+
// The JIT build folds this page's _js entries and dynamic icons into the
|
|
63
|
+
// response, so first paint needs no secondary fetch. _jsEntries arrives as
|
|
64
|
+
// module text — compile it to the { hash: fn } object Page expects.
|
|
65
|
+
// _dynamicIcons is already plain data — leave it for Page to inject.
|
|
66
|
+
if (data._jsEntries) data._jsEntries = parseJsModule(data._jsEntries);
|
|
73
67
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (cssLink) {
|
|
77
|
-
cssLink.href = `${basePath}/tailwind-jit.css?v=${Date.now()}`;
|
|
68
|
+
if (data?.dynamic === true) {
|
|
69
|
+
dynamicUrls.add(url);
|
|
78
70
|
}
|
|
79
71
|
|
|
80
72
|
return data;
|
|
81
73
|
}
|
|
82
74
|
|
|
83
75
|
function usePageConfig(pageId, basePath) {
|
|
84
|
-
|
|
76
|
+
// Forward the current query string so server-side Dynamic block resolution
|
|
77
|
+
// sees the same urlQuery as an initial HTML load. Including it in the SWR
|
|
78
|
+
// key also caches dynamic pages per query string.
|
|
79
|
+
const url = `${basePath}/api/page/${pageId}${window.location.search}`;
|
|
85
80
|
// Include reloadVersion in the SWR key so that after a config reload,
|
|
86
|
-
// previously cached page data is not reused.
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
// previously cached page data is not reused. Dynamic pages also key on the
|
|
82
|
+
// navigation version — server-resolved content must re-resolve on every
|
|
83
|
+
// navigation, never serve from the SWR cache. The fetcher receives
|
|
84
|
+
// [url, ...versions] but only uses url — the versions just bust the cache.
|
|
85
|
+
const navVersion = dynamicUrls.has(url) ? getNavVersion() : 0;
|
|
86
|
+
const { data } = useSWR(
|
|
87
|
+
[url, getReloadVersion(), navVersion],
|
|
88
|
+
([fetchUrl]) => fetchPageConfig(fetchUrl),
|
|
89
|
+
{
|
|
90
|
+
suspense: true,
|
|
91
|
+
}
|
|
92
|
+
);
|
|
91
93
|
return { data };
|
|
92
94
|
}
|
|
93
95
|
|
|
@@ -0,0 +1,62 @@
|
|
|
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
|
+
// usePageConfig imports swr and useMutateCache only for the hook; the fetcher
|
|
20
|
+
// under test needs neither. Mock them so the module loads in the node test env.
|
|
21
|
+
jest.unstable_mockModule('swr', () => ({ default: jest.fn() }));
|
|
22
|
+
jest.unstable_mockModule('./useMutateCache.js', () => ({
|
|
23
|
+
getNavVersion: jest.fn(),
|
|
24
|
+
getReloadVersion: jest.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const { fetchPageConfig } = await import('./usePageConfig.js');
|
|
28
|
+
|
|
29
|
+
function mockJsonResponse(body) {
|
|
30
|
+
return { status: 200, ok: true, json: async () => body };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
delete global.fetch;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('fetchPageConfig compiles inlined _jsEntries module text into a { hash: fn } object', async () => {
|
|
38
|
+
global.fetch = jest.fn(async () =>
|
|
39
|
+
mockJsonResponse({
|
|
40
|
+
id: 'p',
|
|
41
|
+
_jsEntries: "export default { 'h1': ({ args }) => { return args.x + 1; } };",
|
|
42
|
+
_dynamicIcons: { FiZap: { tag: 'svg' } },
|
|
43
|
+
})
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const data = await fetchPageConfig('http://localhost/api/page/p');
|
|
47
|
+
|
|
48
|
+
expect(typeof data._jsEntries.h1).toBe('function');
|
|
49
|
+
expect(data._jsEntries.h1({ args: { x: 1 } })).toBe(2);
|
|
50
|
+
// _dynamicIcons passes through untouched.
|
|
51
|
+
expect(data._dynamicIcons).toEqual({ FiZap: { tag: 'svg' } });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('fetchPageConfig issues only the page-config request — no /api/js or /api/icons fetch', async () => {
|
|
55
|
+
global.fetch = jest.fn(async () => mockJsonResponse({ id: 'p' }));
|
|
56
|
+
|
|
57
|
+
const data = await fetchPageConfig('http://localhost/api/page/p');
|
|
58
|
+
|
|
59
|
+
expect(data._jsEntries).toBeUndefined();
|
|
60
|
+
expect(global.fetch).toHaveBeenCalledTimes(1);
|
|
61
|
+
expect(global.fetch.mock.calls[0][0]).toBe('http://localhost/api/page/p');
|
|
62
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
|
|
20
|
+
import drawAnnotationsSvg from '../../client/feedback/drawAnnotationsSvg.js';
|
|
21
|
+
import { getBrowser, openPage } from './getBrowser.js';
|
|
22
|
+
|
|
23
|
+
// Renders the page headless at the batch's recorded viewport/scroll, injects
|
|
24
|
+
// the developer's drawn shapes and element outlines as an SVG layer inside
|
|
25
|
+
// the page, screenshots the composite, and saves it under the config dir's
|
|
26
|
+
// .lowdefy/annotations/ (gitignored). The saved path goes into the formatted
|
|
27
|
+
// feedback text so an agent can read the image. Never throws — returns
|
|
28
|
+
// { path } or { error }.
|
|
29
|
+
async function captureAnnotatedScreenshot({ origin, batch, fileName }) {
|
|
30
|
+
let browser;
|
|
31
|
+
try {
|
|
32
|
+
browser = await getBrowser();
|
|
33
|
+
} catch (error) {
|
|
34
|
+
return { error: `No Chromium available for the annotated screenshot (${error.message}).` };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const viewport = batch.viewport ?? {};
|
|
38
|
+
const width = viewport.width || 1280;
|
|
39
|
+
const height = viewport.height || 800;
|
|
40
|
+
const urlQuery = batch.urlQuery && batch.urlQuery !== '?' ? batch.urlQuery : '';
|
|
41
|
+
|
|
42
|
+
let context;
|
|
43
|
+
try {
|
|
44
|
+
const opened = await openPage({
|
|
45
|
+
browser,
|
|
46
|
+
origin,
|
|
47
|
+
pageId: `${batch.pageId}${urlQuery}`,
|
|
48
|
+
width,
|
|
49
|
+
height,
|
|
50
|
+
});
|
|
51
|
+
context = opened.context;
|
|
52
|
+
const { page } = opened;
|
|
53
|
+
|
|
54
|
+
if (viewport.scrollX || viewport.scrollY) {
|
|
55
|
+
await page.evaluate(
|
|
56
|
+
([x, y]) => window.scrollTo(x, y),
|
|
57
|
+
[viewport.scrollX ?? 0, viewport.scrollY ?? 0]
|
|
58
|
+
);
|
|
59
|
+
await page.waitForTimeout(200);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Shapes and element rects are viewport-relative at the recorded scroll
|
|
63
|
+
// offset — after scrolling to the same offset they overlay 1:1. The
|
|
64
|
+
// compositor is shared with the in-tab capture (drawAnnotationsSvg is
|
|
65
|
+
// self-contained, so Playwright can serialize it into the page).
|
|
66
|
+
await page.evaluate(drawAnnotationsSvg, batch.annotations ?? []);
|
|
67
|
+
await page.waitForTimeout(150);
|
|
68
|
+
|
|
69
|
+
const buffer = await page.screenshot({ type: 'png' });
|
|
70
|
+
|
|
71
|
+
const configDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd();
|
|
72
|
+
const dir = path.join(configDirectory, '.lowdefy', 'annotations');
|
|
73
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
74
|
+
const name =
|
|
75
|
+
fileName ?? `${batch.pageId}-${new Date().toISOString().replace(/[:.]/g, '-')}.png`;
|
|
76
|
+
fs.writeFileSync(path.join(dir, name), buffer);
|
|
77
|
+
|
|
78
|
+
return { path: path.join('.lowdefy', 'annotations', name) };
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return { error: `Failed to capture annotated screenshot: ${error.message}` };
|
|
81
|
+
} finally {
|
|
82
|
+
if (context) {
|
|
83
|
+
await context.close();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export default captureAnnotatedScreenshot;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import fs from 'fs';
|
|
18
|
+
import path from 'path';
|
|
19
|
+
|
|
20
|
+
// Shared path/file-listing helpers for createConfigCheckpoint.js,
|
|
21
|
+
// listConfigCheckpoints.js and revertConfigCheckpoint.js.
|
|
22
|
+
|
|
23
|
+
const CONFIG_FILE_EXTENSIONS = ['.yaml', '.yml', '.json', '.md', '.js', '.env'];
|
|
24
|
+
const EXCLUDED_DIRECTORY_NAMES = ['.lowdefy', 'node_modules', '.git', 'checkpoints'];
|
|
25
|
+
const MAX_CHECKPOINTS = 20;
|
|
26
|
+
const MANIFEST_FILE_NAME = 'manifest.json';
|
|
27
|
+
|
|
28
|
+
function getConfigDirectory() {
|
|
29
|
+
return process.env.LOWDEFY_DIRECTORY_CONFIG || process.cwd();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getCheckpointsRoot() {
|
|
33
|
+
return path.join(getConfigDirectory(), '.lowdefy', 'checkpoints');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// path.extname('.env') is '' (dotfiles have no extension in Node's eyes), so a
|
|
37
|
+
// dotfile named exactly ".env" needs its own basename check.
|
|
38
|
+
function isConfigFile(filePath) {
|
|
39
|
+
if (path.basename(filePath) === '.env') {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return CONFIG_FILE_EXTENSIONS.includes(path.extname(filePath));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Recursively lists config-relevant files under `configDirectory`, returned as
|
|
46
|
+
// paths relative to it. Skips directories that should never be snapshotted —
|
|
47
|
+
// build output (.lowdefy, which also holds the checkpoints themselves),
|
|
48
|
+
// dependencies (node_modules) and version control (.git).
|
|
49
|
+
function listConfigFiles({ configDirectory }) {
|
|
50
|
+
const results = [];
|
|
51
|
+
|
|
52
|
+
function walk(currentDir) {
|
|
53
|
+
for (const entry of fs.readdirSync(currentDir, { withFileTypes: true })) {
|
|
54
|
+
const fullPath = path.join(currentDir, entry.name);
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
if (EXCLUDED_DIRECTORY_NAMES.includes(entry.name)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
walk(fullPath);
|
|
60
|
+
} else if (entry.isFile() && isConfigFile(fullPath)) {
|
|
61
|
+
results.push(path.relative(configDirectory, fullPath));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (fs.existsSync(configDirectory)) {
|
|
67
|
+
walk(configDirectory);
|
|
68
|
+
}
|
|
69
|
+
return results;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Slugifies a checkpoint label for use in a directory name — lowercase,
|
|
73
|
+
// non-alphanumeric runs collapsed to a single "-", leading/trailing "-"
|
|
74
|
+
// trimmed. Falls back to "checkpoint" when the label slugifies to nothing
|
|
75
|
+
// (e.g. an empty string or a label made only of punctuation).
|
|
76
|
+
function slug(label) {
|
|
77
|
+
const slugified = String(label ?? '')
|
|
78
|
+
.toLowerCase()
|
|
79
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
80
|
+
.replace(/^-+|-+$/g, '');
|
|
81
|
+
return slugified === '' ? 'checkpoint' : slugified;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export {
|
|
85
|
+
getCheckpointsRoot,
|
|
86
|
+
getConfigDirectory,
|
|
87
|
+
listConfigFiles,
|
|
88
|
+
MANIFEST_FILE_NAME,
|
|
89
|
+
MAX_CHECKPOINTS,
|
|
90
|
+
slug,
|
|
91
|
+
};
|