@manifest-configuration-language/browser 0.1.6 → 0.3.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 +7 -3
- package/dist/mcl.wasm +2 -2
- package/package.json +1 -1
- package/src/index.d.ts +83 -8
- package/src/index.js +312 -51
- package/src/limits.js +2 -0
- package/src/protocol.d.ts +62 -6
- 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
|
-
`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.
|
|
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. `project.references({ entrypoint, position })` returns the declaration and references found in the supplied MCL project. These 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, references, 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 edits for local symbols and exported declarations, including qualified value and type references in the supplied MCL project. It returns no edits when a rename would be ambiguous or collide with an existing exported declaration.
|
|
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: 96778456 bytes
|
|
4
|
+
sha256: 037981cf0ac4773dca73ca1cd9ddffa8b9e9b25aedb15d8c72b1be8ea26c5558
|
package/package.json
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -13,22 +13,32 @@ import type {
|
|
|
13
13
|
LintDiagnostic as ProtocolLintDiagnostic,
|
|
14
14
|
ParseResult,
|
|
15
15
|
RelatedLocation as ProtocolRelatedLocation,
|
|
16
|
+
ReferenceLocation as ProtocolReferenceLocation,
|
|
17
|
+
ReferencesResult as ProtocolReferencesResult,
|
|
16
18
|
RenderResult,
|
|
19
|
+
RenameEdit as ProtocolRenameEdit,
|
|
20
|
+
RenameResult as ProtocolRenameResult,
|
|
17
21
|
Request as WasmRequest,
|
|
18
22
|
SourceRange as ProtocolSourceRange,
|
|
23
|
+
SourceMap as ProtocolSourceMap,
|
|
24
|
+
SourceMapEntry as ProtocolSourceMapEntry,
|
|
25
|
+
SourceMapOrigin as ProtocolSourceMapOrigin,
|
|
19
26
|
} from "./protocol.js";
|
|
20
27
|
|
|
21
28
|
export type MclOperation = WasmRequest["operation"];
|
|
22
29
|
|
|
23
30
|
export declare const maxSourceBytes: number;
|
|
31
|
+
export declare const maxSourceMapBytes: number;
|
|
32
|
+
export declare const maxRenameNameLength: number;
|
|
24
33
|
export declare const maxProjectFiles: number;
|
|
25
34
|
export declare const maxProjectFileBytes: number;
|
|
26
35
|
export declare const maxProjectBytes: number;
|
|
27
36
|
export declare const maxProjectPathLength: number;
|
|
28
37
|
export declare const maxProjectPathDepth: number;
|
|
29
38
|
export declare function isValidProjectPath(path: unknown): path is string;
|
|
39
|
+
export declare function isValidProjectFileSet(paths: readonly string[]): boolean;
|
|
30
40
|
|
|
31
|
-
/** Root-relative project files passed to MCL's
|
|
41
|
+
/** Root-relative project files passed to MCL's in-memory session filesystem. Uint8Array values are copied before transfer so callers can reuse them. */
|
|
32
42
|
export type ProjectFile = string | Uint8Array;
|
|
33
43
|
export type ProjectFiles = Readonly<Record<string, ProjectFile>>;
|
|
34
44
|
|
|
@@ -42,7 +52,14 @@ export type DefinitionResult = ProtocolDefinitionResult;
|
|
|
42
52
|
export type DefinitionLocation = ProtocolDefinitionLocation;
|
|
43
53
|
export type HoverResult = ProtocolHoverResult;
|
|
44
54
|
export type HoverInfo = ProtocolHoverInfo;
|
|
55
|
+
export type RenameEdit = ProtocolRenameEdit;
|
|
56
|
+
export type RenameResult = ProtocolRenameResult;
|
|
57
|
+
export type ReferenceLocation = ProtocolReferenceLocation;
|
|
58
|
+
export type ReferencesResult = ProtocolReferencesResult;
|
|
45
59
|
export type SourceRange = ProtocolSourceRange;
|
|
60
|
+
export type SourceMap = ProtocolSourceMap;
|
|
61
|
+
export type SourceMapEntry = ProtocolSourceMapEntry;
|
|
62
|
+
export type SourceMapOrigin = ProtocolSourceMapOrigin;
|
|
46
63
|
|
|
47
64
|
export type MclResult =
|
|
48
65
|
| ({ operation: "parse" } & ParseResult)
|
|
@@ -50,6 +67,8 @@ export type MclResult =
|
|
|
50
67
|
| ({ operation: "completion" } & ProtocolCompletionResult)
|
|
51
68
|
| ({ operation: "definition" } & ProtocolDefinitionResult)
|
|
52
69
|
| ({ operation: "hover" } & ProtocolHoverResult)
|
|
70
|
+
| ({ operation: "references" } & ProtocolReferencesResult)
|
|
71
|
+
| ({ operation: "rename" } & ProtocolRenameResult)
|
|
53
72
|
| ({ operation: "format" } & FormatResult)
|
|
54
73
|
| ({ operation: "check" } & CheckResult)
|
|
55
74
|
| ({ operation: "evaluate" } & EvaluateResult)
|
|
@@ -67,22 +86,70 @@ export interface MclRuntime {
|
|
|
67
86
|
parse(source: string, options?: RequestOptions): Promise<MclResult & { operation: "parse" }>;
|
|
68
87
|
lint(source: string, options?: RequestOptions): Promise<MclResult & { operation: "lint" }>;
|
|
69
88
|
format(source: string, options?: RequestOptions): Promise<MclResult & { operation: "format" }>;
|
|
70
|
-
check(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
89
|
+
check(
|
|
90
|
+
source: string,
|
|
91
|
+
files?: ProjectFiles,
|
|
92
|
+
options?: RequestOptions,
|
|
93
|
+
): Promise<MclResult & { operation: "check" }>;
|
|
94
|
+
evaluate(
|
|
95
|
+
source: string,
|
|
96
|
+
files?: ProjectFiles,
|
|
97
|
+
options?: RequestOptions,
|
|
98
|
+
): Promise<MclResult & { operation: "evaluate" }>;
|
|
99
|
+
render(
|
|
100
|
+
source: string,
|
|
101
|
+
files?: ProjectFiles,
|
|
102
|
+
options?: RequestOptions,
|
|
103
|
+
): Promise<MclResult & { operation: "render" }>;
|
|
104
|
+
definition(
|
|
105
|
+
source: string,
|
|
106
|
+
options: PositionRequestOptions,
|
|
107
|
+
): Promise<MclResult & { operation: "definition" }>;
|
|
108
|
+
definition(
|
|
109
|
+
source: string,
|
|
110
|
+
files: ProjectFiles | undefined,
|
|
111
|
+
options: PositionRequestOptions,
|
|
112
|
+
): Promise<MclResult & { operation: "definition" }>;
|
|
113
|
+
hover(
|
|
114
|
+
source: string,
|
|
115
|
+
options: PositionRequestOptions,
|
|
116
|
+
): Promise<MclResult & { operation: "hover" }>;
|
|
117
|
+
hover(
|
|
118
|
+
source: string,
|
|
119
|
+
files: ProjectFiles | undefined,
|
|
120
|
+
options: PositionRequestOptions,
|
|
121
|
+
): Promise<MclResult & { operation: "hover" }>;
|
|
122
|
+
references(
|
|
123
|
+
source: string,
|
|
124
|
+
options: PositionRequestOptions,
|
|
125
|
+
): Promise<MclResult & { operation: "references" }>;
|
|
126
|
+
references(
|
|
127
|
+
source: string,
|
|
128
|
+
files: ProjectFiles | undefined,
|
|
129
|
+
options: PositionRequestOptions,
|
|
130
|
+
): Promise<MclResult & { operation: "references" }>;
|
|
131
|
+
rename(
|
|
132
|
+
source: string,
|
|
133
|
+
options: RenameRequestOptions,
|
|
134
|
+
): Promise<MclResult & { operation: "rename" }>;
|
|
135
|
+
rename(
|
|
136
|
+
source: string,
|
|
137
|
+
files: ProjectFiles | undefined,
|
|
138
|
+
options: RenameRequestOptions,
|
|
139
|
+
): Promise<MclResult & { operation: "rename" }>;
|
|
77
140
|
dispose(): void;
|
|
78
141
|
}
|
|
79
142
|
|
|
80
143
|
export interface MclProject {
|
|
144
|
+
/** Returns a new complete snapshot handle for this Worker session. Only changed files are sent on the next operation. */
|
|
145
|
+
update(files: ProjectFiles): MclProject;
|
|
81
146
|
parse(options?: ProjectOperationOptions): Promise<MclResult & { operation: "parse" }>;
|
|
82
147
|
lint(options?: ProjectOperationOptions): Promise<MclResult & { operation: "lint" }>;
|
|
83
148
|
complete(options: ProjectCompletionOptions): Promise<MclResult & { operation: "completion" }>;
|
|
84
149
|
definition(options: ProjectNavigationOptions): Promise<MclResult & { operation: "definition" }>;
|
|
85
150
|
hover(options: ProjectNavigationOptions): Promise<MclResult & { operation: "hover" }>;
|
|
151
|
+
references(options: ProjectNavigationOptions): Promise<MclResult & { operation: "references" }>;
|
|
152
|
+
rename(options: ProjectRenameOptions): Promise<MclResult & { operation: "rename" }>;
|
|
86
153
|
format(options?: ProjectOperationOptions): Promise<MclResult & { operation: "format" }>;
|
|
87
154
|
check(options?: ProjectOperationOptions): Promise<MclResult & { operation: "check" }>;
|
|
88
155
|
evaluate(options?: ProjectOperationOptions): Promise<MclResult & { operation: "evaluate" }>;
|
|
@@ -106,11 +173,19 @@ export interface ProjectNavigationOptions extends ProjectOperationOptions {
|
|
|
106
173
|
position: CompletionPosition;
|
|
107
174
|
}
|
|
108
175
|
|
|
176
|
+
export interface ProjectRenameOptions extends ProjectNavigationOptions {
|
|
177
|
+
newName: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
109
180
|
export interface PositionRequestOptions extends RequestOptions {
|
|
110
181
|
entrypoint?: string;
|
|
111
182
|
position: CompletionPosition;
|
|
112
183
|
}
|
|
113
184
|
|
|
185
|
+
export interface RenameRequestOptions extends PositionRequestOptions {
|
|
186
|
+
newName: string;
|
|
187
|
+
}
|
|
188
|
+
|
|
114
189
|
export declare class MclRuntimeError extends Error {
|
|
115
190
|
readonly code: string;
|
|
116
191
|
readonly path?: string;
|
package/src/index.js
CHANGED
|
@@ -4,14 +4,31 @@ import {
|
|
|
4
4
|
maxProjectFiles,
|
|
5
5
|
maxProjectPathDepth,
|
|
6
6
|
maxProjectPathLength,
|
|
7
|
+
maxRenameNameLength,
|
|
7
8
|
maxSourceBytes,
|
|
9
|
+
maxSourceMapBytes,
|
|
8
10
|
} from "./limits.js";
|
|
9
11
|
|
|
10
|
-
export {
|
|
12
|
+
export {
|
|
13
|
+
maxProjectBytes,
|
|
14
|
+
maxProjectFileBytes,
|
|
15
|
+
maxProjectFiles,
|
|
16
|
+
maxProjectPathDepth,
|
|
17
|
+
maxProjectPathLength,
|
|
18
|
+
maxRenameNameLength,
|
|
19
|
+
maxSourceBytes,
|
|
20
|
+
maxSourceMapBytes,
|
|
21
|
+
};
|
|
11
22
|
|
|
12
23
|
export function isValidProjectPath(value) {
|
|
13
|
-
if (
|
|
14
|
-
|
|
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
|
+
) {
|
|
15
32
|
return false;
|
|
16
33
|
}
|
|
17
34
|
let pathLength = 0;
|
|
@@ -23,6 +40,25 @@ export function isValidProjectPath(value) {
|
|
|
23
40
|
return value.split("/").every((part) => part !== "" && part !== "." && part !== "..");
|
|
24
41
|
}
|
|
25
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
|
+
}
|
|
61
|
+
|
|
26
62
|
const defaultInitializationTimeoutMs = 30_000;
|
|
27
63
|
const defaultOperationTimeoutMs = 10_000;
|
|
28
64
|
const wasmURL = new URL("../dist/mcl.wasm", import.meta.url).href;
|
|
@@ -43,7 +79,9 @@ export class MclRuntimeError extends Error {
|
|
|
43
79
|
}
|
|
44
80
|
|
|
45
81
|
export function createMclRuntime(options = {}) {
|
|
46
|
-
const workerFactory =
|
|
82
|
+
const workerFactory =
|
|
83
|
+
options.workerFactory ??
|
|
84
|
+
(() => new Worker(new URL("./worker.js", import.meta.url), { type: "module" }));
|
|
47
85
|
const initializationTimeoutMs = options.initializationTimeoutMs ?? defaultInitializationTimeoutMs;
|
|
48
86
|
let worker;
|
|
49
87
|
let ready;
|
|
@@ -51,6 +89,7 @@ export function createMclRuntime(options = {}) {
|
|
|
51
89
|
let queue = Promise.resolve();
|
|
52
90
|
let sequence = 0;
|
|
53
91
|
let workerGeneration = 0;
|
|
92
|
+
let workerProject;
|
|
54
93
|
let stopCurrent;
|
|
55
94
|
let stopInitialization;
|
|
56
95
|
|
|
@@ -60,6 +99,7 @@ export function createMclRuntime(options = {}) {
|
|
|
60
99
|
const currentWorker = workerFactory();
|
|
61
100
|
worker = currentWorker;
|
|
62
101
|
workerGeneration++;
|
|
102
|
+
workerProject = undefined;
|
|
63
103
|
let resolveReady;
|
|
64
104
|
let rejectReady;
|
|
65
105
|
const initialization = new Promise((resolve, reject) => {
|
|
@@ -67,7 +107,10 @@ export function createMclRuntime(options = {}) {
|
|
|
67
107
|
rejectReady = reject;
|
|
68
108
|
});
|
|
69
109
|
ready = initialization;
|
|
70
|
-
const timer = setTimeout(
|
|
110
|
+
const timer = setTimeout(
|
|
111
|
+
() => fail(new Error("MCL WebAssembly initialization timed out")),
|
|
112
|
+
initializationTimeoutMs,
|
|
113
|
+
);
|
|
71
114
|
const onMessage = (event) => {
|
|
72
115
|
if (event.data?.type === "ready") {
|
|
73
116
|
clearTimeout(timer);
|
|
@@ -122,14 +165,111 @@ export function createMclRuntime(options = {}) {
|
|
|
122
165
|
}
|
|
123
166
|
}
|
|
124
167
|
|
|
125
|
-
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
|
+
) {
|
|
126
261
|
const signal = requestOptions.signal;
|
|
127
262
|
if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
|
|
128
263
|
const run = async () => {
|
|
129
264
|
if (disposed) throw new Error("MCL runtime has been disposed");
|
|
130
|
-
if (requestOptions.signal?.aborted)
|
|
131
|
-
|
|
132
|
-
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
|
+
});
|
|
133
273
|
let timer;
|
|
134
274
|
try {
|
|
135
275
|
if (timeoutMs <= 0) throw timeoutError;
|
|
@@ -138,11 +278,23 @@ export function createMclRuntime(options = {}) {
|
|
|
138
278
|
else stopInitialization?.(timeoutError);
|
|
139
279
|
}, timeoutMs);
|
|
140
280
|
await waitUntilReady(requestOptions.signal);
|
|
141
|
-
if (requestOptions.signal?.aborted)
|
|
142
|
-
|
|
143
|
-
const requestId = ++sequence;
|
|
281
|
+
if (requestOptions.signal?.aborted)
|
|
282
|
+
throw new DOMException("The operation was aborted", "AbortError");
|
|
144
283
|
const currentWorker = worker;
|
|
145
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;
|
|
146
298
|
return await new Promise((resolve, reject) => {
|
|
147
299
|
let settled = false;
|
|
148
300
|
const cleanup = () => {
|
|
@@ -163,7 +315,8 @@ export function createMclRuntime(options = {}) {
|
|
|
163
315
|
const response = event.data;
|
|
164
316
|
if (response?.type !== "response" || response.requestId !== requestId) return;
|
|
165
317
|
if (!response.value.ok) finish(reject, new MclRuntimeError(response.value.error));
|
|
166
|
-
else if (operation === "lint")
|
|
318
|
+
else if (operation === "lint")
|
|
319
|
+
finish(resolve, { operation, diagnostics: response.value.result });
|
|
167
320
|
else finish(resolve, { operation, ...response.value.result });
|
|
168
321
|
};
|
|
169
322
|
const onError = (event) => stop(new Error(event.message || "MCL worker failed"));
|
|
@@ -181,7 +334,18 @@ export function createMclRuntime(options = {}) {
|
|
|
181
334
|
requestOptions.signal?.addEventListener("abort", onAbort, { once: true });
|
|
182
335
|
stopCurrent = stop;
|
|
183
336
|
try {
|
|
184
|
-
|
|
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 ?? []);
|
|
185
349
|
} catch (error) {
|
|
186
350
|
stop(error);
|
|
187
351
|
}
|
|
@@ -201,17 +365,23 @@ export function createMclRuntime(options = {}) {
|
|
|
201
365
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
202
366
|
if (signal.aborted) onAbort();
|
|
203
367
|
});
|
|
204
|
-
return Promise.race([result, aborted]).finally(() =>
|
|
368
|
+
return Promise.race([result, aborted]).finally(() =>
|
|
369
|
+
signal.removeEventListener("abort", onAbort),
|
|
370
|
+
);
|
|
205
371
|
}
|
|
206
372
|
|
|
207
373
|
function warmUp(requestOptions = {}) {
|
|
208
374
|
const signal = requestOptions.signal;
|
|
209
|
-
if (signal?.aborted)
|
|
375
|
+
if (signal?.aborted)
|
|
376
|
+
return Promise.reject(new DOMException("The operation was aborted", "AbortError"));
|
|
210
377
|
const run = async () => {
|
|
211
378
|
if (disposed) throw new Error("MCL runtime has been disposed");
|
|
212
379
|
if (signal?.aborted) throw new DOMException("The operation was aborted", "AbortError");
|
|
213
380
|
const timeoutMs = requestOptions.timeoutMs ?? initializationTimeoutMs;
|
|
214
|
-
const timeoutError = new MclRuntimeError({
|
|
381
|
+
const timeoutError = new MclRuntimeError({
|
|
382
|
+
code: "MCL-WASM-TIMEOUT",
|
|
383
|
+
message: "MCL WebAssembly initialization timed out",
|
|
384
|
+
});
|
|
215
385
|
let timer;
|
|
216
386
|
try {
|
|
217
387
|
if (timeoutMs <= 0) throw timeoutError;
|
|
@@ -230,48 +400,115 @@ export function createMclRuntime(options = {}) {
|
|
|
230
400
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
231
401
|
if (signal.aborted) onAbort();
|
|
232
402
|
});
|
|
233
|
-
return Promise.race([result, aborted]).finally(() =>
|
|
403
|
+
return Promise.race([result, aborted]).finally(() =>
|
|
404
|
+
signal.removeEventListener("abort", onAbort),
|
|
405
|
+
);
|
|
234
406
|
}
|
|
235
407
|
|
|
236
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) {
|
|
237
426
|
const entries = Object.entries(files ?? {});
|
|
238
|
-
if (entries.length === 0)
|
|
239
|
-
|
|
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
|
+
);
|
|
240
435
|
let totalBytes = 0;
|
|
241
|
-
// Project paths are user controlled; a null prototype keeps names such as
|
|
242
|
-
// `__proto__` as ordinary file paths instead of mutating the file map.
|
|
243
436
|
const projectFiles = Object.create(null);
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
if (typeof
|
|
247
|
-
|
|
248
|
-
|
|
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
|
+
);
|
|
249
457
|
totalBytes += fileBytes;
|
|
250
|
-
if (totalBytes > maxProjectBytes)
|
|
251
|
-
|
|
458
|
+
if (totalBytes > maxProjectBytes)
|
|
459
|
+
throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
|
|
460
|
+
projectFiles[filePath] = content;
|
|
461
|
+
fileSizes[filePath] = fileBytes;
|
|
252
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
|
+
}
|
|
253
479
|
|
|
254
|
-
|
|
255
|
-
const defaultEntrypoint = Object.hasOwn(projectFiles, "main.mcl") ? "main.mcl" : sourcePaths.length === 1 ? sourcePaths[0] : undefined;
|
|
480
|
+
function makeProject(id, snapshot) {
|
|
256
481
|
const runProjectOperation = async (operation, requestOptions = {}) => {
|
|
257
|
-
const entrypoint = requestOptions.entrypoint ?? defaultEntrypoint;
|
|
258
|
-
if (!entrypoint)
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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);
|
|
262
490
|
const sourceBytes = new TextEncoder().encode(source).byteLength;
|
|
263
|
-
if (sourceBytes > maxSourceBytes)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
);
|
|
268
502
|
};
|
|
269
503
|
return Object.freeze({
|
|
504
|
+
update: (nextFiles) => makeProject(id, createProjectSnapshot(nextFiles, snapshot)),
|
|
270
505
|
parse: (requestOptions) => runProjectOperation("parse", requestOptions),
|
|
271
506
|
lint: (requestOptions) => runProjectOperation("lint", requestOptions),
|
|
272
507
|
complete: (requestOptions) => runProjectOperation("completion", requestOptions),
|
|
273
508
|
definition: (requestOptions) => runProjectOperation("definition", requestOptions),
|
|
274
509
|
hover: (requestOptions) => runProjectOperation("hover", requestOptions),
|
|
510
|
+
references: (requestOptions) => runProjectOperation("references", requestOptions),
|
|
511
|
+
rename: (requestOptions) => runProjectOperation("rename", requestOptions),
|
|
275
512
|
format: (requestOptions) => runProjectOperation("format", requestOptions),
|
|
276
513
|
check: (requestOptions) => runProjectOperation("check", requestOptions),
|
|
277
514
|
evaluate: (requestOptions) => runProjectOperation("evaluate", requestOptions),
|
|
@@ -284,23 +521,33 @@ export function createMclRuntime(options = {}) {
|
|
|
284
521
|
}
|
|
285
522
|
|
|
286
523
|
function prepareFilesForTransfer(source, files, entrypoint) {
|
|
287
|
-
if (!isSafeRelativePath(entrypoint))
|
|
524
|
+
if (!isSafeRelativePath(entrypoint))
|
|
525
|
+
throw new TypeError(`Invalid project entrypoint: ${entrypoint}`);
|
|
288
526
|
const sourceBytes = new TextEncoder().encode(source).byteLength;
|
|
289
|
-
if (sourceBytes > maxSourceBytes)
|
|
527
|
+
if (sourceBytes > maxSourceBytes)
|
|
528
|
+
throw new RangeError(`Source exceeds the ${maxSourceBytes}-byte limit`);
|
|
290
529
|
const entries = Object.entries(files ?? {});
|
|
291
|
-
if (entries.length + 1 > maxProjectFiles)
|
|
530
|
+
if (entries.length + 1 > maxProjectFiles)
|
|
531
|
+
throw new RangeError(`Project has more than ${maxProjectFiles} files`);
|
|
292
532
|
let totalBytes = sourceBytes;
|
|
293
533
|
const transferredFiles = Object.create(null);
|
|
294
534
|
const transferables = [];
|
|
295
535
|
for (const [filePath, content] of entries) {
|
|
296
|
-
if (!isSafeRelativePath(filePath) || filePath === entrypoint)
|
|
536
|
+
if (!isSafeRelativePath(filePath) || filePath === entrypoint)
|
|
537
|
+
throw new TypeError(`Invalid project file path: ${filePath}`);
|
|
297
538
|
if (typeof content === "string") {
|
|
298
539
|
const fileBytes = new TextEncoder().encode(content).byteLength;
|
|
299
|
-
if (fileBytes > maxProjectFileBytes)
|
|
540
|
+
if (fileBytes > maxProjectFileBytes)
|
|
541
|
+
throw new RangeError(
|
|
542
|
+
`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`,
|
|
543
|
+
);
|
|
300
544
|
totalBytes += fileBytes;
|
|
301
545
|
transferredFiles[filePath] = content;
|
|
302
546
|
} else if (content instanceof Uint8Array) {
|
|
303
|
-
if (content.byteLength > maxProjectFileBytes)
|
|
547
|
+
if (content.byteLength > maxProjectFileBytes)
|
|
548
|
+
throw new RangeError(
|
|
549
|
+
`Project file ${filePath} exceeds the ${maxProjectFileBytes}-byte limit`,
|
|
550
|
+
);
|
|
304
551
|
totalBytes += content.byteLength;
|
|
305
552
|
const copy = new Uint8Array(content);
|
|
306
553
|
transferredFiles[filePath] = copy;
|
|
@@ -308,7 +555,8 @@ export function createMclRuntime(options = {}) {
|
|
|
308
555
|
} else {
|
|
309
556
|
throw new TypeError(`Project file ${filePath} must be a string or Uint8Array`);
|
|
310
557
|
}
|
|
311
|
-
if (totalBytes > maxProjectBytes)
|
|
558
|
+
if (totalBytes > maxProjectBytes)
|
|
559
|
+
throw new RangeError(`Project files exceed the ${maxProjectBytes}-byte limit`);
|
|
312
560
|
}
|
|
313
561
|
return { files: transferredFiles, transferables };
|
|
314
562
|
}
|
|
@@ -317,7 +565,14 @@ export function createMclRuntime(options = {}) {
|
|
|
317
565
|
const files = requestOptions === undefined ? undefined : filesOrOptions;
|
|
318
566
|
const options = requestOptions ?? filesOrOptions;
|
|
319
567
|
if (!options?.position) throw new TypeError("A source position is required");
|
|
320
|
-
return execute(
|
|
568
|
+
return execute(
|
|
569
|
+
operation,
|
|
570
|
+
source,
|
|
571
|
+
files,
|
|
572
|
+
options,
|
|
573
|
+
options.entrypoint ?? "main.mcl",
|
|
574
|
+
options.position,
|
|
575
|
+
);
|
|
321
576
|
}
|
|
322
577
|
|
|
323
578
|
return {
|
|
@@ -329,8 +584,14 @@ export function createMclRuntime(options = {}) {
|
|
|
329
584
|
check: (source, files, requestOptions) => execute("check", source, files, requestOptions),
|
|
330
585
|
evaluate: (source, files, requestOptions) => execute("evaluate", source, files, requestOptions),
|
|
331
586
|
render: (source, files, requestOptions) => execute("render", source, files, requestOptions),
|
|
332
|
-
definition: (source, filesOrOptions, requestOptions) =>
|
|
333
|
-
|
|
587
|
+
definition: (source, filesOrOptions, requestOptions) =>
|
|
588
|
+
runSourceNavigation("definition", source, filesOrOptions, requestOptions),
|
|
589
|
+
hover: (source, filesOrOptions, requestOptions) =>
|
|
590
|
+
runSourceNavigation("hover", source, filesOrOptions, requestOptions),
|
|
591
|
+
references: (source, filesOrOptions, requestOptions) =>
|
|
592
|
+
runSourceNavigation("references", source, filesOrOptions, requestOptions),
|
|
593
|
+
rename: (source, filesOrOptions, requestOptions) =>
|
|
594
|
+
runSourceNavigation("rename", source, filesOrOptions, requestOptions),
|
|
334
595
|
dispose() {
|
|
335
596
|
if (disposed) return;
|
|
336
597
|
disposed = true;
|
package/src/limits.js
CHANGED
|
@@ -4,6 +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;
|
|
8
10
|
export const maxProjectPathLength = 512;
|
|
9
11
|
export const maxProjectPathDepth = 64;
|
package/src/protocol.d.ts
CHANGED
|
@@ -4,8 +4,20 @@ export type MCLWebAssemblyRequestAndResponseProtocol = Request | Response;
|
|
|
4
4
|
export type Request = {
|
|
5
5
|
requestId?: number;
|
|
6
6
|
revision?: number;
|
|
7
|
-
operation:
|
|
7
|
+
operation:
|
|
8
|
+
| "parse"
|
|
9
|
+
| "lint"
|
|
10
|
+
| "format"
|
|
11
|
+
| "check"
|
|
12
|
+
| "evaluate"
|
|
13
|
+
| "render"
|
|
14
|
+
| "completion"
|
|
15
|
+
| "definition"
|
|
16
|
+
| "hover"
|
|
17
|
+
| "references"
|
|
18
|
+
| "rename";
|
|
8
19
|
position?: CompletionPosition;
|
|
20
|
+
newName?: string;
|
|
9
21
|
entrypoint?: ProjectPath;
|
|
10
22
|
/**
|
|
11
23
|
* UTF-8 entrypoint source, limited by the runtime.
|
|
@@ -30,6 +42,8 @@ export type FileInput = {
|
|
|
30
42
|
content?: string;
|
|
31
43
|
};
|
|
32
44
|
export type LintResult = LintDiagnostic[];
|
|
45
|
+
export type ProjectPath1 = ProjectPath2;
|
|
46
|
+
export type ProjectPath2 = string;
|
|
33
47
|
|
|
34
48
|
export interface CompletionPosition {
|
|
35
49
|
line: number;
|
|
@@ -48,7 +62,9 @@ export interface Response {
|
|
|
48
62
|
| RenderResult
|
|
49
63
|
| CompletionResult
|
|
50
64
|
| DefinitionResult
|
|
51
|
-
| HoverResult
|
|
65
|
+
| HoverResult
|
|
66
|
+
| ReferencesResult
|
|
67
|
+
| RenameResult;
|
|
52
68
|
error?: Error;
|
|
53
69
|
}
|
|
54
70
|
export interface ParseResult {
|
|
@@ -78,6 +94,35 @@ export interface EvaluateResult {
|
|
|
78
94
|
export interface RenderResult {
|
|
79
95
|
yaml: string;
|
|
80
96
|
artifacts: number;
|
|
97
|
+
sourceMap: SourceMap;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Source map entries are limited by maxSourceMapBytes. Ranges use zero-based UTF-16 line and character positions with an exclusive end.
|
|
101
|
+
*/
|
|
102
|
+
export interface SourceMap {
|
|
103
|
+
complete: boolean;
|
|
104
|
+
entries: SourceMapEntry[];
|
|
105
|
+
}
|
|
106
|
+
export interface SourceMapEntry {
|
|
107
|
+
document: number;
|
|
108
|
+
/**
|
|
109
|
+
* JSON Pointer into the rendered YAML document.
|
|
110
|
+
*/
|
|
111
|
+
path: string;
|
|
112
|
+
kind: "key" | "value";
|
|
113
|
+
output: SourceRange;
|
|
114
|
+
sources: SourceMapOrigin[];
|
|
115
|
+
}
|
|
116
|
+
export interface SourceRange {
|
|
117
|
+
start: CompletionPosition;
|
|
118
|
+
end: CompletionPosition;
|
|
119
|
+
}
|
|
120
|
+
export interface SourceMapOrigin {
|
|
121
|
+
path: ProjectPath1;
|
|
122
|
+
range: SourceRange;
|
|
123
|
+
kind: "mcl" | "mcl-overlay-base" | "mcl-overlay-patch";
|
|
124
|
+
multiplicity: "unique" | "repeated" | "ambiguous";
|
|
125
|
+
occurrences: number;
|
|
81
126
|
}
|
|
82
127
|
export interface CompletionResult {
|
|
83
128
|
isIncomplete: boolean;
|
|
@@ -95,10 +140,6 @@ export interface DefinitionLocation {
|
|
|
95
140
|
path: string;
|
|
96
141
|
range: SourceRange;
|
|
97
142
|
}
|
|
98
|
-
export interface SourceRange {
|
|
99
|
-
start: CompletionPosition;
|
|
100
|
-
end: CompletionPosition;
|
|
101
|
-
}
|
|
102
143
|
export interface HoverResult {
|
|
103
144
|
hover: HoverInfo | null;
|
|
104
145
|
}
|
|
@@ -106,6 +147,21 @@ export interface HoverInfo {
|
|
|
106
147
|
contents: string;
|
|
107
148
|
range: SourceRange;
|
|
108
149
|
}
|
|
150
|
+
export interface ReferencesResult {
|
|
151
|
+
references: ReferenceLocation[];
|
|
152
|
+
}
|
|
153
|
+
export interface ReferenceLocation {
|
|
154
|
+
path: ProjectPath1;
|
|
155
|
+
range: SourceRange;
|
|
156
|
+
}
|
|
157
|
+
export interface RenameResult {
|
|
158
|
+
edits: RenameEdit[];
|
|
159
|
+
}
|
|
160
|
+
export interface RenameEdit {
|
|
161
|
+
path: ProjectPath1;
|
|
162
|
+
range: SourceRange;
|
|
163
|
+
newText: string;
|
|
164
|
+
}
|
|
109
165
|
export interface Error {
|
|
110
166
|
code: string;
|
|
111
167
|
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
|
}
|