@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
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import lowdefyConfig from '../build/config.js';
|
|
20
|
+
import { getBrowser, openPage } from './getBrowser.js';
|
|
21
|
+
import { loadMocks } from './devMockRegistry.js';
|
|
22
|
+
import { readCheckpoint } from './checkpointStore.js';
|
|
23
|
+
|
|
24
|
+
const VERIFY_KEY_COUNT = 3;
|
|
25
|
+
|
|
26
|
+
// Loads a state checkpoint's recorded requests into devMockRegistry (shared
|
|
27
|
+
// by both modes below — src/routes/request.js consults it regardless of how
|
|
28
|
+
// the page was opened), then either:
|
|
29
|
+
// - 'headless': drives a headless page to the checkpoint's pageId/urlQuery,
|
|
30
|
+
// injects its recorded state directly into the live context, and verifies
|
|
31
|
+
// a few keys round-tripped. Good for an agent verifying its own change.
|
|
32
|
+
// Takes a `user` so a checkpoint captured on a role-gated page restores
|
|
33
|
+
// into a page that actually renders (see resolveHeadlessUser.js).
|
|
34
|
+
// - 'registry-only': just returns a URL a human can open in a real browser
|
|
35
|
+
// tab — client/Inspector.jsx's `?_checkpoint=` bootstrap does the state
|
|
36
|
+
// injection client-side once that tab loads.
|
|
37
|
+
async function loadState({ origin, name, mode = 'headless', user }) {
|
|
38
|
+
if (type.isNone(name) || !type.isString(name)) {
|
|
39
|
+
return { error: `loadState requires a "name" string. Received ${JSON.stringify(name)}.` };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// `user` only reaches a page this function opens itself. In 'registry-only'
|
|
43
|
+
// mode the developer opens the returned URL in their own browser, carrying
|
|
44
|
+
// their own session, so an injected caller would be silently dropped.
|
|
45
|
+
if (!type.isNone(user) && mode === 'registry-only') {
|
|
46
|
+
return {
|
|
47
|
+
error:
|
|
48
|
+
'loadState cannot apply "user" in "registry-only" mode — the developer opens the returned URL in their own browser, carrying their own session. Use the default "headless" mode.',
|
|
49
|
+
invalidInput: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let checkpoint;
|
|
54
|
+
try {
|
|
55
|
+
checkpoint = readCheckpoint({ name });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return { error: error.message };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const pageId = checkpoint.checkpoint.pageId;
|
|
61
|
+
if (type.isNone(pageId)) {
|
|
62
|
+
return { error: `Checkpoint "${name}" has no recorded pageId.` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
loadMocks({ pageId, mocks: checkpoint.requests });
|
|
66
|
+
|
|
67
|
+
const basePath = lowdefyConfig.basePath ?? '';
|
|
68
|
+
const urlQuery = checkpoint.urlQuery ?? '';
|
|
69
|
+
|
|
70
|
+
if (mode === 'registry-only') {
|
|
71
|
+
const separator = urlQuery.includes('?') ? '&' : '?';
|
|
72
|
+
const url = `${origin}${basePath}/${pageId}${urlQuery}${separator}_checkpoint=${encodeURIComponent(
|
|
73
|
+
name
|
|
74
|
+
)}`;
|
|
75
|
+
return {
|
|
76
|
+
url,
|
|
77
|
+
instructions:
|
|
78
|
+
'Open this URL in a browser — state is injected on load, requests serve recorded data.',
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (mode !== 'headless') {
|
|
83
|
+
return {
|
|
84
|
+
error: `loadState "mode" must be "headless" or "registry-only". Received ${JSON.stringify(
|
|
85
|
+
mode
|
|
86
|
+
)}.`,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (type.isNone(origin) || !type.isString(origin)) {
|
|
91
|
+
return { error: `loadState requires an "origin" string. Received ${JSON.stringify(origin)}.` };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let browser;
|
|
95
|
+
try {
|
|
96
|
+
browser = await getBrowser();
|
|
97
|
+
} catch (error) {
|
|
98
|
+
return {
|
|
99
|
+
error: `No Chromium available. Run: npx playwright install chromium (${error.message})`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let context;
|
|
104
|
+
try {
|
|
105
|
+
// buildPageUrl (used internally by openPage) joins origin/basePath/pageId
|
|
106
|
+
// as a plain string, so folding the query string into the pageId segment
|
|
107
|
+
// reproduces the checkpoint's exact URL without needing a query-aware
|
|
108
|
+
// variant of openPage.
|
|
109
|
+
const opened = await openPage({ browser, origin, pageId: `${pageId}${urlQuery}`, user });
|
|
110
|
+
context = opened.context;
|
|
111
|
+
const { page } = opened;
|
|
112
|
+
|
|
113
|
+
await page.waitForFunction((id) => Boolean(window.lowdefy?.contexts?.[`page:${id}`]), pageId, {
|
|
114
|
+
timeout: 15000,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const stateEntries = Object.entries(checkpoint.state ?? {});
|
|
118
|
+
await page.evaluate(
|
|
119
|
+
({ id, entries }) => {
|
|
120
|
+
const pageContext = window.lowdefy.contexts[`page:${id}`];
|
|
121
|
+
entries.forEach(([key, value]) => {
|
|
122
|
+
pageContext._internal.State.set(key, value);
|
|
123
|
+
});
|
|
124
|
+
pageContext._internal.update();
|
|
125
|
+
},
|
|
126
|
+
{ id: pageId, entries: stateEntries }
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const verifyKeys = stateEntries.slice(0, VERIFY_KEY_COUNT).map(([key]) => key);
|
|
130
|
+
const verifiedKeys = await page.evaluate(
|
|
131
|
+
({ id, keys }) => {
|
|
132
|
+
const pageContext = window.lowdefy.contexts[`page:${id}`];
|
|
133
|
+
const result = {};
|
|
134
|
+
keys.forEach((key) => {
|
|
135
|
+
result[key] = pageContext.state?.[key];
|
|
136
|
+
});
|
|
137
|
+
return result;
|
|
138
|
+
},
|
|
139
|
+
{ id: pageId, keys: verifyKeys }
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return { loaded: true, mode, url: opened.url, verifiedKeys };
|
|
143
|
+
} catch (error) {
|
|
144
|
+
return { error: `Failed to load checkpoint "${name}": ${error.message}` };
|
|
145
|
+
} finally {
|
|
146
|
+
if (context) {
|
|
147
|
+
await context.close();
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export default loadState;
|
|
@@ -0,0 +1,28 @@
|
|
|
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
|
+
// Mirrors the error mapping in src/routes/jitPage.js so agent-facing page
|
|
18
|
+
// config responses carry the same shape as the dev client's JIT build errors.
|
|
19
|
+
function mapPageBuildErrors(error) {
|
|
20
|
+
const rawErrors = error.buildErrors ?? [error];
|
|
21
|
+
return rawErrors.map((err) => ({
|
|
22
|
+
type: err.name ?? 'Error',
|
|
23
|
+
message: err.message,
|
|
24
|
+
source: err.source ?? null,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default mapPageBuildErrors;
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
const KINDS = {
|
|
18
|
+
action: 'actions',
|
|
19
|
+
actions: 'actions',
|
|
20
|
+
agent: 'agents',
|
|
21
|
+
agents: 'agents',
|
|
22
|
+
block: 'blocks',
|
|
23
|
+
blocks: 'blocks',
|
|
24
|
+
connection: 'connections',
|
|
25
|
+
connections: 'connections',
|
|
26
|
+
notification: 'notifications',
|
|
27
|
+
notifications: 'notifications',
|
|
28
|
+
operator: 'operators',
|
|
29
|
+
operators: 'operators',
|
|
30
|
+
request: 'requests',
|
|
31
|
+
requests: 'requests',
|
|
32
|
+
websocket: 'websockets',
|
|
33
|
+
websockets: 'websockets',
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function normalizeTypeKind({ kind }) {
|
|
37
|
+
return KINDS[String(kind ?? '').toLowerCase()] ?? null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default normalizeTypeKind;
|
|
@@ -14,19 +14,22 @@
|
|
|
14
14
|
limitations under the License.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { serializer } from '@lowdefy/helpers';
|
|
18
20
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
// Read fresh on every call — the dev build rewrites artifacts on config
|
|
22
|
+
// change and the docs routes must serve the current state.
|
|
23
|
+
function readBuildArtifact({ name, deserialize = false }) {
|
|
24
|
+
const filePath = path.join(process.cwd(), 'build', name);
|
|
25
|
+
if (!fs.existsSync(filePath)) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
29
|
+
if (deserialize) {
|
|
30
|
+
return serializer.deserialize(raw);
|
|
24
31
|
}
|
|
25
|
-
|
|
26
|
-
const { blockId, payload, pageId } = req.body;
|
|
27
|
-
context.logger.info({ event: 'call_api_endpoint', blockId, endpointId, pageId });
|
|
28
|
-
const response = await callEndpoint(context, { blockId, endpointId, pageId, payload });
|
|
29
|
-
res.status(200).json(response);
|
|
32
|
+
return raw;
|
|
30
33
|
}
|
|
31
34
|
|
|
32
|
-
export default
|
|
35
|
+
export default readBuildArtifact;
|
|
@@ -0,0 +1,26 @@
|
|
|
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 readBuildArtifact from './readBuildArtifact.js';
|
|
18
|
+
|
|
19
|
+
// Factored out of getPageConfig so the artifact-read side (deserializing
|
|
20
|
+
// build/pages/<pageId>.json) is testable without exercising the JIT build
|
|
21
|
+
// pipeline (buildPageIfNeeded) that getPageConfig also drives.
|
|
22
|
+
function readPageArtifact({ pageId }) {
|
|
23
|
+
return readBuildArtifact({ name: `pages/${pageId}.json`, deserialize: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default readPageArtifact;
|
|
@@ -0,0 +1,49 @@
|
|
|
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 { createRequire } from 'node:module';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
// Resolve from the server directory so installed plugins (including local
|
|
22
|
+
// workspace: packages) resolve exactly as the server imports them.
|
|
23
|
+
const require = createRequire(path.join(process.cwd(), 'package.json'));
|
|
24
|
+
|
|
25
|
+
const cache = new Map();
|
|
26
|
+
|
|
27
|
+
function resolvePluginDir({ packageName }) {
|
|
28
|
+
if (cache.has(packageName)) {
|
|
29
|
+
return cache.get(packageName);
|
|
30
|
+
}
|
|
31
|
+
let dir = null;
|
|
32
|
+
// Installed plugins are direct deps of the server, so node_modules holds a
|
|
33
|
+
// link to each. Plugin exports maps rarely expose ./package.json, so
|
|
34
|
+
// require.resolve on it fails — check the directory first.
|
|
35
|
+
const linked = path.join(process.cwd(), 'node_modules', packageName);
|
|
36
|
+
if (fs.existsSync(path.join(linked, 'package.json'))) {
|
|
37
|
+
dir = fs.realpathSync(linked);
|
|
38
|
+
} else {
|
|
39
|
+
try {
|
|
40
|
+
dir = path.dirname(require.resolve(`${packageName}/package.json`));
|
|
41
|
+
} catch {
|
|
42
|
+
// Package not resolvable — degrade gracefully.
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
cache.set(packageName, dir);
|
|
46
|
+
return dir;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export default resolvePluginDir;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { type } from '@lowdefy/helpers';
|
|
18
|
+
|
|
19
|
+
import tabAvailable from './tabAvailable.js';
|
|
20
|
+
|
|
21
|
+
// The single tab-vs-headless decision, shared by inspectState and evalOperator
|
|
22
|
+
// so a change to the rule lands in both. Prefers a real, already-open browser
|
|
23
|
+
// tab over a fresh headless one — a live tab reflects whatever a developer is
|
|
24
|
+
// actually looking at (their own interactions, in-flight requests), while
|
|
25
|
+
// headless always starts a clean navigation.
|
|
26
|
+
//
|
|
27
|
+
// `user` is headless-only — the developer's tab carries their real session,
|
|
28
|
+
// which the dev server cannot re-identify — so it selects the headless source,
|
|
29
|
+
// and contradicts an explicit `source: 'tab'`.
|
|
30
|
+
//
|
|
31
|
+
// Returns { tryTab }, or { error, invalidInput } for a contradictory call.
|
|
32
|
+
// `invalidInput` marks the caller's own mistake rather than a failed render, so
|
|
33
|
+
// the HTTP routes can answer 400 instead of a 502 that reads as "the renderer
|
|
34
|
+
// broke, retry".
|
|
35
|
+
function resolveSource({ name, pageId, source, user }) {
|
|
36
|
+
const hasUser = !type.isNone(user);
|
|
37
|
+
if (hasUser && source === 'tab') {
|
|
38
|
+
return {
|
|
39
|
+
error: `${name} cannot apply "user" to the developer's live tab — it carries their real session. Omit "source", or use "headless".`,
|
|
40
|
+
invalidInput: true,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
tryTab: source === 'tab' || (source === undefined && !hasUser && tabAvailable({ pageId })),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default resolveSource;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright 2020-2026 Lowdefy, Inc
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this file except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
|
|
10
|
+
Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
See the License for the specific language governing permissions and
|
|
14
|
+
limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { jest } from '@jest/globals';
|
|
18
|
+
|
|
19
|
+
const mockTabAvailable = jest.fn();
|
|
20
|
+
|
|
21
|
+
jest.unstable_mockModule('./tabAvailable.js', () => ({
|
|
22
|
+
default: mockTabAvailable,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
const { default: resolveSource } = await import('./resolveSource.js');
|
|
26
|
+
|
|
27
|
+
beforeEach(() => {
|
|
28
|
+
jest.clearAllMocks();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('resolveSource tries the tab when source is "tab"', () => {
|
|
32
|
+
expect(resolveSource({ name: 'inspectState', pageId: 'home', source: 'tab' })).toEqual({
|
|
33
|
+
tryTab: true,
|
|
34
|
+
});
|
|
35
|
+
expect(mockTabAvailable).not.toHaveBeenCalled();
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('resolveSource does not try the tab when source is "headless"', () => {
|
|
39
|
+
expect(resolveSource({ name: 'inspectState', pageId: 'home', source: 'headless' })).toEqual({
|
|
40
|
+
tryTab: false,
|
|
41
|
+
});
|
|
42
|
+
expect(mockTabAvailable).not.toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('resolveSource tries the tab when source is unset and a tab is available', () => {
|
|
46
|
+
mockTabAvailable.mockReturnValue(true);
|
|
47
|
+
|
|
48
|
+
expect(resolveSource({ name: 'inspectState', pageId: 'home' })).toEqual({ tryTab: true });
|
|
49
|
+
expect(mockTabAvailable).toHaveBeenCalledWith({ pageId: 'home' });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('resolveSource does not try the tab when source is unset and no tab is available', () => {
|
|
53
|
+
mockTabAvailable.mockReturnValue(false);
|
|
54
|
+
|
|
55
|
+
expect(resolveSource({ name: 'inspectState', pageId: 'home' })).toEqual({ tryTab: false });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('resolveSource selects headless over an available tab when a user is given', () => {
|
|
59
|
+
mockTabAvailable.mockReturnValue(true);
|
|
60
|
+
|
|
61
|
+
expect(
|
|
62
|
+
resolveSource({ name: 'inspectState', pageId: 'home', user: { roles: ['admin'] } })
|
|
63
|
+
).toEqual({ tryTab: false });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('resolveSource marks a user combined with source "tab" as invalid input', () => {
|
|
67
|
+
const result = resolveSource({
|
|
68
|
+
name: 'inspectState',
|
|
69
|
+
pageId: 'home',
|
|
70
|
+
source: 'tab',
|
|
71
|
+
user: { roles: ['admin'] },
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
expect(result.invalidInput).toBe(true);
|
|
75
|
+
expect(result.error).toBe(
|
|
76
|
+
'inspectState cannot apply "user" to the developer\'s live tab — it carries their real session. Omit "source", or use "headless".'
|
|
77
|
+
);
|
|
78
|
+
expect(result.tryTab).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test('resolveSource names the calling function in the invalid-input error', () => {
|
|
82
|
+
const result = resolveSource({
|
|
83
|
+
name: 'evalOperator',
|
|
84
|
+
pageId: 'home',
|
|
85
|
+
source: 'tab',
|
|
86
|
+
user: { roles: ['admin'] },
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(result.error).toMatch(/^evalOperator cannot apply "user"/);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('resolveSource treats a null user as no user', () => {
|
|
93
|
+
mockTabAvailable.mockReturnValue(true);
|
|
94
|
+
|
|
95
|
+
expect(
|
|
96
|
+
resolveSource({ name: 'inspectState', pageId: 'home', source: 'tab', user: null })
|
|
97
|
+
).toEqual({
|
|
98
|
+
tryTab: true,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
import { type } from '@lowdefy/helpers';
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
getCheckpointsRoot,
|
|
23
|
+
getConfigDirectory,
|
|
24
|
+
listConfigFiles,
|
|
25
|
+
MANIFEST_FILE_NAME,
|
|
26
|
+
} from './checkpointPaths.js';
|
|
27
|
+
|
|
28
|
+
function copyFromSnapshot({ snapshotDirectory, configDirectory, relativeFilePath }) {
|
|
29
|
+
const source = path.join(snapshotDirectory, relativeFilePath);
|
|
30
|
+
const destination = path.join(configDirectory, relativeFilePath);
|
|
31
|
+
fs.mkdirSync(path.dirname(destination), { recursive: true });
|
|
32
|
+
fs.copyFileSync(source, destination);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Restores the config directory to exactly the state captured by checkpoint
|
|
36
|
+
// `id`: every file the manifest lists is copied back (covering both
|
|
37
|
+
// modified files and files deleted after the checkpoint was taken), and any
|
|
38
|
+
// config-relevant file that exists now but isn't in the manifest is deleted
|
|
39
|
+
// (covering files added after the checkpoint). The manifest — not a diff
|
|
40
|
+
// against the live directory — is the single source of truth for what
|
|
41
|
+
// "restored" means, so reverts are exact and repeatable.
|
|
42
|
+
function revertConfigCheckpoint({ id }) {
|
|
43
|
+
if (type.isNone(id) || !type.isString(id)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`revertConfigCheckpoint requires an "id" string. Received ${JSON.stringify(id)}.`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const checkpointsRoot = getCheckpointsRoot();
|
|
50
|
+
const snapshotDirectory = path.join(checkpointsRoot, id);
|
|
51
|
+
const manifestPath = path.join(snapshotDirectory, MANIFEST_FILE_NAME);
|
|
52
|
+
if (!fs.existsSync(manifestPath)) {
|
|
53
|
+
throw new Error(`Config checkpoint "${id}" not found.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
57
|
+
const configDirectory = getConfigDirectory();
|
|
58
|
+
|
|
59
|
+
manifest.files.forEach((relativeFilePath) => {
|
|
60
|
+
copyFromSnapshot({ snapshotDirectory, configDirectory, relativeFilePath });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const manifestFiles = new Set(manifest.files);
|
|
64
|
+
const currentFiles = listConfigFiles({ configDirectory });
|
|
65
|
+
const deleted = currentFiles.filter((relativeFilePath) => !manifestFiles.has(relativeFilePath));
|
|
66
|
+
deleted.forEach((relativeFilePath) => {
|
|
67
|
+
fs.rmSync(path.join(configDirectory, relativeFilePath));
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return { restored: manifest.files, deleted };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export default revertConfigCheckpoint;
|