@zuplo/editor 1.0.31094607111 → 1.0.31661404480

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 (141) hide show
  1. package/README.md +25 -0
  2. package/dist/adapters/memory-fs.d.ts +0 -1
  3. package/dist/adapters/memory-preferences.d.ts +0 -1
  4. package/dist/adapters/memory.d.ts +0 -1
  5. package/dist/adapters/memory.js +194 -10
  6. package/dist/adapters/memory.js.map +7 -1
  7. package/dist/app.d.ts +29 -10
  8. package/dist/app.js +10 -116
  9. package/dist/app.js.map +7 -1
  10. package/dist/chunks/chunk-54KOYG5C.js +10 -0
  11. package/dist/chunks/chunk-54KOYG5C.js.map +7 -0
  12. package/dist/chunks/chunk-C7VL3DX7.js +47 -0
  13. package/dist/chunks/chunk-C7VL3DX7.js.map +7 -0
  14. package/dist/chunks/chunk-FELEIEAB.js +40 -0
  15. package/dist/chunks/chunk-FELEIEAB.js.map +7 -0
  16. package/dist/chunks/chunk-HDZMPWV2.js +11390 -0
  17. package/dist/chunks/chunk-HDZMPWV2.js.map +7 -0
  18. package/dist/client/index.html +3 -3
  19. package/dist/client/static/css/main-D4ruAwAg.css +1 -0
  20. package/dist/client/static/js/GraphiQLConsole-C3mUtrG9.js +71 -0
  21. package/dist/client/static/js/{McpCapabilityFilterDesigner-CFYXS7Y4.js → McpCapabilityFilterDesigner-COGUyP07.js} +1 -1
  22. package/dist/client/static/js/{SchemaReference-DnRuA77Y.js → SchemaReference-BpeLq7I3.js} +1 -1
  23. package/dist/client/static/js/{dist-JDzf7X4h.js → dist-DP_G_tkV.js} +13 -13
  24. package/dist/client/static/js/{esm-CSg4wlYg.js → esm-BfamZtsm.js} +1 -1
  25. package/dist/client/static/js/{hint-8S237UzV.js → hint--tq9iIeq.js} +1 -1
  26. package/dist/client/static/js/{hint-BPU1gkpx.js → hint-BMbDJNc4.js} +1 -1
  27. package/dist/client/static/js/{info-Dekw9zL3.js → info-CdXcsT3t.js} +1 -1
  28. package/dist/client/static/js/{jump-wNrWwMpB.js → jump-D1TPL3ME.js} +1 -1
  29. package/dist/client/static/js/{lint-DjDCd_sO.js → lint-Cdq0ksKl.js} +1 -1
  30. package/dist/client/static/js/main-BfGOC9Wr.js +1585 -0
  31. package/dist/client/static/js/{mode-B0minaVe.js → mode-BNaswVl-.js} +1 -1
  32. package/dist/client/static/js/{mode-DdjZuoTn.js → mode-DshT-YZo.js} +1 -1
  33. package/dist/client/static/js/{mode--1945Gkn.js → mode-Dt0n7fTF.js} +1 -1
  34. package/dist/config.d.ts +0 -1
  35. package/dist/fixtures/preview-project.d.ts +0 -1
  36. package/dist/fixtures/preview-project.js +19 -20
  37. package/dist/fixtures/preview-project.js.map +7 -1
  38. package/dist/index.d.ts +1 -2
  39. package/dist/index.js +3426 -5
  40. package/dist/index.js.map +7 -1
  41. package/dist/interfaces.d.ts +0 -1
  42. package/dist/interfaces.portal.d.ts +0 -1
  43. package/dist/ports.d.ts +0 -1
  44. package/dist/ports.js +12 -71
  45. package/dist/ports.js.map +7 -1
  46. package/dist/server.d.ts +6 -3
  47. package/package.json +1 -27
  48. package/dist/adapters/memory-fs.d.ts.map +0 -1
  49. package/dist/adapters/memory-fs.js +0 -204
  50. package/dist/adapters/memory-fs.js.map +0 -1
  51. package/dist/adapters/memory-fs.spec.d.ts +0 -2
  52. package/dist/adapters/memory-fs.spec.d.ts.map +0 -1
  53. package/dist/adapters/memory-fs.spec.js +0 -74
  54. package/dist/adapters/memory-fs.spec.js.map +0 -1
  55. package/dist/adapters/memory-preferences.d.ts.map +0 -1
  56. package/dist/adapters/memory-preferences.js +0 -18
  57. package/dist/adapters/memory-preferences.js.map +0 -1
  58. package/dist/adapters/memory.d.ts.map +0 -1
  59. package/dist/adapters/node-fs.d.ts +0 -11
  60. package/dist/adapters/node-fs.d.ts.map +0 -1
  61. package/dist/adapters/node-fs.js +0 -133
  62. package/dist/adapters/node-fs.js.map +0 -1
  63. package/dist/adapters/node-preferences.d.ts +0 -10
  64. package/dist/adapters/node-preferences.d.ts.map +0 -1
  65. package/dist/adapters/node-preferences.js +0 -38
  66. package/dist/adapters/node-preferences.js.map +0 -1
  67. package/dist/app.d.ts.map +0 -1
  68. package/dist/client/static/css/main-DSfZObJX.css +0 -1
  69. package/dist/client/static/js/GraphiQLConsole-CDlqCZZz.js +0 -71
  70. package/dist/client/static/js/main-BdGwM1To.js +0 -1585
  71. package/dist/config.d.ts.map +0 -1
  72. package/dist/config.js +0 -111
  73. package/dist/config.js.map +0 -1
  74. package/dist/constants.d.ts +0 -46
  75. package/dist/constants.d.ts.map +0 -1
  76. package/dist/constants.js +0 -46
  77. package/dist/constants.js.map +0 -1
  78. package/dist/error-handler.d.ts +0 -14
  79. package/dist/error-handler.d.ts.map +0 -1
  80. package/dist/error-handler.js +0 -108
  81. package/dist/error-handler.js.map +0 -1
  82. package/dist/fixtures/preview-project.d.ts.map +0 -1
  83. package/dist/index.d.ts.map +0 -1
  84. package/dist/interfaces.d.ts.map +0 -1
  85. package/dist/interfaces.js +0 -2
  86. package/dist/interfaces.js.map +0 -1
  87. package/dist/interfaces.portal.d.ts.map +0 -1
  88. package/dist/interfaces.portal.js +0 -14
  89. package/dist/interfaces.portal.js.map +0 -1
  90. package/dist/ports.d.ts.map +0 -1
  91. package/dist/routes/api.d.ts +0 -16
  92. package/dist/routes/api.d.ts.map +0 -1
  93. package/dist/routes/api.js +0 -116
  94. package/dist/routes/api.js.map +0 -1
  95. package/dist/routes/api.spec.d.ts +0 -2
  96. package/dist/routes/api.spec.d.ts.map +0 -1
  97. package/dist/routes/api.spec.js +0 -220
  98. package/dist/routes/api.spec.js.map +0 -1
  99. package/dist/routes/editor.d.ts +0 -17
  100. package/dist/routes/editor.d.ts.map +0 -1
  101. package/dist/routes/editor.js +0 -131
  102. package/dist/routes/editor.js.map +0 -1
  103. package/dist/routes/editor.spec.d.ts +0 -2
  104. package/dist/routes/editor.spec.d.ts.map +0 -1
  105. package/dist/routes/editor.spec.js +0 -207
  106. package/dist/routes/editor.spec.js.map +0 -1
  107. package/dist/routes/fs-race-condition.spec.d.ts +0 -2
  108. package/dist/routes/fs-race-condition.spec.d.ts.map +0 -1
  109. package/dist/routes/fs-race-condition.spec.js +0 -235
  110. package/dist/routes/fs-race-condition.spec.js.map +0 -1
  111. package/dist/routes/fs.d.ts +0 -10
  112. package/dist/routes/fs.d.ts.map +0 -1
  113. package/dist/routes/fs.js +0 -325
  114. package/dist/routes/fs.js.map +0 -1
  115. package/dist/routes/fs.spec.d.ts +0 -2
  116. package/dist/routes/fs.spec.d.ts.map +0 -1
  117. package/dist/routes/fs.spec.js +0 -613
  118. package/dist/routes/fs.spec.js.map +0 -1
  119. package/dist/routes/test.d.ts +0 -8
  120. package/dist/routes/test.d.ts.map +0 -1
  121. package/dist/routes/test.js +0 -130
  122. package/dist/routes/test.js.map +0 -1
  123. package/dist/routes/test.spec.d.ts +0 -2
  124. package/dist/routes/test.spec.d.ts.map +0 -1
  125. package/dist/routes/test.spec.js +0 -250
  126. package/dist/routes/test.spec.js.map +0 -1
  127. package/dist/server.d.ts.map +0 -1
  128. package/dist/server.js +0 -147
  129. package/dist/server.js.map +0 -1
  130. package/dist/test-utils/setup.d.ts +0 -53
  131. package/dist/test-utils/setup.d.ts.map +0 -1
  132. package/dist/test-utils/setup.js +0 -82
  133. package/dist/test-utils/setup.js.map +0 -1
  134. package/dist/utils/path.d.ts +0 -20
  135. package/dist/utils/path.d.ts.map +0 -1
  136. package/dist/utils/path.js +0 -24
  137. package/dist/utils/path.js.map +0 -1
  138. package/dist/validation.d.ts +0 -31
  139. package/dist/validation.d.ts.map +0 -1
  140. package/dist/validation.js +0 -42
  141. package/dist/validation.js.map +0 -1
package/README.md CHANGED
@@ -2,3 +2,28 @@
2
2
 
3
3
  This project is not meant to be used independently. It is part of the
4
4
  [Zuplo CLI](https://zuplo.com/docs/cli)
5
+
6
+ ## Publishing
7
+
8
+ The published package is **`publish/`**, not this directory.
9
+
10
+ ```bash
11
+ pnpm --filter @zuplo/editor build:package
12
+ npm publish ./packages/editor/publish
13
+ ```
14
+
15
+ This directory is a workspace member, so publishing it directly would ship the
16
+ spec files and a `dependencies` list containing everything the editor imports.
17
+ `build:package` assembles the real artifact instead: every runtime dependency is
18
+ bundled and tree-shaken, only the declarations reachable from the entry points
19
+ are copied, and the `package.json` is generated from scratch. **The published
20
+ package has no dependencies** — see
21
+ [`scripts/build-package.ts`](./scripts/build-package.ts).
22
+
23
+ That last part is why `EditorApp.fetch` is a plain function and
24
+ `AssetSource.serve` takes a `Request`, rather than the `Hono` and hono `Context`
25
+ they really are: a published declaration can only name a type a consumer can
26
+ resolve, and hono is bundled. The build fails if a reachable declaration names
27
+ anything that isn't installable, so this can't regress quietly.
28
+
29
+ `npm publish` from this directory fails on purpose.
@@ -41,4 +41,3 @@ export interface MemoryFileSystem extends FileSystemPort, ChangeNotifier {
41
41
  * no chokidar to drive it.
42
42
  */
43
43
  export declare function createMemoryFileSystem(options?: MemoryFileSystemOptions): MemoryFileSystem;
44
- //# sourceMappingURL=memory-fs.d.ts.map
@@ -7,4 +7,3 @@ import type { EditorPreferencesPort } from "../ports.js";
7
7
  * simply forgotten when the isolate recycles.
8
8
  */
9
9
  export declare function createMemoryEditorPreferences(): EditorPreferencesPort;
10
- //# sourceMappingURL=memory-preferences.d.ts.map
@@ -7,4 +7,3 @@
7
7
  */
8
8
  export { createMemoryFileSystem, type MemoryFileSystem, type MemoryFileSystemOptions, } from "./memory-fs.js";
9
9
  export { createMemoryEditorPreferences } from "./memory-preferences.js";
10
- //# sourceMappingURL=memory.d.ts.map
@@ -1,10 +1,194 @@
1
- /**
2
- * The platform-free adapters, collected so a consumer that must avoid `node:*`
3
- * has a single import that is guaranteed not to reach it.
4
- *
5
- * The preview Worker imports from here rather than from the package root, which
6
- * pulls in `LocalEditorServer` and with it `@hono/node-server` and chokidar.
7
- */
8
- export { createMemoryFileSystem, } from "./memory-fs.js";
9
- export { createMemoryEditorPreferences } from "./memory-preferences.js";
10
- //# sourceMappingURL=memory.js.map
1
+ import {
2
+ editorConfig
3
+ } from "../chunks/chunk-C7VL3DX7.js";
4
+ import {
5
+ PathNotFoundError,
6
+ toSafeSegments
7
+ } from "../chunks/chunk-FELEIEAB.js";
8
+ import "../chunks/chunk-54KOYG5C.js";
9
+
10
+ // src/adapters/memory-fs.ts
11
+ var decoder = new TextDecoder();
12
+ function createMemoryFileSystem(options = {}) {
13
+ const {
14
+ files: seed = {},
15
+ watchedDirs = editorConfig.watch.watchedDirs,
16
+ now = () => /* @__PURE__ */ new Date(),
17
+ // One timestamp for the whole seed rather than one per file, so `reset()`
18
+ // restores the project exactly as it was first loaded.
19
+ seedMtime = now()
20
+ } = options;
21
+ const files = /* @__PURE__ */ new Map();
22
+ const emptyDirs = /* @__PURE__ */ new Map();
23
+ const listeners = /* @__PURE__ */ new Set();
24
+ const key = (path) => toSafeSegments(path).join("/");
25
+ const parentOf = (path) => path.slice(0, Math.max(0, path.lastIndexOf("/")));
26
+ const isWatched = (path) => watchedDirs.some((dir) => path === dir || path.startsWith(`${dir}/`));
27
+ const emit = (path) => {
28
+ if (!isWatched(path)) {
29
+ return;
30
+ }
31
+ for (const listener of listeners) {
32
+ listener(path);
33
+ }
34
+ };
35
+ const hasChildren = (path) => {
36
+ const prefix = `${path}/`;
37
+ for (const filePath of files.keys()) {
38
+ if (filePath.startsWith(prefix)) {
39
+ return true;
40
+ }
41
+ }
42
+ for (const dirPath of emptyDirs.keys()) {
43
+ if (dirPath.startsWith(prefix)) {
44
+ return true;
45
+ }
46
+ }
47
+ return false;
48
+ };
49
+ const isDirectory = (path) => (
50
+ // The root always exists, even in an empty project.
51
+ path === "" || emptyDirs.has(path) || hasChildren(path)
52
+ );
53
+ const addDirs = (path, timestamp) => {
54
+ for (let dir = path; dir !== ""; dir = parentOf(dir)) {
55
+ if (!files.has(dir)) {
56
+ emptyDirs.set(dir, timestamp);
57
+ }
58
+ }
59
+ };
60
+ const write = (path, content, at) => {
61
+ const mtime = at ?? now();
62
+ emptyDirs.delete(path);
63
+ addDirs(parentOf(path), mtime);
64
+ files.set(path, { content, mtime });
65
+ return { type: "file", mtime };
66
+ };
67
+ const load = () => {
68
+ files.clear();
69
+ emptyDirs.clear();
70
+ for (const [path, content] of Object.entries(seed)) {
71
+ write(key(path), content, seedMtime);
72
+ }
73
+ };
74
+ load();
75
+ return {
76
+ onChange(listener) {
77
+ listeners.add(listener);
78
+ return () => listeners.delete(listener);
79
+ },
80
+ reset() {
81
+ load();
82
+ },
83
+ // Every method below is `async` even though nothing here awaits: it makes a
84
+ // rejected promise the only way these fail, matching the Node adapter. A
85
+ // sync throw from `key()` on a traversal attempt would otherwise escape
86
+ // before the promise existed, and callers would have to guard both ways.
87
+ async readFileWithStat(path) {
88
+ const file = files.get(key(path));
89
+ if (!file) {
90
+ throw new PathNotFoundError(path);
91
+ }
92
+ return {
93
+ content: typeof file.content === "string" ? file.content : decoder.decode(file.content),
94
+ mtime: file.mtime
95
+ };
96
+ },
97
+ async listDir(path) {
98
+ const dir = key(path);
99
+ if (!isDirectory(dir)) {
100
+ throw new PathNotFoundError(path);
101
+ }
102
+ const prefix = dir === "" ? "" : `${dir}/`;
103
+ const entries = /* @__PURE__ */ new Map();
104
+ const collect = (fullPath, type) => {
105
+ if (!fullPath.startsWith(prefix)) {
106
+ return;
107
+ }
108
+ const rest = fullPath.slice(prefix.length);
109
+ if (rest === "") {
110
+ return;
111
+ }
112
+ const slash = rest.indexOf("/");
113
+ const name = slash === -1 ? rest : rest.slice(0, slash);
114
+ entries.set(name, {
115
+ name,
116
+ type: slash === -1 ? type : "directory"
117
+ });
118
+ };
119
+ for (const filePath of files.keys()) {
120
+ collect(filePath, "file");
121
+ }
122
+ for (const dirPath of emptyDirs.keys()) {
123
+ collect(dirPath, "directory");
124
+ }
125
+ return [...entries.values()];
126
+ },
127
+ async statPath(path) {
128
+ const target = key(path);
129
+ const file = files.get(target);
130
+ if (file) {
131
+ return { type: "file", mtime: file.mtime };
132
+ }
133
+ if (isDirectory(target)) {
134
+ return {
135
+ type: "directory",
136
+ mtime: emptyDirs.get(target) ?? now()
137
+ };
138
+ }
139
+ return void 0;
140
+ },
141
+ async writeFile(path, content) {
142
+ const target = key(path);
143
+ const stat = write(target, content);
144
+ emit(target);
145
+ return stat;
146
+ },
147
+ async remove(path) {
148
+ const target = key(path);
149
+ if (!files.delete(target)) {
150
+ throw new PathNotFoundError(path);
151
+ }
152
+ addDirs(parentOf(target), now());
153
+ emit(target);
154
+ },
155
+ async rename(from, to) {
156
+ const fromKey = key(from);
157
+ const toKey = key(to);
158
+ const file = files.get(fromKey);
159
+ if (!file) {
160
+ throw new PathNotFoundError(from);
161
+ }
162
+ files.delete(fromKey);
163
+ addDirs(parentOf(fromKey), now());
164
+ emptyDirs.delete(toKey);
165
+ addDirs(parentOf(toKey), file.mtime);
166
+ files.set(toKey, file);
167
+ emit(fromKey);
168
+ emit(toKey);
169
+ },
170
+ async mkdirp(path) {
171
+ const target = key(path);
172
+ if (target !== "") {
173
+ addDirs(target, now());
174
+ }
175
+ }
176
+ };
177
+ }
178
+
179
+ // src/adapters/memory-preferences.ts
180
+ function createMemoryEditorPreferences() {
181
+ let editor = null;
182
+ return {
183
+ get: () => Promise.resolve(editor),
184
+ set: (value) => {
185
+ editor = value;
186
+ return Promise.resolve();
187
+ }
188
+ };
189
+ }
190
+ export {
191
+ createMemoryEditorPreferences,
192
+ createMemoryFileSystem
193
+ };
194
+ //# sourceMappingURL=memory.js.map
@@ -1 +1,7 @@
1
- {"version":3,"file":"memory.js","sourceRoot":"","sources":["../../src/adapters/memory.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EACL,sBAAsB,GAGvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,EAAE,6BAA6B,EAAE,MAAM,yBAAyB,CAAC"}
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/adapters/memory-fs.ts", "../../../src/adapters/memory-preferences.ts"],
4
+ "sourcesContent": ["import { editorConfig } from \"../constants.js\";\nimport {\n type ChangeNotifier,\n type DirEntry,\n type FileSystemPort,\n type FileWithStat,\n PathNotFoundError,\n type PathStat,\n toSafeSegments,\n} from \"../ports.js\";\n\ninterface MemoryFile {\n content: string | Uint8Array;\n mtime: Date;\n}\n\nexport interface MemoryFileSystemOptions {\n /**\n * The project to start from, as project-relative path \u2192 file content. Parent\n * directories are implied and don't need listing.\n */\n files?: Record<string, string>;\n\n /**\n * Top-level directories whose changes are reported to `onChange` listeners.\n * Defaults to the same set chokidar watches on Node, so the preview's\n * live-reload behaviour matches the CLI's rather than being broader.\n */\n watchedDirs?: readonly string[];\n\n /** Injectable clock, so tests can control mtimes. */\n now?: () => Date;\n\n /**\n * The mtime the seeded project starts with. Defaults to now.\n *\n * Worth setting to a constant wherever the same seed is loaded more than\n * once \u2014 a Worker builds one of these per isolate, and if each picked its own\n * startup time, a client that read a file from one isolate would be told its\n * save conflicts when it landed on another. The file it read and the file it\n * is saving over are byte-identical; only the clock disagreed.\n */\n seedMtime?: Date;\n}\n\nexport interface MemoryFileSystem extends FileSystemPort, ChangeNotifier {\n /** Discards all edits and returns to the seeded project. */\n reset(): void;\n}\n\nconst decoder = new TextDecoder();\n\n/**\n * A `FileSystemPort` held entirely in memory \u2014 what the per-PR preview Worker\n * runs against, since a Worker has no filesystem.\n *\n * Directories are implied by the paths of the files inside them, plus any\n * created explicitly, which is enough for a project tree and avoids maintaining\n * a parallel directory structure that could disagree with the files.\n *\n * It doubles as the `ChangeNotifier`: writes report themselves, so the SPA's\n * live-reload path stays exercised in the preview instead of going quiet with\n * no chokidar to drive it.\n */\nexport function createMemoryFileSystem(\n options: MemoryFileSystemOptions = {},\n): MemoryFileSystem {\n const {\n files: seed = {},\n watchedDirs = editorConfig.watch.watchedDirs,\n now = () => new Date(),\n // One timestamp for the whole seed rather than one per file, so `reset()`\n // restores the project exactly as it was first loaded.\n seedMtime = now(),\n } = options;\n\n const files = new Map<string, MemoryFile>();\n /** Directories that exist without (yet) containing a file. */\n const emptyDirs = new Map<string, Date>();\n const listeners = new Set<(path: string) => void>();\n\n /** Canonicalises a path and refuses traversal, exactly as Node's adapter does. */\n const key = (path: string): string => toSafeSegments(path).join(\"/\");\n\n const parentOf = (path: string): string =>\n path.slice(0, Math.max(0, path.lastIndexOf(\"/\")));\n\n const isWatched = (path: string): boolean =>\n watchedDirs.some((dir) => path === dir || path.startsWith(`${dir}/`));\n\n /**\n * Announces a change the way chokidar does on Node \u2014 including for the\n * editor's own saves, which also trip the real watcher.\n */\n const emit = (path: string): void => {\n if (!isWatched(path)) {\n return;\n }\n for (const listener of listeners) {\n listener(path);\n }\n };\n\n /** True when anything lives under `path`, which is what makes it a directory. */\n const hasChildren = (path: string): boolean => {\n const prefix = `${path}/`;\n for (const filePath of files.keys()) {\n if (filePath.startsWith(prefix)) {\n return true;\n }\n }\n for (const dirPath of emptyDirs.keys()) {\n if (dirPath.startsWith(prefix)) {\n return true;\n }\n }\n return false;\n };\n\n const isDirectory = (path: string): boolean =>\n // The root always exists, even in an empty project.\n path === \"\" || emptyDirs.has(path) || hasChildren(path);\n\n /**\n * Records `path` and its ancestors as directories.\n *\n * They're only remembered while empty: once a file sits inside, the file's\n * own path is what makes the directory exist, and keeping a second record\n * would let the two disagree.\n */\n const addDirs = (path: string, timestamp: Date): void => {\n for (let dir = path; dir !== \"\"; dir = parentOf(dir)) {\n if (!files.has(dir)) {\n emptyDirs.set(dir, timestamp);\n }\n }\n };\n\n const write = (\n path: string,\n content: string | Uint8Array,\n at?: Date,\n ): PathStat => {\n const mtime = at ?? now();\n // A file and a directory can't share a path, and the file wins \u2014 the same\n // way writing over a directory on disk fails rather than merging.\n emptyDirs.delete(path);\n addDirs(parentOf(path), mtime);\n files.set(path, { content, mtime });\n return { type: \"file\", mtime };\n };\n\n const load = (): void => {\n files.clear();\n emptyDirs.clear();\n for (const [path, content] of Object.entries(seed)) {\n write(key(path), content, seedMtime);\n }\n };\n\n load();\n\n return {\n onChange(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n\n reset() {\n load();\n },\n\n // Every method below is `async` even though nothing here awaits: it makes a\n // rejected promise the only way these fail, matching the Node adapter. A\n // sync throw from `key()` on a traversal attempt would otherwise escape\n // before the promise existed, and callers would have to guard both ways.\n async readFileWithStat(path: string): Promise<FileWithStat> {\n const file = files.get(key(path));\n if (!file) {\n throw new PathNotFoundError(path);\n }\n\n // Content and mtime come from one immutable record, so they always\n // describe the same version \u2014 the property the Node adapter needs an open\n // file handle to get.\n return {\n content:\n typeof file.content === \"string\"\n ? file.content\n : decoder.decode(file.content),\n mtime: file.mtime,\n };\n },\n\n async listDir(path: string): Promise<DirEntry[]> {\n const dir = key(path);\n if (!isDirectory(dir)) {\n throw new PathNotFoundError(path);\n }\n\n const prefix = dir === \"\" ? \"\" : `${dir}/`;\n const entries = new Map<string, DirEntry>();\n\n const collect = (fullPath: string, type: DirEntry[\"type\"]): void => {\n if (!fullPath.startsWith(prefix)) {\n return;\n }\n const rest = fullPath.slice(prefix.length);\n if (rest === \"\") {\n return;\n }\n const slash = rest.indexOf(\"/\");\n // A path continuing past the first segment describes a grandchild, so\n // the segment itself is a directory rather than the entry.\n const name = slash === -1 ? rest : rest.slice(0, slash);\n entries.set(name, {\n name,\n type: slash === -1 ? type : \"directory\",\n });\n };\n\n for (const filePath of files.keys()) {\n collect(filePath, \"file\");\n }\n for (const dirPath of emptyDirs.keys()) {\n collect(dirPath, \"directory\");\n }\n\n return [...entries.values()];\n },\n\n async statPath(path: string): Promise<PathStat | undefined> {\n const target = key(path);\n\n const file = files.get(target);\n if (file) {\n return { type: \"file\", mtime: file.mtime };\n }\n\n if (isDirectory(target)) {\n return {\n type: \"directory\",\n mtime: emptyDirs.get(target) ?? now(),\n };\n }\n\n return undefined;\n },\n\n async writeFile(\n path: string,\n content: string | Uint8Array,\n ): Promise<PathStat> {\n const target = key(path);\n const stat = write(target, content);\n emit(target);\n return stat;\n },\n\n async remove(path: string): Promise<void> {\n const target = key(path);\n if (!files.delete(target)) {\n throw new PathNotFoundError(path);\n }\n // The parent may have just become empty, and an empty directory still\n // exists on disk after its last file is deleted.\n addDirs(parentOf(target), now());\n emit(target);\n },\n\n async rename(from: string, to: string): Promise<void> {\n const fromKey = key(from);\n const toKey = key(to);\n\n const file = files.get(fromKey);\n if (!file) {\n throw new PathNotFoundError(from);\n }\n\n files.delete(fromKey);\n addDirs(parentOf(fromKey), now());\n // Carries its mtime across, as a rename on disk does \u2014 the content didn't\n // change, so a client holding a `Last-Modified` for it stays valid.\n emptyDirs.delete(toKey);\n addDirs(parentOf(toKey), file.mtime);\n files.set(toKey, file);\n\n emit(fromKey);\n emit(toKey);\n },\n\n async mkdirp(path: string): Promise<void> {\n const target = key(path);\n if (target !== \"\") {\n addDirs(target, now());\n }\n },\n };\n}\n", "import type { EditorPreferencesPort } from \"../ports.js\";\n\n/**\n * Keeps the user's external-editor choice in memory for the life of the process.\n *\n * Used by the preview Worker, which has no home directory to persist to. The\n * preference dialog and toolbar behave correctly within a session; the choice is\n * simply forgotten when the isolate recycles.\n */\nexport function createMemoryEditorPreferences(): EditorPreferencesPort {\n let editor: string | null = null;\n\n return {\n get: () => Promise.resolve(editor),\n set: (value: string) => {\n editor = value;\n return Promise.resolve();\n },\n };\n}\n"],
5
+ "mappings": ";;;;;;;;;;AAkDA,IAAM,UAAU,IAAI,YAAY;AAczB,SAAS,uBACd,UAAmC,CAAC,GAClB;AAClB,QAAM;AAAA,IACJ,OAAO,OAAO,CAAC;AAAA,IACf,cAAc,aAAa,MAAM;AAAA,IACjC,MAAM,MAAM,oBAAI,KAAK;AAAA;AAAA;AAAA,IAGrB,YAAY,IAAI;AAAA,EAClB,IAAI;AAEJ,QAAM,QAAQ,oBAAI,IAAwB;AAE1C,QAAM,YAAY,oBAAI,IAAkB;AACxC,QAAM,YAAY,oBAAI,IAA4B;AAGlD,QAAM,MAAM,CAAC,SAAyB,eAAe,IAAI,EAAE,KAAK,GAAG;AAEnE,QAAM,WAAW,CAAC,SAChB,KAAK,MAAM,GAAG,KAAK,IAAI,GAAG,KAAK,YAAY,GAAG,CAAC,CAAC;AAElD,QAAM,YAAY,CAAC,SACjB,YAAY,KAAK,CAAC,QAAQ,SAAS,OAAO,KAAK,WAAW,GAAG,GAAG,GAAG,CAAC;AAMtE,QAAM,OAAO,CAAC,SAAuB;AACnC,QAAI,CAAC,UAAU,IAAI,GAAG;AACpB;AAAA,IACF;AACA,eAAW,YAAY,WAAW;AAChC,eAAS,IAAI;AAAA,IACf;AAAA,EACF;AAGA,QAAM,cAAc,CAAC,SAA0B;AAC7C,UAAM,SAAS,GAAG,IAAI;AACtB,eAAW,YAAY,MAAM,KAAK,GAAG;AACnC,UAAI,SAAS,WAAW,MAAM,GAAG;AAC/B,eAAO;AAAA,MACT;AAAA,IACF;AACA,eAAW,WAAW,UAAU,KAAK,GAAG;AACtC,UAAI,QAAQ,WAAW,MAAM,GAAG;AAC9B,eAAO;AAAA,MACT;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,QAAM,cAAc,CAAC;AAAA;AAAA,IAEnB,SAAS,MAAM,UAAU,IAAI,IAAI,KAAK,YAAY,IAAI;AAAA;AASxD,QAAM,UAAU,CAAC,MAAc,cAA0B;AACvD,aAAS,MAAM,MAAM,QAAQ,IAAI,MAAM,SAAS,GAAG,GAAG;AACpD,UAAI,CAAC,MAAM,IAAI,GAAG,GAAG;AACnB,kBAAU,IAAI,KAAK,SAAS;AAAA,MAC9B;AAAA,IACF;AAAA,EACF;AAEA,QAAM,QAAQ,CACZ,MACA,SACA,OACa;AACb,UAAM,QAAQ,MAAM,IAAI;AAGxB,cAAU,OAAO,IAAI;AACrB,YAAQ,SAAS,IAAI,GAAG,KAAK;AAC7B,UAAM,IAAI,MAAM,EAAE,SAAS,MAAM,CAAC;AAClC,WAAO,EAAE,MAAM,QAAQ,MAAM;AAAA,EAC/B;AAEA,QAAM,OAAO,MAAY;AACvB,UAAM,MAAM;AACZ,cAAU,MAAM;AAChB,eAAW,CAAC,MAAM,OAAO,KAAK,OAAO,QAAQ,IAAI,GAAG;AAClD,YAAM,IAAI,IAAI,GAAG,SAAS,SAAS;AAAA,IACrC;AAAA,EACF;AAEA,OAAK;AAEL,SAAO;AAAA,IACL,SAAS,UAAU;AACjB,gBAAU,IAAI,QAAQ;AACtB,aAAO,MAAM,UAAU,OAAO,QAAQ;AAAA,IACxC;AAAA,IAEA,QAAQ;AACN,WAAK;AAAA,IACP;AAAA;AAAA;AAAA;AAAA;AAAA,IAMA,MAAM,iBAAiB,MAAqC;AAC1D,YAAM,OAAO,MAAM,IAAI,IAAI,IAAI,CAAC;AAChC,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,kBAAkB,IAAI;AAAA,MAClC;AAKA,aAAO;AAAA,QACL,SACE,OAAO,KAAK,YAAY,WACpB,KAAK,UACL,QAAQ,OAAO,KAAK,OAAO;AAAA,QACjC,OAAO,KAAK;AAAA,MACd;AAAA,IACF;AAAA,IAEA,MAAM,QAAQ,MAAmC;AAC/C,YAAM,MAAM,IAAI,IAAI;AACpB,UAAI,CAAC,YAAY,GAAG,GAAG;AACrB,cAAM,IAAI,kBAAkB,IAAI;AAAA,MAClC;AAEA,YAAM,SAAS,QAAQ,KAAK,KAAK,GAAG,GAAG;AACvC,YAAM,UAAU,oBAAI,IAAsB;AAE1C,YAAM,UAAU,CAAC,UAAkB,SAAiC;AAClE,YAAI,CAAC,SAAS,WAAW,MAAM,GAAG;AAChC;AAAA,QACF;AACA,cAAM,OAAO,SAAS,MAAM,OAAO,MAAM;AACzC,YAAI,SAAS,IAAI;AACf;AAAA,QACF;AACA,cAAM,QAAQ,KAAK,QAAQ,GAAG;AAG9B,cAAM,OAAO,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;AACtD,gBAAQ,IAAI,MAAM;AAAA,UAChB;AAAA,UACA,MAAM,UAAU,KAAK,OAAO;AAAA,QAC9B,CAAC;AAAA,MACH;AAEA,iBAAW,YAAY,MAAM,KAAK,GAAG;AACnC,gBAAQ,UAAU,MAAM;AAAA,MAC1B;AACA,iBAAW,WAAW,UAAU,KAAK,GAAG;AACtC,gBAAQ,SAAS,WAAW;AAAA,MAC9B;AAEA,aAAO,CAAC,GAAG,QAAQ,OAAO,CAAC;AAAA,IAC7B;AAAA,IAEA,MAAM,SAAS,MAA6C;AAC1D,YAAM,SAAS,IAAI,IAAI;AAEvB,YAAM,OAAO,MAAM,IAAI,MAAM;AAC7B,UAAI,MAAM;AACR,eAAO,EAAE,MAAM,QAAQ,OAAO,KAAK,MAAM;AAAA,MAC3C;AAEA,UAAI,YAAY,MAAM,GAAG;AACvB,eAAO;AAAA,UACL,MAAM;AAAA,UACN,OAAO,UAAU,IAAI,MAAM,KAAK,IAAI;AAAA,QACtC;AAAA,MACF;AAEA,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,UACJ,MACA,SACmB;AACnB,YAAM,SAAS,IAAI,IAAI;AACvB,YAAM,OAAO,MAAM,QAAQ,OAAO;AAClC,WAAK,MAAM;AACX,aAAO;AAAA,IACT;AAAA,IAEA,MAAM,OAAO,MAA6B;AACxC,YAAM,SAAS,IAAI,IAAI;AACvB,UAAI,CAAC,MAAM,OAAO,MAAM,GAAG;AACzB,cAAM,IAAI,kBAAkB,IAAI;AAAA,MAClC;AAGA,cAAQ,SAAS,MAAM,GAAG,IAAI,CAAC;AAC/B,WAAK,MAAM;AAAA,IACb;AAAA,IAEA,MAAM,OAAO,MAAc,IAA2B;AACpD,YAAM,UAAU,IAAI,IAAI;AACxB,YAAM,QAAQ,IAAI,EAAE;AAEpB,YAAM,OAAO,MAAM,IAAI,OAAO;AAC9B,UAAI,CAAC,MAAM;AACT,cAAM,IAAI,kBAAkB,IAAI;AAAA,MAClC;AAEA,YAAM,OAAO,OAAO;AACpB,cAAQ,SAAS,OAAO,GAAG,IAAI,CAAC;AAGhC,gBAAU,OAAO,KAAK;AACtB,cAAQ,SAAS,KAAK,GAAG,KAAK,KAAK;AACnC,YAAM,IAAI,OAAO,IAAI;AAErB,WAAK,OAAO;AACZ,WAAK,KAAK;AAAA,IACZ;AAAA,IAEA,MAAM,OAAO,MAA6B;AACxC,YAAM,SAAS,IAAI,IAAI;AACvB,UAAI,WAAW,IAAI;AACjB,gBAAQ,QAAQ,IAAI,CAAC;AAAA,MACvB;AAAA,IACF;AAAA,EACF;AACF;;;ACjSO,SAAS,gCAAuD;AACrE,MAAI,SAAwB;AAE5B,SAAO;AAAA,IACL,KAAK,MAAM,QAAQ,QAAQ,MAAM;AAAA,IACjC,KAAK,CAAC,UAAkB;AACtB,eAAS;AACT,aAAO,QAAQ,QAAQ;AAAA,IACzB;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
package/dist/app.d.ts CHANGED
@@ -1,14 +1,16 @@
1
- import { type Context, Hono } from "hono";
2
1
  import { type LocalEditorLabels } from "./config.js";
3
2
  import type { CommonLogger } from "./interfaces.js";
4
3
  import type { ChangeNotifier, EditorPreferencesPort, FileSystemPort } from "./ports.js";
5
4
  /**
6
5
  * The built SPA, however the platform happens to hold it.
7
6
  *
8
- * `serve` is handed the live Hono context rather than a path, because both
9
- * implementations are thin wrappers over something that already expects one
10
- * `serveStatic` on Node, Cloudflare's assets binding in the Worker and
11
- * synthesising a context for them is what breaks.
7
+ * `serve` is handed the request rather than the Hono context this app is holding
8
+ * when it calls: hono is bundled into the published package, so nothing that
9
+ * installs it can resolve a type naming `Context`. Which costs nothing here,
10
+ * because a request is already what the Worker's implementation wants —
11
+ * Cloudflare's assets binding takes one — and the Node implementation runs
12
+ * `serveStatic` in a Hono app of its own to get a real context rather than a
13
+ * synthesised one.
12
14
  */
13
15
  export interface AssetSource {
14
16
  /**
@@ -18,8 +20,26 @@ export interface AssetSource {
18
20
  */
19
21
  readDocument(): Promise<string | undefined>;
20
22
  /** The built asset the request is for, or `undefined` if there isn't one. */
21
- serve(context: Context): Promise<Response | undefined>;
23
+ serve(request: Request): Promise<Response | undefined>;
22
24
  }
25
+ /**
26
+ * A request handler, in the shape every listener the editor runs behind already
27
+ * wants: `serve` from `@hono/node-server` takes exactly this, and so does a
28
+ * Worker's `fetch` export.
29
+ *
30
+ * Stated structurally instead of as the `Hono` the app really is, for the same
31
+ * reason `AssetSource.serve` takes a request — hono is bundled, so it can't
32
+ * appear in the published types.
33
+ *
34
+ * The trailing arguments are the Worker's `env` and `ExecutionContext`, which
35
+ * Hono passes through to route handlers; nothing in this package reads them.
36
+ * They're `any[]` rather than `unknown[]` because parameters are checked
37
+ * contravariantly: the type has to accept Hono's own `fetch`, whose third
38
+ * parameter is a Cloudflare `ExecutionContext` and so cannot be handed an
39
+ * `unknown`. Here `any` is how you say "this type doesn't constrain these",
40
+ * which is the opposite of what `unknown` says in this position.
41
+ */
42
+ export type EditorFetch = (request: Request, ...rest: any[]) => Response | Promise<Response>;
23
43
  export interface EditorAppOptions {
24
44
  /** The project the editor reads and writes. */
25
45
  fs: FileSystemPort;
@@ -48,10 +68,10 @@ export interface EditorAppOptions {
48
68
  }
49
69
  export interface EditorApp {
50
70
  /**
51
- * The Hono app. Hand its `fetch` to whatever is listening — `@hono/node-server`
52
- * on Node, the Worker's default export on Cloudflare.
71
+ * Hand this to whatever is listening — `@hono/node-server` on Node, the
72
+ * Worker's default export on Cloudflare.
53
73
  */
54
- app: Hono;
74
+ fetch: EditorFetch;
55
75
  /** Releases SSE clients and stops listening for changes. */
56
76
  dispose: () => Promise<void>;
57
77
  }
@@ -64,4 +84,3 @@ export interface EditorApp {
64
84
  * routes rather than approximations of each other.
65
85
  */
66
86
  export declare function createEditorApp(options: EditorAppOptions): EditorApp;
67
- //# sourceMappingURL=app.d.ts.map
package/dist/app.js CHANGED
@@ -1,116 +1,10 @@
1
- import { ApiError, HttpStatusCode } from "@zuplo/errors";
2
- import { Hono } from "hono";
3
- import { bodyLimit } from "hono/body-limit";
4
- import { cors } from "hono/cors";
5
- import { getLocalEditorConfig } from "./config.js";
6
- import { editorConfig } from "./constants.js";
7
- import { createErrorHandler, createNotFoundHandler } from "./error-handler.js";
8
- import { createApiRoutes } from "./routes/api.js";
9
- const staticFileExtensions = [".css", ".js", ".json", ".svg", ".png", ".ico"];
10
- /**
11
- * Builds the editor's HTTP app from injected platform capabilities.
12
- *
13
- * This is the whole server minus the platform: no listener, no filesystem, no
14
- * file watcher, no logger implementation. `LocalEditorServer` wraps it for Node
15
- * and the preview Worker wraps it for Cloudflare, so both run these exact
16
- * routes rather than approximations of each other.
17
- */
18
- export function createEditorApp(options) {
19
- const { fs, preferences, assets, changes, logger, deploymentUrl, sourceDirectory, labels, } = options;
20
- const app = new Hono();
21
- const sseClients = new Set();
22
- app.onError(createErrorHandler(logger));
23
- app.notFound(createNotFoundHandler());
24
- // Middleware only wraps routes registered after it, so this has to come
25
- // before the routes below.
26
- app.use(bodyLimit({
27
- maxSize: editorConfig.server.bodyLimit,
28
- onError: () => {
29
- throw new ApiError({
30
- type: "https://zup.fail/http-status/413",
31
- title: "Payload Too Large",
32
- status: HttpStatusCode.PAYLOAD_TOO_LARGE,
33
- detail: `Request body exceeds the maximum size of ${editorConfig.server.bodyLimit} bytes`,
34
- });
35
- },
36
- }));
37
- app.use(cors({
38
- origin: (origin) => origin ?? "*",
39
- allowMethods: [
40
- "GET",
41
- "POST",
42
- "PATCH",
43
- "PUT",
44
- "DELETE",
45
- "OPTIONS",
46
- "HEAD",
47
- ],
48
- allowHeaders: ["*"],
49
- exposeHeaders: ["*"],
50
- }));
51
- app.route("/api", createApiRoutes({
52
- fs,
53
- editorPreferences: preferences,
54
- sourceDirectory,
55
- logger,
56
- sseClients,
57
- deploymentUrl,
58
- }));
59
- if (assets) {
60
- app.on(["GET", "HEAD"], "*", async (c) => {
61
- const isDocumentRequest = c.req.header("accept")?.includes("text/html") &&
62
- !staticFileExtensions.some((ext) => c.req.path.endsWith(ext));
63
- if (!isDocumentRequest) {
64
- return ((await assets.serve(c)) ??
65
- // The asset source hands misses back rather than 404ing itself, so
66
- // they get the same problem+json every other miss does.
67
- c.notFound());
68
- }
69
- // Every navigation gets index.html — the SPA owns routing.
70
- const template = await assets.readDocument();
71
- if (!template) {
72
- // No client build yet, which is normal in development: Vite is in front
73
- // and this handler is never reached there.
74
- return c.notFound();
75
- }
76
- const localEditorConfig = await getLocalEditorConfig({
77
- deploymentUrl,
78
- fs,
79
- logger,
80
- labels,
81
- });
82
- // Inject localEditorConfig into the HTML
83
- const html = template.replace(`"{{localEditorConfig}}"`, // Note the quotes are correct
84
- // A replacer function, so `$&` and friends in the JSON aren't treated
85
- // as substitution patterns.
86
- () => JSON.stringify(localEditorConfig));
87
- return c.html(html);
88
- });
89
- }
90
- const stopWatching = changes?.onChange((filePath) => {
91
- // Broadcast to all SSE clients
92
- const data = JSON.stringify({ type: "change", filePath });
93
- for (const client of sseClients) {
94
- client.send("change", data).catch((err) => {
95
- logger.warn({ err }, "Error sending SSE change event");
96
- });
97
- }
98
- logger.info(`File changed: ${filePath}`);
99
- });
100
- const dispose = async () => {
101
- stopWatching?.();
102
- for (const client of sseClients) {
103
- try {
104
- if (!client.closed) {
105
- await client.close();
106
- }
107
- }
108
- catch (err) {
109
- logger.warn({ err }, "Error closing SSE client");
110
- }
111
- }
112
- sseClients.clear();
113
- };
114
- return { app, dispose };
115
- }
116
- //# sourceMappingURL=app.js.map
1
+ import {
2
+ createEditorApp
3
+ } from "./chunks/chunk-HDZMPWV2.js";
4
+ import "./chunks/chunk-C7VL3DX7.js";
5
+ import "./chunks/chunk-FELEIEAB.js";
6
+ import "./chunks/chunk-54KOYG5C.js";
7
+ export {
8
+ createEditorApp
9
+ };
10
+ //# sourceMappingURL=app.js.map
package/dist/app.js.map CHANGED
@@ -1 +1,7 @@
1
- {"version":3,"file":"app.js","sourceRoot":"","sources":["../src/app.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAgB,IAAI,EAAE,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,oBAAoB,EAA0B,MAAM,aAAa,CAAC;AAC3E,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAO/E,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAmElD,MAAM,oBAAoB,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAE9E;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,OAAyB;IACvD,MAAM,EACJ,EAAE,EACF,WAAW,EACX,MAAM,EACN,OAAO,EACP,MAAM,EACN,aAAa,EACb,eAAe,EACf,MAAM,GACP,GAAG,OAAO,CAAC;IAEZ,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,UAAU,GAAG,IAAI,GAAG,EAAa,CAAC;IAExC,GAAG,CAAC,OAAO,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC;IACxC,GAAG,CAAC,QAAQ,CAAC,qBAAqB,EAAE,CAAC,CAAC;IAEtC,wEAAwE;IACxE,2BAA2B;IAC3B,GAAG,CAAC,GAAG,CACL,SAAS,CAAC;QACR,OAAO,EAAE,YAAY,CAAC,MAAM,CAAC,SAAS;QACtC,OAAO,EAAE,GAAG,EAAE;YACZ,MAAM,IAAI,QAAQ,CAAC;gBACjB,IAAI,EAAE,kCAAkC;gBACxC,KAAK,EAAE,mBAAmB;gBAC1B,MAAM,EAAE,cAAc,CAAC,iBAAiB;gBACxC,MAAM,EAAE,4CAA4C,YAAY,CAAC,MAAM,CAAC,SAAS,QAAQ;aAC1F,CAAC,CAAC;QACL,CAAC;KACF,CAAC,CACH,CAAC;IACF,GAAG,CAAC,GAAG,CACL,IAAI,CAAC;QACH,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,IAAI,GAAG;QACjC,YAAY,EAAE;YACZ,KAAK;YACL,MAAM;YACN,OAAO;YACP,KAAK;YACL,QAAQ;YACR,SAAS;YACT,MAAM;SACP;QACD,YAAY,EAAE,CAAC,GAAG,CAAC;QACnB,aAAa,EAAE,CAAC,GAAG,CAAC;KACrB,CAAC,CACH,CAAC;IAEF,GAAG,CAAC,KAAK,CACP,MAAM,EACN,eAAe,CAAC;QACd,EAAE;QACF,iBAAiB,EAAE,WAAW;QAC9B,eAAe;QACf,MAAM;QACN,UAAU;QACV,aAAa;KACd,CAAC,CACH,CAAC;IAEF,IAAI,MAAM,EAAE,CAAC;QACX,GAAG,CAAC,EAAE,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;YACvC,MAAM,iBAAiB,GACrB,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,WAAW,CAAC;gBAC7C,CAAC,oBAAoB,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC;YAEhE,IAAI,CAAC,iBAAiB,EAAE,CAAC;gBACvB,OAAO,CACL,CAAC,MAAM,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;oBACvB,mEAAmE;oBACnE,wDAAwD;oBACxD,CAAC,CAAC,QAAQ,EAAE,CACb,CAAC;YACJ,CAAC;YAED,2DAA2D;YAC3D,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,YAAY,EAAE,CAAC;YAC7C,IAAI,CAAC,QAAQ,EAAE,CAAC;gBACd,wEAAwE;gBACxE,2CAA2C;gBAC3C,OAAO,CAAC,CAAC,QAAQ,EAAE,CAAC;YACtB,CAAC;YAED,MAAM,iBAAiB,GAAG,MAAM,oBAAoB,CAAC;gBACnD,aAAa;gBACb,EAAE;gBACF,MAAM;gBACN,MAAM;aACP,CAAC,CAAC;YAEH,yCAAyC;YACzC,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAC3B,yBAAyB,EAAE,8BAA8B;YACzD,sEAAsE;YACtE,4BAA4B;YAC5B,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,iBAAiB,CAAC,CACxC,CAAC;YAEF,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACtB,CAAC,CAAC,CAAC;IACL,CAAC;IAED,MAAM,YAAY,GAAG,OAAO,EAAE,QAAQ,CAAC,CAAC,QAAQ,EAAE,EAAE;QAClD,+BAA+B;QAC/B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,QAAQ,EAAE,CAAC,CAAC;QAC1D,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;YAChC,MAAM,CAAC,IAAI,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;gBACxC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,gCAAgC,CAAC,CAAC;YACzD,CAAC,CAAC,CAAC;QACL,CAAC;QAED,MAAM,CAAC,IAAI,CAAC,iBAAiB,QAAQ,EAAE,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH,MAAM,OAAO,GAAG,KAAK,IAAmB,EAAE;QACxC,YAAY,EAAE,EAAE,CAAC;QAEjB,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;YAChC,IAAI,CAAC;gBACH,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;oBACnB,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC;gBACvB,CAAC;YACH,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,EAAE,0BAA0B,CAAC,CAAC;YACnD,CAAC;QACH,CAAC;QACD,UAAU,CAAC,KAAK,EAAE,CAAC;IACrB,CAAC,CAAC;IAEF,OAAO,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC;AAC1B,CAAC"}
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,10 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __export = (target, all) => {
3
+ for (var name in all)
4
+ __defProp(target, name, { get: all[name], enumerable: true });
5
+ };
6
+
7
+ export {
8
+ __export
9
+ };
10
+ //# sourceMappingURL=chunk-54KOYG5C.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": [],
4
+ "sourcesContent": [],
5
+ "mappings": "",
6
+ "names": []
7
+ }
@@ -0,0 +1,47 @@
1
+ // src/constants.ts
2
+ var editorConfig = {
3
+ /**
4
+ * Server configuration
5
+ */
6
+ server: {
7
+ /** Default port for the editor server */
8
+ port: 9100,
9
+ /** Default port for the local Zuplo gateway */
10
+ gatewayPort: 9e3,
11
+ /** Maximum request body size (100MB) */
12
+ bodyLimit: 100 * 1024 * 1024,
13
+ /** Default host for the server */
14
+ host: "localhost"
15
+ },
16
+ /**
17
+ * Server-Sent Events configuration
18
+ */
19
+ sse: {
20
+ /**
21
+ * Keep-alive interval in milliseconds (30 seconds)
22
+ * Prevents proxy/browser timeouts for long-lived connections
23
+ */
24
+ keepAliveInterval: 3e4
25
+ },
26
+ /**
27
+ * File operation configuration
28
+ */
29
+ file: {
30
+ /** Maximum file size for uploads (100MB) */
31
+ maxFileSize: 100 * 1024 * 1024,
32
+ /** Allowed directories for file operations */
33
+ allowedDirs: ["config", "modules", "schemas"]
34
+ },
35
+ /**
36
+ * File watching configuration
37
+ */
38
+ watch: {
39
+ /** Directories to watch for changes */
40
+ watchedDirs: ["config", "modules", "schemas"]
41
+ }
42
+ };
43
+
44
+ export {
45
+ editorConfig
46
+ };
47
+ //# sourceMappingURL=chunk-C7VL3DX7.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/constants.ts"],
4
+ "sourcesContent": ["/**\n * Configuration constants for the local editor server.\n * Centralizes all magic numbers and configuration values.\n */\nexport const editorConfig = {\n /**\n * Server configuration\n */\n server: {\n /** Default port for the editor server */\n port: 9100,\n /** Default port for the local Zuplo gateway */\n gatewayPort: 9000,\n /** Maximum request body size (100MB) */\n bodyLimit: 100 * 1024 * 1024,\n /** Default host for the server */\n host: \"localhost\",\n },\n\n /**\n * Server-Sent Events configuration\n */\n sse: {\n /**\n * Keep-alive interval in milliseconds (30 seconds)\n * Prevents proxy/browser timeouts for long-lived connections\n */\n keepAliveInterval: 30000,\n },\n\n /**\n * File operation configuration\n */\n file: {\n /** Maximum file size for uploads (100MB) */\n maxFileSize: 100 * 1024 * 1024,\n\n /** Allowed directories for file operations */\n allowedDirs: [\"config\", \"modules\", \"schemas\"] as const,\n },\n\n /**\n * File watching configuration\n */\n watch: {\n /** Directories to watch for changes */\n watchedDirs: [\"config\", \"modules\", \"schemas\"] as const,\n },\n} as const;\n"],
5
+ "mappings": ";AAIO,IAAM,eAAe;AAAA;AAAA;AAAA;AAAA,EAI1B,QAAQ;AAAA;AAAA,IAEN,MAAM;AAAA;AAAA,IAEN,aAAa;AAAA;AAAA,IAEb,WAAW,MAAM,OAAO;AAAA;AAAA,IAExB,MAAM;AAAA,EACR;AAAA;AAAA;AAAA;AAAA,EAKA,KAAK;AAAA;AAAA;AAAA;AAAA;AAAA,IAKH,mBAAmB;AAAA,EACrB;AAAA;AAAA;AAAA;AAAA,EAKA,MAAM;AAAA;AAAA,IAEJ,aAAa,MAAM,OAAO;AAAA;AAAA,IAG1B,aAAa,CAAC,UAAU,WAAW,SAAS;AAAA,EAC9C;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO;AAAA;AAAA,IAEL,aAAa,CAAC,UAAU,WAAW,SAAS;AAAA,EAC9C;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,40 @@
1
+ // src/ports.ts
2
+ var UnsafePathError = class extends Error {
3
+ path;
4
+ constructor(path) {
5
+ super(`Path escapes the project directory: ${path}`);
6
+ this.name = "UnsafePathError";
7
+ this.path = path;
8
+ }
9
+ };
10
+ var PathNotFoundError = class extends Error {
11
+ path;
12
+ constructor(path) {
13
+ super(`Path not found: ${path}`);
14
+ this.name = "PathNotFoundError";
15
+ this.path = path;
16
+ }
17
+ };
18
+ function toSafeSegments(path) {
19
+ if (/^[a-zA-Z]:/.test(path) || path.startsWith("\\\\")) {
20
+ throw new UnsafePathError(path);
21
+ }
22
+ const segments = [];
23
+ for (const segment of path.split(/[/\\]+/)) {
24
+ if (segment === "" || segment === ".") {
25
+ continue;
26
+ }
27
+ if (segment === "..") {
28
+ throw new UnsafePathError(path);
29
+ }
30
+ segments.push(segment);
31
+ }
32
+ return segments;
33
+ }
34
+
35
+ export {
36
+ UnsafePathError,
37
+ PathNotFoundError,
38
+ toSafeSegments
39
+ };
40
+ //# sourceMappingURL=chunk-FELEIEAB.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/ports.ts"],
4
+ "sourcesContent": ["/**\n * The platform capabilities the editor's routes need, stated in the routes' own\n * terms rather than as a subset of `node:fs`.\n *\n * Everything here is injected, so the same Hono app runs against a developer's\n * real project directory (the Zuplo CLI) and against an in-memory project (the\n * per-PR preview Worker). Keeping the seam this narrow is what stops the two\n * from drifting: a route can't reach for `node:fs` without adding a method\n * here, and the preview build fails when something does.\n */\n\n/**\n * A path that escaped the project root, e.g. `?path=../../../etc/passwd`.\n *\n * Rejecting traversal is the port's job rather than each route's, because a\n * route that forgets is a directory-traversal hole and there is no other\n * chokepoint every path passes through.\n */\nexport class UnsafePathError extends Error {\n readonly path: string;\n\n constructor(path: string) {\n super(`Path escapes the project directory: ${path}`);\n this.name = \"UnsafePathError\";\n this.path = path;\n }\n}\n\n/**\n * A path that doesn't exist.\n *\n * Routes branch on this instead of on `ENOENT`, so a miss reports identically\n * however the filesystem is implemented \u2014 the case an errno-shaped port gets\n * subtly wrong.\n */\nexport class PathNotFoundError extends Error {\n readonly path: string;\n\n constructor(path: string) {\n super(`Path not found: ${path}`);\n this.name = \"PathNotFoundError\";\n this.path = path;\n }\n}\n\nexport type PathType = \"file\" | \"directory\";\n\nexport interface DirEntry {\n /** The entry's own name, not a path. */\n name: string;\n type: PathType;\n}\n\nexport interface PathStat {\n type: PathType;\n mtime: Date;\n}\n\nexport interface FileWithStat {\n content: string;\n /** The mtime of the content returned alongside it \u2014 see `readFileWithStat`. */\n mtime: Date;\n}\n\n/**\n * A project directory the editor can read and write.\n *\n * Every path is relative to the project root and slash-separated\n * (`config/routes.oas.json`), with or without a leading slash. Implementations\n * reject paths that climb out of the root with `UnsafePathError`.\n */\nexport interface FileSystemPort {\n /**\n * Reads a file and the mtime describing *that* content, together.\n *\n * The pair has to be consistent: the portal saves the returned mtime as\n * `Last-Modified` and sends it back as `If-Unmodified-Since` to detect\n * clobbering, so a stat taken separately from the read can describe a\n * different version of the file and produce phantom conflicts \u2014 or miss real\n * ones. How that's achieved is the adapter's problem (Node holds a single\n * file handle open across both).\n *\n * @throws PathNotFoundError when the file doesn't exist.\n */\n readFileWithStat(path: string): Promise<FileWithStat>;\n\n /**\n * Lists a directory's immediate children. Order is not significant.\n *\n * @throws PathNotFoundError when the directory doesn't exist.\n */\n listDir(path: string): Promise<DirEntry[]>;\n\n /** Returns `undefined` when nothing exists at `path`. */\n statPath(path: string): Promise<PathStat | undefined>;\n\n /**\n * Writes a file, creating any missing parent directories, and returns the\n * stat it now has \u2014 the caller hands that mtime straight back to the client.\n */\n writeFile(path: string, content: string | Uint8Array): Promise<PathStat>;\n\n /** @throws PathNotFoundError when `from` doesn't exist. */\n rename(from: string, to: string): Promise<void>;\n\n /** @throws PathNotFoundError when `path` doesn't exist. */\n remove(path: string): Promise<void>;\n\n /** Creates a directory and any missing parents. Succeeds if it exists. */\n mkdirp(path: string): Promise<void>;\n}\n\n/**\n * Which external editor the user picked for \"open in editor\".\n *\n * Separate from `FileSystemPort` because this lives outside the project \u2014 an\n * XDG config file under the user's home directory on Node, and nowhere at all\n * in a Worker, which has no home directory to write to.\n */\nexport interface EditorPreferencesPort {\n /** The saved editor id, or `null` if the user hasn't chosen one. */\n get(): Promise<string | null>;\n set(editor: string): Promise<void>;\n}\n\n/**\n * Tells the editor a file changed underneath it, so the SPA can reload it.\n *\n * On Node this is chokidar watching the project directory. In the preview it's\n * the in-memory filesystem reporting its own writes, which is what keeps the\n * SPA's live-reload path exercised there rather than dormant.\n */\nexport interface ChangeNotifier {\n /**\n * Registers `listener`, called with a project-relative path on each change.\n * Returns a function that unsubscribes.\n */\n onChange(listener: (path: string) => void): () => void;\n}\n\n/**\n * Splits a project-relative path into its segments, rejecting anything that\n * climbs out of the project or isn't relative to begin with.\n *\n * Both adapters route every path through this, so traversal is refused once, at\n * the boundary, instead of relying on each route to remember.\n */\nexport function toSafeSegments(path: string): string[] {\n // A Windows drive letter or a UNC path is absolute but survives POSIX\n // normalization, so it has to be caught by shape rather than by segment.\n if (/^[a-zA-Z]:/.test(path) || path.startsWith(\"\\\\\\\\\")) {\n throw new UnsafePathError(path);\n }\n\n const segments: string[] = [];\n // Backslashes are separators on Windows; treating them as ordinary characters\n // would let `..\\..` through as a single innocuous-looking segment.\n for (const segment of path.split(/[/\\\\]+/)) {\n if (segment === \"\" || segment === \".\") {\n continue;\n }\n if (segment === \"..\") {\n // Refuse rather than resolve. `config/../modules` is legitimate but\n // nothing the portal sends, and allowing it means the guard depends on\n // segment order \u2014 every prefix would have to stay inside the root too.\n throw new UnsafePathError(path);\n }\n segments.push(segment);\n }\n\n return segments;\n}\n"],
5
+ "mappings": ";AAkBO,IAAM,kBAAN,cAA8B,MAAM;AAAA,EAChC;AAAA,EAET,YAAY,MAAc;AACxB,UAAM,uCAAuC,IAAI,EAAE;AACnD,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AASO,IAAM,oBAAN,cAAgC,MAAM;AAAA,EAClC;AAAA,EAET,YAAY,MAAc;AACxB,UAAM,mBAAmB,IAAI,EAAE;AAC/B,SAAK,OAAO;AACZ,SAAK,OAAO;AAAA,EACd;AACF;AAwGO,SAAS,eAAe,MAAwB;AAGrD,MAAI,aAAa,KAAK,IAAI,KAAK,KAAK,WAAW,MAAM,GAAG;AACtD,UAAM,IAAI,gBAAgB,IAAI;AAAA,EAChC;AAEA,QAAM,WAAqB,CAAC;AAG5B,aAAW,WAAW,KAAK,MAAM,QAAQ,GAAG;AAC1C,QAAI,YAAY,MAAM,YAAY,KAAK;AACrC;AAAA,IACF;AACA,QAAI,YAAY,MAAM;AAIpB,YAAM,IAAI,gBAAgB,IAAI;AAAA,IAChC;AACA,aAAS,KAAK,OAAO;AAAA,EACvB;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }