@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 +6 -2
- package/dist/mcl.wasm +2 -2
- package/package.json +1 -1
- package/src/index.d.ts +71 -8
- package/src/index.js +332 -52
- package/src/limits.js +4 -0
- package/src/protocol.d.ts +49 -10
- package/src/worker.js +68 -8
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
|
|
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
|
|
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:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 96317496 bytes
|
|
4
|
+
sha256: 58f90cac0af9f6b4608f787528617729b99a6fcb65db52f168f95a1a6703e98f
|
package/package.json
CHANGED
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
|
|
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(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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 {
|
|
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 {
|
|
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 =
|
|
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(
|
|
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
|
|
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)
|
|
110
|
-
|
|
111
|
-
const
|
|
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
|
|
277
|
+
if (stopCurrent) stopCurrent(timeoutError);
|
|
278
|
+
else stopInitialization?.(timeoutError);
|
|
117
279
|
}, timeoutMs);
|
|
118
280
|
await waitUntilReady(requestOptions.signal);
|
|
119
|
-
if (requestOptions.signal?.aborted)
|
|
120
|
-
|
|
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")
|
|
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
|
-
|
|
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(() =>
|
|
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)
|
|
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({
|
|
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(() =>
|
|
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)
|
|
217
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
if (typeof
|
|
225
|
-
|
|
226
|
-
|
|
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)
|
|
229
|
-
|
|
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
|
-
|
|
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)
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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)
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
|
519
|
+
return isValidProjectPath(filePath);
|
|
262
520
|
}
|
|
263
521
|
|
|
264
522
|
function prepareFilesForTransfer(source, files, entrypoint) {
|
|
265
|
-
if (!isSafeRelativePath(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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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(
|
|
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) =>
|
|
311
|
-
|
|
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:
|
|
7
|
+
operation:
|
|
8
|
+
"parse" | "lint" | "format" | "check" | "evaluate" | "render" | "completion" | "definition" | "hover" | "rename";
|
|
8
9
|
position?: CompletionPosition;
|
|
9
|
-
|
|
10
|
-
|
|
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 = () =>
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
69
|
-
|
|
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(
|
|
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: {
|
|
190
|
+
error: {
|
|
191
|
+
code: "MCL-WASM-INPUT",
|
|
192
|
+
message: error instanceof Error ? error.message : String(error),
|
|
193
|
+
},
|
|
134
194
|
},
|
|
135
195
|
});
|
|
136
196
|
}
|