@manifest-configuration-language/browser 0.1.5 → 0.2.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/README.md CHANGED
@@ -21,13 +21,17 @@ try {
21
21
 
22
22
  Call `runtime.warmUp()` to load the WebAssembly runtime before the first project operation. Pass `signal` and `timeoutMs` to warm-up or an operation to cancel it or set an upper bound for that call. Warm-up's timeout and `initializationTimeoutMs` bound worker startup; an operation's `timeoutMs` bounds execution after startup. Cancelling or timing out worker startup terminates that worker; the next queued operation can create a fresh worker. Cancelling or timing out an operation terminates its worker. Operations and warm-up are serialized because the current Go runtime uses one in-memory project root.
23
23
 
24
- Project files can be UTF-8 strings or `Uint8Array` values. UTF-8 files remain inline in the JSON request. The Browser SDK copies each `Uint8Array` and transfers its buffer to the Worker; the Worker sends only `{ "encoding": "binary" }` metadata in the request JSON and passes a path-to-`Uint8Array` map as the second argument to `mclWasm.run(requestJSON, binaryFiles)`. The Go runtime copies those bytes into its project filesystem. The SDK-owned and caller-owned arrays remain reusable after each operation.
24
+ Project files can be UTF-8 strings or `Uint8Array` values. UTF-8 files remain inline in project updates. The Browser SDK copies each changed `Uint8Array` and transfers its buffer to the Worker; the Worker sends only `{ "encoding": "binary" }` metadata in the update JSON and passes a path-to-`Uint8Array` map to the Go/WASM project session. The Go runtime copies those bytes into its bounded project filesystem. The SDK-owned and caller-owned arrays remain reusable after each update.
25
+
26
+ An open project is a Worker-backed session. Call `project.update(files)` with the complete next snapshot when project files change, then use the returned project handle for subsequent operations. The SDK validates changed files, sends only changed, added, and removed files to the Worker, and applies those deltas to a bounded in-memory Go/WASM filesystem. Each operation sends its entrypoint source and session ID; the project filesystem is not resent or rebuilt. If a canceled or timed-out operation terminates the Worker, the next operation restores the complete snapshot before running. Existing `runtime.parse`, `runtime.render`, and other source-based methods continue to use per-request files.
27
+
28
+ `runtime.render` returns a `sourceMap` alongside the YAML. Each entry identifies a YAML document, a JSON Pointer within that document, and whether the output range covers a key or value. Source ranges use zero-based UTF-16 line and character positions with an exclusive end, matching LSP and CodeMirror positions; the engine converts its internal UTF-8 byte offsets before returning them. Source paths are project-relative. The source map is limited by `maxSourceMapBytes`; `sourceMap.complete` is false when the map was truncated or an MCL source range could not be resolved.
25
29
 
26
30
  `project.complete({ entrypoint, position })` returns the MCL language-service candidates for a zero-based UTF-16 position. Completion uses the same local project files as parsing and rendering, including schemas and Helm values schemas. It does not access a Kubernetes API server or any other network service.
27
31
 
28
32
  `project.definition({ entrypoint, position })` returns a project-relative declaration path and zero-based UTF-16 range for local and imported MCL declarations. `project.hover({ entrypoint, position })` returns declaration syntax, including explicit type annotations and function or rule signatures. Hover does not infer runtime value types, and both operations use only project files already supplied to the browser runtime.
29
33
 
30
- The package exports the completion, definition, and hover result types, `maxSourceBytes`, `maxProjectFiles`, `maxProjectFileBytes`, and `maxProjectBytes` so browser interfaces can consume the runtime's public types and limits.
34
+ The package exports the completion, definition, hover, rename, and source map result types, `maxSourceBytes`, `maxSourceMapBytes`, `maxRenameNameLength`, `maxProjectFiles`, `maxProjectFileBytes`, and `maxProjectBytes` so browser interfaces can consume the runtime's public types and limits. `isValidProjectFileSet(paths)` checks path validity, duplicate paths, and file/directory conflicts before a UI creates or renames project files. `rename()` returns source edits for local and non-exported top-level declarations within the active file; exported and cross-file symbols are left unchanged so the result never omits external references.
31
35
 
32
36
  MCL decodes the request into a root-relative `io/fs` filesystem for project files, module imports, schemas, and dependency locks. Kubernetes resource scope checks use built-in metadata and CRDs in the project; unknown resource kinds can still be rendered. The runtime limits are defined in the [protocol schema](https://github.com/manifest-configuration-language/mcl/blob/main/cmd/mcl-wasm/protocol.schema.json). The worker blocks `fetch` after loading the WASM runtime assets, so MCL project evaluation cannot make external network requests.
33
37
 
package/dist/mcl.wasm CHANGED
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/mcl.wasm
3
- size: 96219377 bytes
4
- sha256: 285f6bf9e6c1720d4db98f9aeedcf0d751e8ef33a5f7a3b2d2d2c501c9f12a9d
3
+ size: 96317496 bytes
4
+ sha256: 58f90cac0af9f6b4608f787528617729b99a6fcb65db52f168f95a1a6703e98f
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@manifest-configuration-language/browser",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Run MCL projects in a browser Web Worker using the Go WebAssembly runtime.",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.d.ts CHANGED
@@ -14,18 +14,29 @@ import type {
14
14
  ParseResult,
15
15
  RelatedLocation as ProtocolRelatedLocation,
16
16
  RenderResult,
17
+ RenameEdit as ProtocolRenameEdit,
18
+ RenameResult as ProtocolRenameResult,
17
19
  Request as WasmRequest,
18
20
  SourceRange as ProtocolSourceRange,
21
+ SourceMap as ProtocolSourceMap,
22
+ SourceMapEntry as ProtocolSourceMapEntry,
23
+ SourceMapOrigin as ProtocolSourceMapOrigin,
19
24
  } from "./protocol.js";
20
25
 
21
26
  export type MclOperation = WasmRequest["operation"];
22
27
 
23
28
  export declare const maxSourceBytes: number;
29
+ export declare const maxSourceMapBytes: number;
30
+ export declare const maxRenameNameLength: number;
24
31
  export declare const maxProjectFiles: number;
25
32
  export declare const maxProjectFileBytes: number;
26
33
  export declare const maxProjectBytes: number;
34
+ export declare const maxProjectPathLength: number;
35
+ export declare const maxProjectPathDepth: number;
36
+ export declare function isValidProjectPath(path: unknown): path is string;
37
+ export declare function isValidProjectFileSet(paths: readonly string[]): boolean;
27
38
 
28
- /** Root-relative project files passed to MCL's per-request filesystem. Uint8Array values are copied before transfer so callers can reuse them. */
39
+ /** Root-relative project files passed to MCL's in-memory session filesystem. Uint8Array values are copied before transfer so callers can reuse them. */
29
40
  export type ProjectFile = string | Uint8Array;
30
41
  export type ProjectFiles = Readonly<Record<string, ProjectFile>>;
31
42
 
@@ -39,7 +50,12 @@ export type DefinitionResult = ProtocolDefinitionResult;
39
50
  export type DefinitionLocation = ProtocolDefinitionLocation;
40
51
  export type HoverResult = ProtocolHoverResult;
41
52
  export type HoverInfo = ProtocolHoverInfo;
53
+ export type RenameEdit = ProtocolRenameEdit;
54
+ export type RenameResult = ProtocolRenameResult;
42
55
  export type SourceRange = ProtocolSourceRange;
56
+ export type SourceMap = ProtocolSourceMap;
57
+ export type SourceMapEntry = ProtocolSourceMapEntry;
58
+ export type SourceMapOrigin = ProtocolSourceMapOrigin;
43
59
 
44
60
  export type MclResult =
45
61
  | ({ operation: "parse" } & ParseResult)
@@ -47,6 +63,7 @@ export type MclResult =
47
63
  | ({ operation: "completion" } & ProtocolCompletionResult)
48
64
  | ({ operation: "definition" } & ProtocolDefinitionResult)
49
65
  | ({ operation: "hover" } & ProtocolHoverResult)
66
+ | ({ operation: "rename" } & ProtocolRenameResult)
50
67
  | ({ operation: "format" } & FormatResult)
51
68
  | ({ operation: "check" } & CheckResult)
52
69
  | ({ operation: "evaluate" } & EvaluateResult)
@@ -64,22 +81,60 @@ export interface MclRuntime {
64
81
  parse(source: string, options?: RequestOptions): Promise<MclResult & { operation: "parse" }>;
65
82
  lint(source: string, options?: RequestOptions): Promise<MclResult & { operation: "lint" }>;
66
83
  format(source: string, options?: RequestOptions): Promise<MclResult & { operation: "format" }>;
67
- check(source: string, files?: ProjectFiles, options?: RequestOptions): Promise<MclResult & { operation: "check" }>;
68
- evaluate(source: string, files?: ProjectFiles, options?: RequestOptions): Promise<MclResult & { operation: "evaluate" }>;
69
- render(source: string, files?: ProjectFiles, options?: RequestOptions): Promise<MclResult & { operation: "render" }>;
70
- definition(source: string, options: PositionRequestOptions): Promise<MclResult & { operation: "definition" }>;
71
- definition(source: string, files: ProjectFiles | undefined, options: PositionRequestOptions): Promise<MclResult & { operation: "definition" }>;
72
- hover(source: string, options: PositionRequestOptions): Promise<MclResult & { operation: "hover" }>;
73
- hover(source: string, files: ProjectFiles | undefined, options: PositionRequestOptions): Promise<MclResult & { operation: "hover" }>;
84
+ check(
85
+ source: string,
86
+ files?: ProjectFiles,
87
+ options?: RequestOptions,
88
+ ): Promise<MclResult & { operation: "check" }>;
89
+ evaluate(
90
+ source: string,
91
+ files?: ProjectFiles,
92
+ options?: RequestOptions,
93
+ ): Promise<MclResult & { operation: "evaluate" }>;
94
+ render(
95
+ source: string,
96
+ files?: ProjectFiles,
97
+ options?: RequestOptions,
98
+ ): Promise<MclResult & { operation: "render" }>;
99
+ definition(
100
+ source: string,
101
+ options: PositionRequestOptions,
102
+ ): Promise<MclResult & { operation: "definition" }>;
103
+ definition(
104
+ source: string,
105
+ files: ProjectFiles | undefined,
106
+ options: PositionRequestOptions,
107
+ ): Promise<MclResult & { operation: "definition" }>;
108
+ hover(
109
+ source: string,
110
+ options: PositionRequestOptions,
111
+ ): Promise<MclResult & { operation: "hover" }>;
112
+ hover(
113
+ source: string,
114
+ files: ProjectFiles | undefined,
115
+ options: PositionRequestOptions,
116
+ ): Promise<MclResult & { operation: "hover" }>;
117
+ rename(
118
+ source: string,
119
+ options: RenameRequestOptions,
120
+ ): Promise<MclResult & { operation: "rename" }>;
121
+ rename(
122
+ source: string,
123
+ files: ProjectFiles | undefined,
124
+ options: RenameRequestOptions,
125
+ ): Promise<MclResult & { operation: "rename" }>;
74
126
  dispose(): void;
75
127
  }
76
128
 
77
129
  export interface MclProject {
130
+ /** Returns a new complete snapshot handle for this Worker session. Only changed files are sent on the next operation. */
131
+ update(files: ProjectFiles): MclProject;
78
132
  parse(options?: ProjectOperationOptions): Promise<MclResult & { operation: "parse" }>;
79
133
  lint(options?: ProjectOperationOptions): Promise<MclResult & { operation: "lint" }>;
80
134
  complete(options: ProjectCompletionOptions): Promise<MclResult & { operation: "completion" }>;
81
135
  definition(options: ProjectNavigationOptions): Promise<MclResult & { operation: "definition" }>;
82
136
  hover(options: ProjectNavigationOptions): Promise<MclResult & { operation: "hover" }>;
137
+ rename(options: ProjectRenameOptions): Promise<MclResult & { operation: "rename" }>;
83
138
  format(options?: ProjectOperationOptions): Promise<MclResult & { operation: "format" }>;
84
139
  check(options?: ProjectOperationOptions): Promise<MclResult & { operation: "check" }>;
85
140
  evaluate(options?: ProjectOperationOptions): Promise<MclResult & { operation: "evaluate" }>;
@@ -103,11 +158,19 @@ export interface ProjectNavigationOptions extends ProjectOperationOptions {
103
158
  position: CompletionPosition;
104
159
  }
105
160
 
161
+ export interface ProjectRenameOptions extends ProjectNavigationOptions {
162
+ newName: string;
163
+ }
164
+
106
165
  export interface PositionRequestOptions extends RequestOptions {
107
166
  entrypoint?: string;
108
167
  position: CompletionPosition;
109
168
  }
110
169
 
170
+ export interface RenameRequestOptions extends PositionRequestOptions {
171
+ newName: string;
172
+ }
173
+
111
174
  export declare class MclRuntimeError extends Error {
112
175
  readonly code: string;
113
176
  readonly path?: string;
package/src/index.js CHANGED
@@ -1,6 +1,63 @@
1
- import { maxProjectBytes, maxProjectFileBytes, maxProjectFiles, maxSourceBytes } from "./limits.js";
1
+ import {
2
+ maxProjectBytes,
3
+ maxProjectFileBytes,
4
+ maxProjectFiles,
5
+ maxProjectPathDepth,
6
+ maxProjectPathLength,
7
+ maxRenameNameLength,
8
+ maxSourceBytes,
9
+ maxSourceMapBytes,
10
+ } from "./limits.js";
2
11
 
3
- export { maxProjectBytes, maxProjectFileBytes, maxProjectFiles, maxSourceBytes };
12
+ export {
13
+ maxProjectBytes,
14
+ maxProjectFileBytes,
15
+ maxProjectFiles,
16
+ maxProjectPathDepth,
17
+ maxProjectPathLength,
18
+ maxRenameNameLength,
19
+ maxSourceBytes,
20
+ maxSourceMapBytes,
21
+ };
22
+
23
+ export function isValidProjectPath(value) {
24
+ if (
25
+ typeof value !== "string" ||
26
+ value.length === 0 ||
27
+ value.startsWith("/") ||
28
+ /^[a-z]:/i.test(value) ||
29
+ value.includes("\\") ||
30
+ value.includes("\0")
31
+ ) {
32
+ return false;
33
+ }
34
+ let pathLength = 0;
35
+ let pathDepth = 1;
36
+ for (const character of value) {
37
+ if (++pathLength > maxProjectPathLength) return false;
38
+ if (character === "/" && ++pathDepth > maxProjectPathDepth) return false;
39
+ }
40
+ return value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
41
+ }
42
+
43
+ export function isValidProjectFileSet(paths) {
44
+ if (!Array.isArray(paths)) return false;
45
+ const files = new Set();
46
+ const directories = new Set();
47
+ for (const filePath of paths) {
48
+ if (!isValidProjectPath(filePath) || files.has(filePath) || directories.has(filePath))
49
+ return false;
50
+ let separator = filePath.indexOf("/");
51
+ while (separator !== -1) {
52
+ const directory = filePath.slice(0, separator);
53
+ if (files.has(directory)) return false;
54
+ directories.add(directory);
55
+ separator = filePath.indexOf("/", separator + 1);
56
+ }
57
+ files.add(filePath);
58
+ }
59
+ return true;
60
+ }
4
61
 
5
62
  const defaultInitializationTimeoutMs = 30_000;
6
63
  const defaultOperationTimeoutMs = 10_000;
@@ -22,7 +79,9 @@ export class MclRuntimeError extends Error {
22
79
  }
23
80
 
24
81
  export function createMclRuntime(options = {}) {
25
- const workerFactory = options.workerFactory ?? (() => new Worker(new URL("./worker.js", import.meta.url), { type: "module" }));
82
+ const workerFactory =
83
+ options.workerFactory ??
84
+ (() => new Worker(new URL("./worker.js", import.meta.url), { type: "module" }));
26
85
  const initializationTimeoutMs = options.initializationTimeoutMs ?? defaultInitializationTimeoutMs;
27
86
  let worker;
28
87
  let ready;
@@ -30,6 +89,7 @@ export function createMclRuntime(options = {}) {
30
89
  let queue = Promise.resolve();
31
90
  let sequence = 0;
32
91
  let workerGeneration = 0;
92
+ let workerProject;
33
93
  let stopCurrent;
34
94
  let stopInitialization;
35
95
 
@@ -39,6 +99,7 @@ export function createMclRuntime(options = {}) {
39
99
  const currentWorker = workerFactory();
40
100
  worker = currentWorker;
41
101
  workerGeneration++;
102
+ workerProject = undefined;
42
103
  let resolveReady;
43
104
  let rejectReady;
44
105
  const initialization = new Promise((resolve, reject) => {
@@ -46,7 +107,10 @@ export function createMclRuntime(options = {}) {
46
107
  rejectReady = reject;
47
108
  });
48
109
  ready = initialization;
49
- const timer = setTimeout(() => fail(new Error("MCL WebAssembly initialization timed out")), initializationTimeoutMs);
110
+ const timer = setTimeout(
111
+ () => fail(new Error("MCL WebAssembly initialization timed out")),
112
+ initializationTimeoutMs,
113
+ );
50
114
  const onMessage = (event) => {
51
115
  if (event.data?.type === "ready") {
52
116
  clearTimeout(timer);
@@ -101,26 +165,136 @@ export function createMclRuntime(options = {}) {
101
165
  }
102
166
  }
103
167
 
104
- async function execute(operation, source, files, requestOptions = {}, entrypoint = "main.mcl", position) {
168
+ async function syncProjectToWorker(project, requestOptions, currentWorker, currentGeneration) {
169
+ const previous = workerProject;
170
+ const reset = !previous || previous.id !== project.id;
171
+ const previousFiles = reset ? Object.create(null) : previous.snapshot.files;
172
+ const updates = Object.create(null);
173
+ const binaryFiles = Object.create(null);
174
+ const transferables = [];
175
+ const deleted = [];
176
+ for (const [filePath, content] of Object.entries(project.snapshot.files)) {
177
+ if (
178
+ !reset &&
179
+ Object.hasOwn(previousFiles, filePath) &&
180
+ sameContent(previousFiles[filePath], content)
181
+ )
182
+ continue;
183
+ if (typeof content === "string") {
184
+ updates[filePath] = { encoding: "utf8", content };
185
+ } else {
186
+ const copy = new Uint8Array(content);
187
+ updates[filePath] = { encoding: "binary" };
188
+ binaryFiles[filePath] = copy;
189
+ transferables.push(copy.buffer);
190
+ }
191
+ }
192
+ if (!reset) {
193
+ for (const filePath of Object.keys(previousFiles)) {
194
+ if (!Object.hasOwn(project.snapshot.files, filePath)) deleted.push(filePath);
195
+ }
196
+ }
197
+ if (!reset && Object.keys(updates).length === 0 && deleted.length === 0) return;
198
+
199
+ const requestId = ++sequence;
200
+ const update = JSON.stringify({ reset, files: updates, deleted });
201
+ await new Promise((resolve, reject) => {
202
+ let settled = false;
203
+ const cleanup = () => {
204
+ currentWorker.removeEventListener("message", onMessage);
205
+ currentWorker.removeEventListener("error", onError);
206
+ requestOptions.signal?.removeEventListener("abort", onAbort);
207
+ };
208
+ const finish = (callback, value) => {
209
+ if (settled) return;
210
+ settled = true;
211
+ cleanup();
212
+ if (stopCurrent === stop) stopCurrent = undefined;
213
+ callback(value);
214
+ };
215
+ const onMessage = (event) => {
216
+ if (worker !== currentWorker || workerGeneration !== currentGeneration) return;
217
+ const response = event.data;
218
+ if (response?.type !== "project-updated" || response.requestId !== requestId) return;
219
+ if (!response.value.ok) finish(reject, new MclRuntimeError(response.value.error));
220
+ else {
221
+ workerProject = { id: project.id, snapshot: project.snapshot };
222
+ finish(resolve);
223
+ }
224
+ };
225
+ const stop = (error) => {
226
+ finish(reject, error);
227
+ currentWorker.terminate();
228
+ if (worker === currentWorker) {
229
+ worker = undefined;
230
+ ready = undefined;
231
+ workerProject = undefined;
232
+ }
233
+ };
234
+ const onError = (event) => stop(new Error(event.message || "MCL worker failed"));
235
+ const onAbort = () => stop(new DOMException("The operation was aborted", "AbortError"));
236
+ currentWorker.addEventListener("message", onMessage);
237
+ currentWorker.addEventListener("error", onError);
238
+ requestOptions.signal?.addEventListener("abort", onAbort, { once: true });
239
+ stopCurrent = stop;
240
+ if (requestOptions.signal?.aborted) return onAbort();
241
+ try {
242
+ currentWorker.postMessage(
243
+ { type: "project-update", requestId, projectId: project.id, update, binaryFiles },
244
+ transferables,
245
+ );
246
+ } catch (error) {
247
+ stop(error);
248
+ }
249
+ });
250
+ }
251
+
252
+ async function execute(
253
+ operation,
254
+ source,
255
+ files,
256
+ requestOptions = {},
257
+ entrypoint = "main.mcl",
258
+ position,
259
+ project,
260
+ ) {
105
261
  const signal = requestOptions.signal;
106
262
  if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
107
263
  const run = async () => {
108
264
  if (disposed) throw new Error("MCL runtime has been disposed");
109
- if (requestOptions.signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
110
- const timeoutMs = requestOptions.timeoutMs ?? options.operationTimeoutMs ?? defaultOperationTimeoutMs;
111
- const timeoutError = new MclRuntimeError({ code: "MCL-WASM-TIMEOUT", message: "MCL operation timed out" });
265
+ if (requestOptions.signal?.aborted)
266
+ throw new DOMException("The operation was aborted", "AbortError");
267
+ const timeoutMs =
268
+ requestOptions.timeoutMs ?? options.operationTimeoutMs ?? defaultOperationTimeoutMs;
269
+ const timeoutError = new MclRuntimeError({
270
+ code: "MCL-WASM-TIMEOUT",
271
+ message: "MCL operation timed out",
272
+ });
112
273
  let timer;
113
274
  try {
114
275
  if (timeoutMs <= 0) throw timeoutError;
115
276
  timer = setTimeout(() => {
116
- (stopCurrent ?? stopInitialization)?.(timeoutError);
277
+ if (stopCurrent) stopCurrent(timeoutError);
278
+ else stopInitialization?.(timeoutError);
117
279
  }, timeoutMs);
118
280
  await waitUntilReady(requestOptions.signal);
119
- if (requestOptions.signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
120
- const preparedFiles = prepareFilesForTransfer(source, files, entrypoint);
121
- const requestId = ++sequence;
281
+ if (requestOptions.signal?.aborted)
282
+ throw new DOMException("The operation was aborted", "AbortError");
122
283
  const currentWorker = worker;
123
284
  const currentGeneration = workerGeneration;
285
+ if (project) {
286
+ await syncProjectToWorker(
287
+ project,
288
+ requestOptions,
289
+ currentWorker,
290
+ currentGeneration,
291
+ timeoutError,
292
+ );
293
+ }
294
+ const preparedFiles = project
295
+ ? undefined
296
+ : prepareFilesForTransfer(source, files, entrypoint);
297
+ const requestId = ++sequence;
124
298
  return await new Promise((resolve, reject) => {
125
299
  let settled = false;
126
300
  const cleanup = () => {
@@ -141,7 +315,8 @@ export function createMclRuntime(options = {}) {
141
315
  const response = event.data;
142
316
  if (response?.type !== "response" || response.requestId !== requestId) return;
143
317
  if (!response.value.ok) finish(reject, new MclRuntimeError(response.value.error));
144
- else if (operation === "lint") finish(resolve, { operation, diagnostics: response.value.result });
318
+ else if (operation === "lint")
319
+ finish(resolve, { operation, diagnostics: response.value.result });
145
320
  else finish(resolve, { operation, ...response.value.result });
146
321
  };
147
322
  const onError = (event) => stop(new Error(event.message || "MCL worker failed"));
@@ -159,7 +334,18 @@ export function createMclRuntime(options = {}) {
159
334
  requestOptions.signal?.addEventListener("abort", onAbort, { once: true });
160
335
  stopCurrent = stop;
161
336
  try {
162
- currentWorker.postMessage({ type: "run", requestId, operation, source, files: preparedFiles.files, entrypoint, position }, preparedFiles.transferables);
337
+ const message = {
338
+ type: "run",
339
+ requestId,
340
+ operation,
341
+ source,
342
+ entrypoint,
343
+ position,
344
+ newName: requestOptions.newName,
345
+ };
346
+ if (project) message.projectId = project.id;
347
+ else message.files = preparedFiles.files;
348
+ currentWorker.postMessage(message, preparedFiles?.transferables ?? []);
163
349
  } catch (error) {
164
350
  stop(error);
165
351
  }
@@ -179,17 +365,23 @@ export function createMclRuntime(options = {}) {
179
365
  signal.addEventListener("abort", onAbort, { once: true });
180
366
  if (signal.aborted) onAbort();
181
367
  });
182
- return Promise.race([result, aborted]).finally(() => signal.removeEventListener("abort", onAbort));
368
+ return Promise.race([result, aborted]).finally(() =>
369
+ signal.removeEventListener("abort", onAbort),
370
+ );
183
371
  }
184
372
 
185
373
  function warmUp(requestOptions = {}) {
186
374
  const signal = requestOptions.signal;
187
- if (signal?.aborted) return Promise.reject(new DOMException("The operation was aborted", "AbortError"));
375
+ if (signal?.aborted)
376
+ return Promise.reject(new DOMException("The operation was aborted", "AbortError"));
188
377
  const run = async () => {
189
378
  if (disposed) throw new Error("MCL runtime has been disposed");
190
379
  if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
191
380
  const timeoutMs = requestOptions.timeoutMs ?? initializationTimeoutMs;
192
- const timeoutError = new MclRuntimeError({ code: "MCL-WASM-TIMEOUT", message: "MCL WebAssembly initialization timed out" });
381
+ const timeoutError = new MclRuntimeError({
382
+ code: "MCL-WASM-TIMEOUT",
383
+ message: "MCL WebAssembly initialization timed out",
384
+ });
193
385
  let timer;
194
386
  try {
195
387
  if (timeoutMs <= 0) throw timeoutError;
@@ -208,48 +400,114 @@ export function createMclRuntime(options = {}) {
208
400
  signal.addEventListener("abort", onAbort, { once: true });
209
401
  if (signal.aborted) onAbort();
210
402
  });
211
- return Promise.race([result, aborted]).finally(() => signal.removeEventListener("abort", onAbort));
403
+ return Promise.race([result, aborted]).finally(() =>
404
+ signal.removeEventListener("abort", onAbort),
405
+ );
212
406
  }
213
407
 
214
408
  function openProject(files) {
409
+ const snapshot = createProjectSnapshot(files);
410
+ const id = `mcl-project-${++sequence}`;
411
+ return makeProject(id, snapshot);
412
+ }
413
+
414
+ function sameContent(left, right) {
415
+ if (left === right) return true;
416
+ if (typeof left !== typeof right) return false;
417
+ if (typeof left === "string") return left === right;
418
+ if (left.byteLength !== right.byteLength) return false;
419
+ for (let index = 0; index < left.byteLength; index++) {
420
+ if (left[index] !== right[index]) return false;
421
+ }
422
+ return true;
423
+ }
424
+
425
+ function createProjectSnapshot(files, previous) {
215
426
  const entries = Object.entries(files ?? {});
216
- if (entries.length === 0) throw new TypeError("Project must contain at least one MCL source file");
217
- if (entries.length > maxProjectFiles) throw new RangeError(`Project has more than ${maxProjectFiles} files`);
427
+ if (entries.length === 0)
428
+ throw new TypeError("Project must contain at least one MCL source file");
429
+ if (entries.length > maxProjectFiles)
430
+ throw new RangeError(`Project has more than ${maxProjectFiles} files`);
431
+ if (!isValidProjectFileSet(entries.map(([filePath]) => filePath)))
432
+ throw new TypeError(
433
+ "Project file paths must be valid and cannot conflict with a file or directory path",
434
+ );
218
435
  let totalBytes = 0;
219
- // Project paths are user controlled; a null prototype keeps names such as
220
- // `__proto__` as ordinary file paths instead of mutating the file map.
221
436
  const projectFiles = Object.create(null);
222
- for (const [filePath, content] of entries) {
223
- if (!isSafeRelativePath(filePath)) throw new TypeError(`Invalid project file path: ${filePath}`);
224
- if (typeof content !== "string" && !(content instanceof Uint8Array)) throw new TypeError(`Project file ${filePath} must be a string or Uint8Array`);
225
- const fileBytes = typeof content === "string" ? new TextEncoder().encode(content).byteLength : content.byteLength;
226
- if (fileBytes > maxProjectFileBytes) throw new RangeError(`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`);
437
+ const fileSizes = Object.create(null);
438
+ for (const [filePath, input] of entries) {
439
+ if (typeof input !== "string" && !(input instanceof Uint8Array))
440
+ throw new TypeError(`Project file ${filePath} must be a string or Uint8Array`);
441
+ const previousContent = previous?.files[filePath];
442
+ const unchanged = previousContent !== undefined && sameContent(previousContent, input);
443
+ const content = unchanged
444
+ ? previousContent
445
+ : typeof input === "string"
446
+ ? input
447
+ : new Uint8Array(input);
448
+ const fileBytes = unchanged
449
+ ? previous.sizes[filePath]
450
+ : typeof content === "string"
451
+ ? new TextEncoder().encode(content).byteLength
452
+ : content.byteLength;
453
+ if (!unchanged && fileBytes > maxProjectFileBytes)
454
+ throw new RangeError(
455
+ `Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`,
456
+ );
227
457
  totalBytes += fileBytes;
228
- if (totalBytes > maxProjectBytes) throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
229
- projectFiles[filePath] = typeof content === "string" ? content : new Uint8Array(content);
458
+ if (totalBytes > maxProjectBytes)
459
+ throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
460
+ projectFiles[filePath] = content;
461
+ fileSizes[filePath] = fileBytes;
230
462
  }
463
+ const sourcePaths = Object.keys(projectFiles)
464
+ .filter((filePath) => filePath.endsWith(".mcl"))
465
+ .sort();
466
+ if (sourcePaths.length === 0)
467
+ throw new TypeError("Project must contain at least one MCL source file");
468
+ const defaultEntrypoint = Object.hasOwn(projectFiles, "main.mcl")
469
+ ? "main.mcl"
470
+ : sourcePaths.length === 1
471
+ ? sourcePaths[0]
472
+ : undefined;
473
+ return Object.freeze({
474
+ files: Object.freeze(projectFiles),
475
+ sizes: Object.freeze(fileSizes),
476
+ defaultEntrypoint,
477
+ });
478
+ }
231
479
 
232
- const sourcePaths = Object.keys(projectFiles).filter((filePath) => filePath.endsWith(".mcl")).sort();
233
- const defaultEntrypoint = Object.hasOwn(projectFiles, "main.mcl") ? "main.mcl" : sourcePaths.length === 1 ? sourcePaths[0] : undefined;
480
+ function makeProject(id, snapshot) {
234
481
  const runProjectOperation = async (operation, requestOptions = {}) => {
235
- const entrypoint = requestOptions.entrypoint ?? defaultEntrypoint;
236
- if (!entrypoint) throw new TypeError("Set entrypoint when the project has multiple MCL source files");
237
- if (!isSafeRelativePath(entrypoint) || !Object.hasOwn(projectFiles, entrypoint)) throw new TypeError(`Project entrypoint does not exist: ${entrypoint}`);
238
- const entry = projectFiles[entrypoint];
239
- const source = typeof entry === "string" ? entry : new TextDecoder("utf-8", { fatal: true }).decode(entry);
482
+ const entrypoint = requestOptions.entrypoint ?? snapshot.defaultEntrypoint;
483
+ if (!entrypoint)
484
+ throw new TypeError("Set entrypoint when the project has multiple MCL source files");
485
+ if (!isSafeRelativePath(entrypoint) || !Object.hasOwn(snapshot.files, entrypoint))
486
+ throw new TypeError(`Project entrypoint does not exist: ${entrypoint}`);
487
+ const entry = snapshot.files[entrypoint];
488
+ const source =
489
+ typeof entry === "string" ? entry : new TextDecoder("utf-8", { fatal: true }).decode(entry);
240
490
  const sourceBytes = new TextEncoder().encode(source).byteLength;
241
- if (sourceBytes > maxSourceBytes) throw new RangeError(`Source exceeds the ${maxSourceBytes}-byte limit`);
242
- const projectBytes = Object.values(projectFiles).reduce((total, file) => total + (typeof file === "string" ? new TextEncoder().encode(file).byteLength : file.byteLength), 0);
243
- if (projectBytes > maxProjectBytes) throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
244
- const otherFiles = Object.fromEntries(Object.entries(projectFiles).filter(([filePath]) => filePath !== entrypoint));
245
- return execute(operation, source, otherFiles, requestOptions, entrypoint, requestOptions.position);
491
+ if (sourceBytes > maxSourceBytes)
492
+ throw new RangeError(`Source exceeds the ${maxSourceBytes}-byte limit`);
493
+ return execute(
494
+ operation,
495
+ source,
496
+ undefined,
497
+ requestOptions,
498
+ entrypoint,
499
+ requestOptions.position,
500
+ { id, snapshot },
501
+ );
246
502
  };
247
503
  return Object.freeze({
504
+ update: (nextFiles) => makeProject(id, createProjectSnapshot(nextFiles, snapshot)),
248
505
  parse: (requestOptions) => runProjectOperation("parse", requestOptions),
249
506
  lint: (requestOptions) => runProjectOperation("lint", requestOptions),
250
507
  complete: (requestOptions) => runProjectOperation("completion", requestOptions),
251
508
  definition: (requestOptions) => runProjectOperation("definition", requestOptions),
252
509
  hover: (requestOptions) => runProjectOperation("hover", requestOptions),
510
+ rename: (requestOptions) => runProjectOperation("rename", requestOptions),
253
511
  format: (requestOptions) => runProjectOperation("format", requestOptions),
254
512
  check: (requestOptions) => runProjectOperation("check", requestOptions),
255
513
  evaluate: (requestOptions) => runProjectOperation("evaluate", requestOptions),
@@ -258,27 +516,37 @@ export function createMclRuntime(options = {}) {
258
516
  }
259
517
 
260
518
  function isSafeRelativePath(filePath) {
261
- return typeof filePath === "string" && filePath.length > 0 && !/^[a-z]:/i.test(filePath) && !filePath.startsWith("/") && !filePath.includes("\\") && filePath.split("/").every((part) => part && part !== "." && part !== "..");
519
+ return isValidProjectPath(filePath);
262
520
  }
263
521
 
264
522
  function prepareFilesForTransfer(source, files, entrypoint) {
265
- if (!isSafeRelativePath(entrypoint)) throw new TypeError(`Invalid project entrypoint: ${entrypoint}`);
523
+ if (!isSafeRelativePath(entrypoint))
524
+ throw new TypeError(`Invalid project entrypoint: ${entrypoint}`);
266
525
  const sourceBytes = new TextEncoder().encode(source).byteLength;
267
- if (sourceBytes > maxSourceBytes) throw new RangeError(`Source exceeds the ${maxSourceBytes}-byte limit`);
526
+ if (sourceBytes > maxSourceBytes)
527
+ throw new RangeError(`Source exceeds the ${maxSourceBytes}-byte limit`);
268
528
  const entries = Object.entries(files ?? {});
269
- if (entries.length + 1 > maxProjectFiles) throw new RangeError(`Project has more than ${maxProjectFiles} files`);
529
+ if (entries.length + 1 > maxProjectFiles)
530
+ throw new RangeError(`Project has more than ${maxProjectFiles} files`);
270
531
  let totalBytes = sourceBytes;
271
532
  const transferredFiles = Object.create(null);
272
533
  const transferables = [];
273
534
  for (const [filePath, content] of entries) {
274
- if (!isSafeRelativePath(filePath) || filePath === entrypoint) throw new TypeError(`Invalid project file path: ${filePath}`);
535
+ if (!isSafeRelativePath(filePath) || filePath === entrypoint)
536
+ throw new TypeError(`Invalid project file path: ${filePath}`);
275
537
  if (typeof content === "string") {
276
538
  const fileBytes = new TextEncoder().encode(content).byteLength;
277
- if (fileBytes > maxProjectFileBytes) throw new RangeError(`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`);
539
+ if (fileBytes > maxProjectFileBytes)
540
+ throw new RangeError(
541
+ `Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`,
542
+ );
278
543
  totalBytes += fileBytes;
279
544
  transferredFiles[filePath] = content;
280
545
  } else if (content instanceof Uint8Array) {
281
- if (content.byteLength > maxProjectFileBytes) throw new RangeError(`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`);
546
+ if (content.byteLength > maxProjectFileBytes)
547
+ throw new RangeError(
548
+ `Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`,
549
+ );
282
550
  totalBytes += content.byteLength;
283
551
  const copy = new Uint8Array(content);
284
552
  transferredFiles[filePath] = copy;
@@ -286,7 +554,8 @@ export function createMclRuntime(options = {}) {
286
554
  } else {
287
555
  throw new TypeError(`Project file ${filePath} must be a string or Uint8Array`);
288
556
  }
289
- if (totalBytes > maxProjectBytes) throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
557
+ if (totalBytes > maxProjectBytes)
558
+ throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
290
559
  }
291
560
  return { files: transferredFiles, transferables };
292
561
  }
@@ -295,7 +564,14 @@ export function createMclRuntime(options = {}) {
295
564
  const files = requestOptions === undefined ? undefined : filesOrOptions;
296
565
  const options = requestOptions ?? filesOrOptions;
297
566
  if (!options?.position) throw new TypeError("A source position is required");
298
- return execute(operation, source, files, options, options.entrypoint ?? "main.mcl", options.position);
567
+ return execute(
568
+ operation,
569
+ source,
570
+ files,
571
+ options,
572
+ options.entrypoint ?? "main.mcl",
573
+ options.position,
574
+ );
299
575
  }
300
576
 
301
577
  return {
@@ -307,8 +583,12 @@ export function createMclRuntime(options = {}) {
307
583
  check: (source, files, requestOptions) => execute("check", source, files, requestOptions),
308
584
  evaluate: (source, files, requestOptions) => execute("evaluate", source, files, requestOptions),
309
585
  render: (source, files, requestOptions) => execute("render", source, files, requestOptions),
310
- definition: (source, filesOrOptions, requestOptions) => runSourceNavigation("definition", source, filesOrOptions, requestOptions),
311
- hover: (source, filesOrOptions, requestOptions) => runSourceNavigation("hover", source, filesOrOptions, requestOptions),
586
+ definition: (source, filesOrOptions, requestOptions) =>
587
+ runSourceNavigation("definition", source, filesOrOptions, requestOptions),
588
+ hover: (source, filesOrOptions, requestOptions) =>
589
+ runSourceNavigation("hover", source, filesOrOptions, requestOptions),
590
+ rename: (source, filesOrOptions, requestOptions) =>
591
+ runSourceNavigation("rename", source, filesOrOptions, requestOptions),
312
592
  dispose() {
313
593
  if (disposed) return;
314
594
  disposed = true;
package/src/limits.js CHANGED
@@ -4,4 +4,8 @@ export const maxProjectFiles = 4096;
4
4
  export const maxProjectFileBytes = 16777216;
5
5
  export const maxProjectBytes = 67108864;
6
6
  export const maxOutputBytes = 1048576;
7
+ export const maxSourceMapBytes = 1048576;
8
+ export const maxRenameNameLength = 256;
7
9
  export const evaluationTimeoutMillis = 4000;
10
+ export const maxProjectPathLength = 512;
11
+ export const maxProjectPathDepth = 64;
package/src/protocol.d.ts CHANGED
@@ -4,12 +4,11 @@ export type MCLWebAssemblyRequestAndResponseProtocol = Request | Response;
4
4
  export type Request = {
5
5
  requestId?: number;
6
6
  revision?: number;
7
- operation: "parse" | "lint" | "format" | "check" | "evaluate" | "render" | "completion" | "definition" | "hover";
7
+ operation:
8
+ "parse" | "lint" | "format" | "check" | "evaluate" | "render" | "completion" | "definition" | "hover" | "rename";
8
9
  position?: CompletionPosition;
9
- /**
10
- * Project-relative MCL source path. Defaults to main.mcl.
11
- */
12
- entrypoint?: string;
10
+ newName?: string;
11
+ entrypoint?: ProjectPath;
13
12
  /**
14
13
  * UTF-8 entrypoint source, limited by the runtime.
15
14
  */
@@ -21,6 +20,10 @@ export type Request = {
21
20
  [k: string]: FileInput;
22
21
  };
23
22
  };
23
+ /**
24
+ * Project-relative MCL source path. Defaults to main.mcl.
25
+ */
26
+ export type ProjectPath = string;
24
27
  /**
25
28
  * Binary files omit content and are supplied in the optional second run argument under the same path.
26
29
  */
@@ -29,6 +32,8 @@ export type FileInput = {
29
32
  content?: string;
30
33
  };
31
34
  export type LintResult = LintDiagnostic[];
35
+ export type ProjectPath1 = ProjectPath2;
36
+ export type ProjectPath2 = string;
32
37
 
33
38
  export interface CompletionPosition {
34
39
  line: number;
@@ -47,7 +52,8 @@ export interface Response {
47
52
  | RenderResult
48
53
  | CompletionResult
49
54
  | DefinitionResult
50
- | HoverResult;
55
+ | HoverResult
56
+ | RenameResult;
51
57
  error?: Error;
52
58
  }
53
59
  export interface ParseResult {
@@ -77,6 +83,35 @@ export interface EvaluateResult {
77
83
  export interface RenderResult {
78
84
  yaml: string;
79
85
  artifacts: number;
86
+ sourceMap: SourceMap;
87
+ }
88
+ /**
89
+ * Source map entries are limited by maxSourceMapBytes. Ranges use zero-based UTF-16 line and character positions with an exclusive end.
90
+ */
91
+ export interface SourceMap {
92
+ complete: boolean;
93
+ entries: SourceMapEntry[];
94
+ }
95
+ export interface SourceMapEntry {
96
+ document: number;
97
+ /**
98
+ * JSON Pointer into the rendered YAML document.
99
+ */
100
+ path: string;
101
+ kind: "key" | "value";
102
+ output: SourceRange;
103
+ sources: SourceMapOrigin[];
104
+ }
105
+ export interface SourceRange {
106
+ start: CompletionPosition;
107
+ end: CompletionPosition;
108
+ }
109
+ export interface SourceMapOrigin {
110
+ path: ProjectPath1;
111
+ range: SourceRange;
112
+ kind: "mcl" | "mcl-overlay-base" | "mcl-overlay-patch";
113
+ multiplicity: "unique" | "repeated" | "ambiguous";
114
+ occurrences: number;
80
115
  }
81
116
  export interface CompletionResult {
82
117
  isIncomplete: boolean;
@@ -94,10 +129,6 @@ export interface DefinitionLocation {
94
129
  path: string;
95
130
  range: SourceRange;
96
131
  }
97
- export interface SourceRange {
98
- start: CompletionPosition;
99
- end: CompletionPosition;
100
- }
101
132
  export interface HoverResult {
102
133
  hover: HoverInfo | null;
103
134
  }
@@ -105,6 +136,14 @@ export interface HoverInfo {
105
136
  contents: string;
106
137
  range: SourceRange;
107
138
  }
139
+ export interface RenameResult {
140
+ edits: RenameEdit[];
141
+ }
142
+ export interface RenameEdit {
143
+ path: ProjectPath1;
144
+ range: SourceRange;
145
+ newText: string;
146
+ }
108
147
  export interface Error {
109
148
  code: string;
110
149
  message: string;
package/src/worker.js CHANGED
@@ -21,7 +21,8 @@ const originalNetworkAPIs = new Map(
21
21
  );
22
22
 
23
23
  function denyNetworkAccess() {
24
- globalThis.fetch = () => Promise.reject(new TypeError("Network access is disabled in the MCL runtime"));
24
+ globalThis.fetch = () =>
25
+ Promise.reject(new TypeError("Network access is disabled in the MCL runtime"));
25
26
  for (const [name] of originalNetworkAPIs) {
26
27
  Object.defineProperty(globalThis, name, {
27
28
  configurable: true,
@@ -43,16 +44,35 @@ async function probeNetworkAccess(url) {
43
44
  }
44
45
 
45
46
  const availableAPIs = ["fetch", ...originalNetworkAPIs.keys()];
46
- const blockedAPIs = availableAPIs.filter(
47
- (name) => name === "fetch" ? globalThis.fetch !== originalFetch : globalThis[name] !== originalNetworkAPIs.get(name),
47
+ const blockedAPIs = availableAPIs.filter((name) =>
48
+ name === "fetch"
49
+ ? globalThis.fetch !== originalFetch
50
+ : globalThis[name] !== originalNetworkAPIs.get(name),
48
51
  );
49
52
  return { fetchBlocked, availableAPIs, blockedAPIs };
50
53
  }
51
54
 
55
+ function waitForWasmReady() {
56
+ return new Promise((resolve) => {
57
+ Object.defineProperty(globalThis, "mclWasmReady", {
58
+ configurable: true,
59
+ set(value) {
60
+ Object.defineProperty(globalThis, "mclWasmReady", {
61
+ configurable: true,
62
+ writable: true,
63
+ value,
64
+ });
65
+ if (value === true) resolve();
66
+ },
67
+ });
68
+ });
69
+ }
70
+
52
71
  async function initialize() {
53
72
  if (initialized) return initialized;
54
73
  initialized = (async () => {
55
- if (!runtimeAssets?.wasmURL || !runtimeAssets?.wasmExecURL) throw new Error("MCL runtime assets were not configured");
74
+ if (!runtimeAssets?.wasmURL || !runtimeAssets?.wasmExecURL)
75
+ throw new Error("MCL runtime assets were not configured");
56
76
  await import(runtimeAssets.wasmExecURL);
57
77
  const go = new Go();
58
78
  const response = await runtimeFetch(runtimeAssets.wasmURL);
@@ -65,8 +85,13 @@ async function initialize() {
65
85
  } catch {
66
86
  ({ instance } = await WebAssembly.instantiate(await response.arrayBuffer(), go.importObject));
67
87
  }
68
- void go.run(instance);
69
- while (!globalThis.mclWasmReady) await new Promise((resolve) => setTimeout(resolve, 0));
88
+ const wasmReady = waitForWasmReady();
89
+ await Promise.race([
90
+ wasmReady,
91
+ go.run(instance).then(() => {
92
+ throw new Error("MCL WebAssembly exited before initialization completed");
93
+ }),
94
+ ]);
70
95
  postMessage({ type: "ready" });
71
96
  })().catch((error) => {
72
97
  postMessage({ type: "fatal", message: error instanceof Error ? error.message : String(error) });
@@ -105,6 +130,29 @@ self.addEventListener("message", async (event) => {
105
130
  initialize().catch(() => {});
106
131
  return;
107
132
  }
133
+ if (message?.type === "project-update") {
134
+ try {
135
+ await initialize();
136
+ const binaryFiles = message.binaryFiles ?? Object.create(null);
137
+ const result = JSON.parse(
138
+ globalThis.mclWasm.updateProject(message.projectId, message.update, binaryFiles),
139
+ );
140
+ postMessage({ type: "project-updated", requestId: message.requestId, value: result });
141
+ } catch (error) {
142
+ postMessage({
143
+ type: "project-updated",
144
+ requestId: message.requestId,
145
+ value: {
146
+ ok: false,
147
+ error: {
148
+ code: "MCL-WASM-PROJECT",
149
+ message: error instanceof Error ? error.message : String(error),
150
+ },
151
+ },
152
+ });
153
+ }
154
+ return;
155
+ }
108
156
  if (message?.type !== "run") return;
109
157
  try {
110
158
  await initialize();
@@ -114,11 +162,20 @@ self.addEventListener("message", async (event) => {
114
162
  revision: message.requestId,
115
163
  operation: message.operation,
116
164
  entrypoint: message.entrypoint,
165
+ newName: message.newName,
117
166
  position: message.position,
118
167
  source: message.source,
119
168
  files: encodedFiles.files,
120
169
  };
121
- const value = JSON.parse(globalThis.mclWasm.run(JSON.stringify(request), encodedFiles.binaryFiles));
170
+ const value = JSON.parse(
171
+ message.projectId === undefined
172
+ ? globalThis.mclWasm.run(JSON.stringify(request), encodedFiles.binaryFiles)
173
+ : globalThis.mclWasm.run(
174
+ JSON.stringify(request),
175
+ encodedFiles.binaryFiles,
176
+ message.projectId,
177
+ ),
178
+ );
122
179
  postMessage({
123
180
  type: "response",
124
181
  requestId: message.requestId,
@@ -130,7 +187,10 @@ self.addEventListener("message", async (event) => {
130
187
  requestId: message.requestId,
131
188
  value: {
132
189
  ok: false,
133
- error: { code: "MCL-WASM-INPUT", message: error instanceof Error ? error.message : String(error) },
190
+ error: {
191
+ code: "MCL-WASM-INPUT",
192
+ message: error instanceof Error ? error.message : String(error),
193
+ },
134
194
  },
135
195
  });
136
196
  }