@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,88 +0,0 @@
1
- /*
2
- Copyright 2020-2026 Lowdefy, Inc
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- import fs from 'node:fs';
18
- import path from 'node:path';
19
-
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;
@@ -1,91 +0,0 @@
1
- /*
2
- Copyright 2020-2026 Lowdefy, Inc
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- import fs from '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
- };
@@ -1,234 +0,0 @@
1
- /*
2
- Copyright 2020-2026 Lowdefy, Inc
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- import fs from 'fs';
18
- import path from 'path';
19
- import { serializer, type } from '@lowdefy/helpers';
20
-
21
- import { getConfigDirectory } from './checkpointPaths.js';
22
-
23
- // State checkpoints (page state + recorded request/api responses) live under
24
- // <configDir>/.lowdefy/state-checkpoints/<name>/ — inside .lowdefy because a
25
- // checkpoint captures the signed-in user's data and live backend responses,
26
- // which must not sit in a committable spot by default (.lowdefy is gitignored
27
- // by convention and ignored by the dev file watcher). A sibling of
28
- // .lowdefy/checkpoints (the config-directory-snapshot checkpoints in
29
- // checkpointPaths.js/createConfigCheckpoint.js) rather than a subdirectory,
30
- // because that store prunes its oldest directories beyond a cap. These two
31
- // "checkpoint" concepts are unrelated: this one is for putting the *running
32
- // app* into a specific state (agent verification, human testing, e2e test
33
- // generation), not for reverting config files.
34
- const NAME_PATTERN = /^[a-z0-9-_]+$/i;
35
-
36
- function getStateCheckpointsRoot() {
37
- return path.join(getConfigDirectory(), '.lowdefy', 'state-checkpoints');
38
- }
39
-
40
- function validateName(name) {
41
- if (type.isNone(name) || !type.isString(name)) {
42
- throw new Error(`Checkpoint "name" must be a string. Received ${JSON.stringify(name)}.`);
43
- }
44
- if (!NAME_PATTERN.test(name)) {
45
- throw new Error(
46
- `Checkpoint name "${name}" is invalid — use only letters, numbers, "-" and "_".`
47
- );
48
- }
49
- }
50
-
51
- function writeJsonFile(filePath, value) {
52
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
53
- fs.writeFileSync(filePath, JSON.stringify(serializer.serialize(value), null, 2));
54
- }
55
-
56
- function readJsonFile(filePath, defaultValue) {
57
- if (!fs.existsSync(filePath)) {
58
- return defaultValue;
59
- }
60
- return serializer.deserialize(JSON.parse(fs.readFileSync(filePath, 'utf8')));
61
- }
62
-
63
- // Requests/api responses arrive as either an array of entries (newest first —
64
- // the shape of context.requests in both Requests.js and Inspector.jsx's
65
- // buildSnapshot) or a single entry object. Either way only the newest
66
- // recording is kept per id.
67
- function newestEntry(value) {
68
- return Array.isArray(value) ? value[0] : value;
69
- }
70
-
71
- function writeEntries({ dir, subdir, entries, parts }) {
72
- Object.entries(entries ?? {}).forEach(([id, value]) => {
73
- const entry = newestEntry(value);
74
- if (type.isNone(entry)) {
75
- return;
76
- }
77
- const { payload, response, error, responseTime } = entry;
78
- writeJsonFile(path.join(dir, subdir, `${id}.json`), { payload, response, error, responseTime });
79
- parts.push(`${subdir}/${id}.json`);
80
- });
81
- }
82
-
83
- function readEntries({ dir, subdir }) {
84
- const entriesDir = path.join(dir, subdir);
85
- const entries = {};
86
- if (!fs.existsSync(entriesDir)) {
87
- return entries;
88
- }
89
- fs.readdirSync(entriesDir)
90
- .filter((fileName) => fileName.endsWith('.json'))
91
- .forEach((fileName) => {
92
- const id = fileName.slice(0, -'.json'.length);
93
- entries[id] = readJsonFile(path.join(entriesDir, fileName), null);
94
- });
95
- return entries;
96
- }
97
-
98
- // Snapshots a running page's state + recorded request/api responses into a
99
- // checkpoint folder. Refuses to clobber an existing checkpoint unless
100
- // `overwrite: true` — when overwriting, the old folder is removed first so a
101
- // checkpoint written with fewer requests than a previous one doesn't leave
102
- // stale per-request files behind.
103
- function writeCheckpoint({ name, snapshot, notes, overwrite }) {
104
- validateName(name);
105
- if (type.isNone(snapshot) || !type.isObject(snapshot)) {
106
- throw new Error(
107
- `writeCheckpoint requires a "snapshot" object. Received ${JSON.stringify(snapshot)}.`
108
- );
109
- }
110
-
111
- const dir = path.join(getStateCheckpointsRoot(), name);
112
- const exists = fs.existsSync(dir);
113
- if (exists && overwrite !== true) {
114
- throw new Error(`Checkpoint "${name}" already exists. Pass overwrite: true to replace it.`);
115
- }
116
- if (exists) {
117
- fs.rmSync(dir, { recursive: true, force: true });
118
- }
119
- fs.mkdirSync(dir, { recursive: true });
120
-
121
- const parts = [];
122
- const manifest = {
123
- name,
124
- pageId: snapshot.pageId ?? null,
125
- url: snapshot.url ?? null,
126
- capturedAt: new Date().toISOString(),
127
- notes: notes ?? null,
128
- };
129
- writeJsonFile(path.join(dir, 'checkpoint.json'), manifest);
130
- parts.push('checkpoint.json');
131
-
132
- writeJsonFile(path.join(dir, 'state.json'), snapshot.state ?? {});
133
- parts.push('state.json');
134
- writeJsonFile(path.join(dir, 'urlQuery.json'), snapshot.urlQuery ?? '');
135
- parts.push('urlQuery.json');
136
- writeJsonFile(path.join(dir, 'inputs.json'), snapshot.input ?? {});
137
- parts.push('inputs.json');
138
- writeJsonFile(path.join(dir, 'user.json'), snapshot.user ?? null);
139
- parts.push('user.json');
140
- writeJsonFile(path.join(dir, 'global.json'), snapshot.global ?? {});
141
- parts.push('global.json');
142
-
143
- writeEntries({ dir, subdir: 'requests', entries: snapshot.requests, parts });
144
- if (!type.isNone(snapshot.apiResponses)) {
145
- writeEntries({ dir, subdir: 'api', entries: snapshot.apiResponses, parts });
146
- }
147
-
148
- return { dir, parts };
149
- }
150
-
151
- // Reads a checkpoint back, tolerant of individually missing parts (a
152
- // developer may hand-edit or delete files in a committed checkpoint folder) —
153
- // only a missing checkpoint directory itself is an error.
154
- function readCheckpoint({ name }) {
155
- validateName(name);
156
- const dir = path.join(getStateCheckpointsRoot(), name);
157
- if (!fs.existsSync(dir)) {
158
- throw new Error(`Checkpoint "${name}" not found.`);
159
- }
160
-
161
- const checkpoint = readJsonFile(path.join(dir, 'checkpoint.json'), {
162
- name,
163
- pageId: null,
164
- url: null,
165
- capturedAt: null,
166
- notes: null,
167
- });
168
-
169
- return {
170
- name,
171
- checkpoint,
172
- state: readJsonFile(path.join(dir, 'state.json'), {}),
173
- urlQuery: readJsonFile(path.join(dir, 'urlQuery.json'), ''),
174
- input: readJsonFile(path.join(dir, 'inputs.json'), {}),
175
- user: readJsonFile(path.join(dir, 'user.json'), null),
176
- global: readJsonFile(path.join(dir, 'global.json'), {}),
177
- requests: readEntries({ dir, subdir: 'requests' }),
178
- api: readEntries({ dir, subdir: 'api' }),
179
- };
180
- }
181
-
182
- function checkpointExists({ name }) {
183
- validateName(name);
184
- return fs.existsSync(path.join(getStateCheckpointsRoot(), name));
185
- }
186
-
187
- // Lists stored state checkpoints, most recent first. Corrupted/hand-edited
188
- // checkpoints without a manifest still show up (with null fields) rather
189
- // than being silently hidden, since these folders are meant to be
190
- // human-editable.
191
- function listStateCheckpoints() {
192
- const root = getStateCheckpointsRoot();
193
- if (!fs.existsSync(root)) {
194
- return [];
195
- }
196
-
197
- const checkpoints = fs
198
- .readdirSync(root, { withFileTypes: true })
199
- .filter((entry) => entry.isDirectory())
200
- .map((entry) => {
201
- const dir = path.join(root, entry.name);
202
- const manifest = readJsonFile(path.join(dir, 'checkpoint.json'), {
203
- name: entry.name,
204
- pageId: null,
205
- capturedAt: null,
206
- notes: null,
207
- });
208
- const requestsDir = path.join(dir, 'requests');
209
- const requestCount = fs.existsSync(requestsDir)
210
- ? fs.readdirSync(requestsDir).filter((fileName) => fileName.endsWith('.json')).length
211
- : 0;
212
- return {
213
- name: entry.name,
214
- pageId: manifest.pageId ?? null,
215
- capturedAt: manifest.capturedAt ?? null,
216
- notes: manifest.notes ?? null,
217
- requestCount,
218
- };
219
- });
220
-
221
- return checkpoints.sort((a, b) => {
222
- if (type.isNone(a.capturedAt)) return 1;
223
- if (type.isNone(b.capturedAt)) return -1;
224
- return a.capturedAt < b.capturedAt ? 1 : -1;
225
- });
226
- }
227
-
228
- export {
229
- checkpointExists,
230
- getStateCheckpointsRoot,
231
- listStateCheckpoints,
232
- readCheckpoint,
233
- writeCheckpoint,
234
- };
@@ -1,65 +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 YAML from 'yaml';
18
-
19
- import { readCheckpoint } from './checkpointStore.js';
20
-
21
- // Errors can't round-trip through YAML as Error instances (and don't need
22
- // to) — @lowdefy/e2e-utils' createMockManager accepts a plain string and
23
- // wraps it itself (`error instanceof Error ? error : new Error(error)`), so
24
- // only the message is kept here.
25
- function toMockError(error) {
26
- if (!error) {
27
- return undefined;
28
- }
29
- return error.message ?? String(error);
30
- }
31
-
32
- function toMockEntries(entries, idKey, extraFields = {}) {
33
- return Object.entries(entries ?? {}).map(([id, entry]) => {
34
- const error = toMockError(entry?.error);
35
- const base = { [idKey]: id, ...extraFields };
36
- if (error) {
37
- return { ...base, error };
38
- }
39
- return { ...base, response: entry?.response ?? null };
40
- });
41
- }
42
-
43
- // Converts a state checkpoint's recorded requests/api responses into the
44
- // exact shape @lowdefy/e2e-utils' loadStaticMocks/createMockManager expect
45
- // (`{ requests: [...], api: [...] }`), so a checkpoint recorded from a live
46
- // dev session can seed a future AI-generated (or hand-written) e2e test's
47
- // mocks file directly.
48
- function checkpointToMocks({ name }) {
49
- let checkpoint;
50
- try {
51
- checkpoint = readCheckpoint({ name });
52
- } catch (error) {
53
- return { error: error.message };
54
- }
55
-
56
- const pageId = checkpoint.checkpoint.pageId;
57
- const requests = toMockEntries(checkpoint.requests, 'requestId', { pageId });
58
- const api = toMockEntries(checkpoint.api, 'endpointId');
59
-
60
- const yamlContent = YAML.stringify({ requests, api });
61
-
62
- return { requests, api, yaml: yamlContent };
63
- }
64
-
65
- export default checkpointToMocks;
@@ -1,156 +0,0 @@
1
- /*
2
- Copyright 2020-2026 Lowdefy, Inc
3
-
4
- Licensed under the Apache License, Version 2.0 (the "License");
5
- you may not use this file except in compliance with the License.
6
- You may obtain a copy of the License at
7
-
8
- http://www.apache.org/licenses/LICENSE-2.0
9
-
10
- Unless required by applicable law or agreed to in writing, software
11
- distributed under the License is distributed on an "AS IS" BASIS,
12
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
- See the License for the specific language governing permissions and
14
- limitations under the License.
15
- */
16
-
17
- import fs from 'fs';
18
- import os from 'os';
19
- import path from 'path';
20
-
21
- import { MAX_CHECKPOINTS } from './checkpointPaths.js';
22
- import createConfigCheckpoint from './createConfigCheckpoint.js';
23
- import listConfigCheckpoints from './listConfigCheckpoints.js';
24
- import revertConfigCheckpoint from './revertConfigCheckpoint.js';
25
-
26
- let configDirectory;
27
- let previousConfigDirectory;
28
- let previousCwd;
29
-
30
- function writeFile(relativePath, content) {
31
- const filePath = path.join(configDirectory, relativePath);
32
- fs.mkdirSync(path.dirname(filePath), { recursive: true });
33
- fs.writeFileSync(filePath, content);
34
- }
35
-
36
- function readFile(relativePath) {
37
- return fs.readFileSync(path.join(configDirectory, relativePath), 'utf8');
38
- }
39
-
40
- function exists(relativePath) {
41
- return fs.existsSync(path.join(configDirectory, relativePath));
42
- }
43
-
44
- // Checkpoint ids are timestamp-prefixed (see createConfigCheckpoint.js), so
45
- // tests asserting ordering/eviction need genuinely distinct timestamps
46
- // between calls. A real (short) delay is used instead of mocking the clock —
47
- // this Node/Jest combination's fake timers cannot redefine `performance`.
48
- function sleep(ms) {
49
- return new Promise((resolve) => setTimeout(resolve, ms));
50
- }
51
-
52
- beforeEach(() => {
53
- configDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'lowdefy-config-checkpoints-test-'));
54
- previousConfigDirectory = process.env.LOWDEFY_DIRECTORY_CONFIG;
55
- process.env.LOWDEFY_DIRECTORY_CONFIG = configDirectory;
56
- previousCwd = process.cwd();
57
- process.chdir(configDirectory);
58
-
59
- writeFile('lowdefy.yaml', 'name: my-app\n');
60
- writeFile(path.join('pages', 'home.yaml'), 'id: home\n');
61
- writeFile('.env', 'SECRET=abc\n');
62
- writeFile('README.md', '# My App\n');
63
- // Should never be snapshotted.
64
- writeFile(path.join('node_modules', 'some-dep', 'index.js'), 'module.exports = {};\n');
65
- writeFile(path.join('.git', 'HEAD'), 'ref: refs/heads/main\n');
66
- });
67
-
68
- afterEach(() => {
69
- process.chdir(previousCwd);
70
- fs.rmSync(configDirectory, { recursive: true, force: true });
71
- if (previousConfigDirectory === undefined) {
72
- delete process.env.LOWDEFY_DIRECTORY_CONFIG;
73
- } else {
74
- process.env.LOWDEFY_DIRECTORY_CONFIG = previousConfigDirectory;
75
- }
76
- });
77
-
78
- test('createConfigCheckpoint snapshots config-relevant files and excludes build/vcs/dependency directories', () => {
79
- const result = createConfigCheckpoint({ label: 'before change' });
80
-
81
- expect(result.fileCount).toBe(4);
82
- expect(result.id).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-before-change$/);
83
-
84
- const snapshotDirectory = path.join(configDirectory, '.lowdefy', 'checkpoints', result.id);
85
- const manifest = JSON.parse(
86
- fs.readFileSync(path.join(snapshotDirectory, 'manifest.json'), 'utf8')
87
- );
88
- expect(manifest.files.sort()).toEqual(
89
- ['lowdefy.yaml', path.join('pages', 'home.yaml'), '.env', 'README.md'].sort()
90
- );
91
-
92
- expect(fs.readFileSync(path.join(snapshotDirectory, 'lowdefy.yaml'), 'utf8')).toBe(
93
- 'name: my-app\n'
94
- );
95
- expect(fs.existsSync(path.join(snapshotDirectory, 'node_modules'))).toBe(false);
96
- expect(fs.existsSync(path.join(snapshotDirectory, '.git'))).toBe(false);
97
- });
98
-
99
- test('listConfigCheckpoints returns an empty array when no checkpoints exist', () => {
100
- expect(listConfigCheckpoints()).toEqual([]);
101
- });
102
-
103
- test('listConfigCheckpoints lists checkpoints most recent first', async () => {
104
- const first = createConfigCheckpoint({ label: 'first' });
105
- await sleep(5);
106
- const second = createConfigCheckpoint({ label: 'second' });
107
-
108
- const checkpoints = listConfigCheckpoints();
109
- expect(checkpoints).toEqual([
110
- { id: second.id, label: 'second', createdAt: expect.any(String), fileCount: 4 },
111
- { id: first.id, label: 'first', createdAt: expect.any(String), fileCount: 4 },
112
- ]);
113
- });
114
-
115
- test('revertConfigCheckpoint restores modified and deleted files, and deletes files added since the checkpoint', () => {
116
- const { id } = createConfigCheckpoint({ label: 'snapshot' });
117
-
118
- // Modify an existing file.
119
- writeFile('lowdefy.yaml', 'name: modified-app\n');
120
- // Delete a file that was in the checkpoint.
121
- fs.rmSync(path.join(configDirectory, 'pages', 'home.yaml'));
122
- // Add a new config-relevant file that was not in the checkpoint.
123
- writeFile(path.join('pages', 'extra.yaml'), 'id: extra\n');
124
-
125
- const result = revertConfigCheckpoint({ id });
126
-
127
- expect(readFile('lowdefy.yaml')).toBe('name: my-app\n');
128
- expect(exists(path.join('pages', 'home.yaml'))).toBe(true);
129
- expect(readFile(path.join('pages', 'home.yaml'))).toBe('id: home\n');
130
- expect(exists(path.join('pages', 'extra.yaml'))).toBe(false);
131
-
132
- expect(result.restored.sort()).toEqual(
133
- ['lowdefy.yaml', path.join('pages', 'home.yaml'), '.env', 'README.md'].sort()
134
- );
135
- expect(result.deleted).toEqual([path.join('pages', 'extra.yaml')]);
136
- });
137
-
138
- test('revertConfigCheckpoint throws an actionable error when the checkpoint id does not exist', () => {
139
- expect(() => revertConfigCheckpoint({ id: 'does-not-exist' })).toThrow(
140
- 'Config checkpoint "does-not-exist" not found.'
141
- );
142
- });
143
-
144
- test('createConfigCheckpoint evicts the oldest checkpoint once more than the cap are stored', async () => {
145
- for (let i = 0; i < MAX_CHECKPOINTS + 1; i += 1) {
146
- createConfigCheckpoint({ label: `checkpoint-${i}` });
147
- await sleep(2);
148
- }
149
-
150
- const checkpoints = listConfigCheckpoints();
151
- expect(checkpoints).toHaveLength(MAX_CHECKPOINTS);
152
- expect(checkpoints.some((checkpoint) => checkpoint.label === 'checkpoint-0')).toBe(false);
153
- expect(
154
- checkpoints.some((checkpoint) => checkpoint.label === `checkpoint-${MAX_CHECKPOINTS}`)
155
- ).toBe(true);
156
- });