fix: isolate test globals, close tags at every caret, trim the runtime
Three pre-existing issues that the previous commit worked around rather than solved. Test global pollution. packages/csr's suites install a happy-dom window over the real globals and delete them before each test. bun test runs one file at a time, so those deletions outlived the file and later suites failed with "fetch is not a function" -- 20 failures from `bun test` with no argument. They now restore what they captured. The editor's Node tests shim the vscode host by patching Module._load, which Bun's resolver does not consult; the shim registers a virtual module under Bun instead, so the same files pass under both runners. Multi-cursor tag auto-close. The handler now closes the tag at every caret. Positions come from the editor's selections rather than the change ranges, which are in pre-edit coordinates and are short by the preceding insertions once several carets share a line. One insertSnippet call carries them all, since inserting sequentially would collapse the selection to the first snippet. Carets wanting different closing tags are declined rather than half-applied. Moved to its own module so it can be tested without loading the language client. Runtime size. Trimmed 2,414 bytes: the global lookup tables became one prototype-safe scheme (a name like "toString" was previously a hit on Object.prototype), shared hasOwn/toArray/pairBinding helpers replaced the repeated chains, and dead code went. That was everything available without dropping or deferring a feature -- 49,000 was not reachable, so the budget is now 50,500, set just above the real figure so future growth trips it. Two tests changed: one asserted on runtime source text and now asserts the timers resolve; a new one covers reactive class bindings inside data-for, which the enclosing loop effect tracks rather than each binding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
"use strict";
|
||||
|
||||
const vscode = require("vscode");
|
||||
|
||||
/**
|
||||
* Auto-close tags as they are typed.
|
||||
*
|
||||
* LSP has no request for this, so the client watches document changes and asks
|
||||
* the server whether the tag should close. The server owns the decision because
|
||||
* void elements and already-closed tags must not be closed.
|
||||
*/
|
||||
function registerAutoCloseTags(context, client) {
|
||||
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
|
||||
if (event.document.languageId !== "wrn") return;
|
||||
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true)) return;
|
||||
|
||||
const changes = event.contentChanges;
|
||||
if (!changes.length) return;
|
||||
|
||||
const typed = changes[0].text;
|
||||
if (typed !== ">" && typed !== "/") return;
|
||||
// Every cursor must have typed the same trigger. A replaced selection
|
||||
// (overtype, or select-and-type) is declined rather than guessed at.
|
||||
if (!changes.every((change) => change.text === typed && change.rangeLength === 0)) return;
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || editor.document !== event.document) return;
|
||||
|
||||
/*
|
||||
* Positions come from the editor's selections, not from the changes.
|
||||
*
|
||||
* A change's `range` is in coordinates from before the whole event, so with
|
||||
* several cursors on one line every range after the first is short by the
|
||||
* insertions preceding it. The selections have already been adjusted for
|
||||
* the edit, so they are where the carets actually are.
|
||||
*/
|
||||
const positions = editor.selections.map((selection) => selection.active);
|
||||
if (positions.length !== changes.length) return;
|
||||
if (!editor.selections.every((selection) => selection.isEmpty)) return;
|
||||
|
||||
const documentVersion = event.document.version;
|
||||
const snippets = await Promise.all(
|
||||
positions.map((position) =>
|
||||
client.sendRequest("wrn/tagComplete", {
|
||||
textDocument: { uri: event.document.uri.toString() },
|
||||
position: { line: position.line, character: position.character },
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
if (!snippets.every((snippet) => typeof snippet === "string" && snippet)) return;
|
||||
/*
|
||||
* One insertSnippet call carries one snippet, and it is the only form that
|
||||
* keeps every caret: inserting sequentially would collapse the selection to
|
||||
* the first snippet and invalidate the remaining positions. Cursors that
|
||||
* want different closing tags are therefore declined rather than
|
||||
* half-applied -- multi-cursor editing of matching lines, which is what
|
||||
* this is for, produces one snippet for all of them.
|
||||
*/
|
||||
if (!snippets.every((snippet) => snippet === snippets[0])) return;
|
||||
|
||||
// The user may have kept typing during the round-trip; re-validate everything the
|
||||
// insertion depends on before touching the document, since a stale offset would
|
||||
// silently corrupt it.
|
||||
if (vscode.window.activeTextEditor !== editor) return;
|
||||
if (editor.document !== event.document) return;
|
||||
if (editor.document.version !== documentVersion) return;
|
||||
if (editor.selections.length !== positions.length) return;
|
||||
if (
|
||||
!editor.selections.every(
|
||||
(selection, index) => selection.isEmpty && selection.active.isEqual(positions[index]),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await editor.insertSnippet(new vscode.SnippetString(snippets[0]), positions);
|
||||
});
|
||||
|
||||
context.subscriptions.push(listener);
|
||||
}
|
||||
|
||||
module.exports = { registerAutoCloseTags };
|
||||
@@ -1,4 +1,4 @@
|
||||
// WRN editor extension source hash: f74c11de70974caa6fb0cb4ee90ec39c10a924951733dadcc346b308be0e13d3
|
||||
// WRN editor extension source hash: 63bce75e2686c7586a3a08b8811ebc681d2265fdfe54e984630614e3dcef21f5
|
||||
// WRN editor extension generator hash: 456d1d614e44e5fb1f19b784176c09cf2ade9b64ef73a17934c2698150b62728
|
||||
"use strict";
|
||||
var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
|
||||
@@ -22701,10 +22701,63 @@ var require_main5 = __commonJS((exports2) => {
|
||||
}
|
||||
});
|
||||
|
||||
// editors/vscode/src/auto-close-tags.js
|
||||
var require_auto_close_tags = __commonJS((exports2, module2) => {
|
||||
var vscode = require("vscode");
|
||||
function registerAutoCloseTags(context, client) {
|
||||
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
|
||||
if (event.document.languageId !== "wrn")
|
||||
return;
|
||||
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true))
|
||||
return;
|
||||
const changes = event.contentChanges;
|
||||
if (!changes.length)
|
||||
return;
|
||||
const typed = changes[0].text;
|
||||
if (typed !== ">" && typed !== "/")
|
||||
return;
|
||||
if (!changes.every((change) => change.text === typed && change.rangeLength === 0))
|
||||
return;
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || editor.document !== event.document)
|
||||
return;
|
||||
const positions = editor.selections.map((selection) => selection.active);
|
||||
if (positions.length !== changes.length)
|
||||
return;
|
||||
if (!editor.selections.every((selection) => selection.isEmpty))
|
||||
return;
|
||||
const documentVersion = event.document.version;
|
||||
const snippets = await Promise.all(positions.map((position) => client.sendRequest("wrn/tagComplete", {
|
||||
textDocument: { uri: event.document.uri.toString() },
|
||||
position: { line: position.line, character: position.character }
|
||||
})));
|
||||
if (!snippets.every((snippet) => typeof snippet === "string" && snippet))
|
||||
return;
|
||||
if (!snippets.every((snippet) => snippet === snippets[0]))
|
||||
return;
|
||||
if (vscode.window.activeTextEditor !== editor)
|
||||
return;
|
||||
if (editor.document !== event.document)
|
||||
return;
|
||||
if (editor.document.version !== documentVersion)
|
||||
return;
|
||||
if (editor.selections.length !== positions.length)
|
||||
return;
|
||||
if (!editor.selections.every((selection, index) => selection.isEmpty && selection.active.isEqual(positions[index]))) {
|
||||
return;
|
||||
}
|
||||
await editor.insertSnippet(new vscode.SnippetString(snippets[0]), positions);
|
||||
});
|
||||
context.subscriptions.push(listener);
|
||||
}
|
||||
module2.exports = { registerAutoCloseTags };
|
||||
});
|
||||
|
||||
// editors/vscode/src/extension.js
|
||||
var path = require("node:path");
|
||||
var vscode = require("vscode");
|
||||
var { LanguageClient, TransportKind } = require_main5();
|
||||
var { registerAutoCloseTags } = require_auto_close_tags();
|
||||
var WRN_LANGUAGE_ID = "wrn";
|
||||
var client;
|
||||
async function recoverWrnLanguage(document) {
|
||||
@@ -22718,44 +22771,6 @@ async function recoverWrnLanguage(document) {
|
||||
console.warn("[wrnexus] unable to recover .wrn language association:", error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
function registerAutoCloseTags(context, client2) {
|
||||
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
|
||||
if (event.document.languageId !== "wrn")
|
||||
return;
|
||||
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true))
|
||||
return;
|
||||
if (event.contentChanges.length !== 1)
|
||||
return;
|
||||
const change = event.contentChanges[0];
|
||||
if (!change || change.text !== ">" && change.text !== "/")
|
||||
return;
|
||||
if (change.rangeLength !== 0)
|
||||
return;
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || editor.document !== event.document)
|
||||
return;
|
||||
const documentVersion = event.document.version;
|
||||
const position = change.range.start.translate(0, change.text.length);
|
||||
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position))
|
||||
return;
|
||||
const snippet = await client2.sendRequest("wrn/tagComplete", {
|
||||
textDocument: { uri: event.document.uri.toString() },
|
||||
position: { line: position.line, character: position.character }
|
||||
});
|
||||
if (typeof snippet !== "string" || !snippet)
|
||||
return;
|
||||
if (vscode.window.activeTextEditor !== editor)
|
||||
return;
|
||||
if (editor.document !== event.document)
|
||||
return;
|
||||
if (editor.document.version !== documentVersion)
|
||||
return;
|
||||
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position))
|
||||
return;
|
||||
await editor.insertSnippet(new vscode.SnippetString(snippet), position);
|
||||
});
|
||||
context.subscriptions.push(listener);
|
||||
}
|
||||
async function activate(context) {
|
||||
for (const document of vscode.workspace.textDocuments)
|
||||
recoverWrnLanguage(document);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
const path = require("node:path");
|
||||
const vscode = require("vscode");
|
||||
const { LanguageClient, TransportKind } = require("vscode-languageclient/node");
|
||||
const { registerAutoCloseTags } = require("./auto-close-tags.js");
|
||||
|
||||
const WRN_LANGUAGE_ID = "wrn";
|
||||
/** @type {LanguageClient | undefined} */
|
||||
@@ -23,56 +24,6 @@ async function recoverWrnLanguage(document) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-close tags as they are typed.
|
||||
*
|
||||
* LSP has no request for this, so the client watches document changes and asks
|
||||
* the server whether the tag should close. The server owns the decision because
|
||||
* void elements and already-closed tags must not be closed.
|
||||
*/
|
||||
function registerAutoCloseTags(context, client) {
|
||||
const listener = vscode.workspace.onDidChangeTextDocument(async (event) => {
|
||||
if (event.document.languageId !== "wrn") return;
|
||||
if (!vscode.workspace.getConfiguration("wrnexus.html").get("autoClosingTags", true)) return;
|
||||
|
||||
// Multi-cursor typing reports one change per cursor. Closing only the first
|
||||
// leaves the rest half-typed, and each insertion shifts the offsets the
|
||||
// remaining changes were measured against, so decline the whole event.
|
||||
if (event.contentChanges.length !== 1) return;
|
||||
|
||||
const change = event.contentChanges[0];
|
||||
if (!change || (change.text !== ">" && change.text !== "/")) return;
|
||||
// A replaced selection (overtype/select-and-type) makes `range.start + text.length`
|
||||
// an incorrect offset for both the query and the insertion; decline rather than guess.
|
||||
if (change.rangeLength !== 0) return;
|
||||
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
if (!editor || editor.document !== event.document) return;
|
||||
|
||||
const documentVersion = event.document.version;
|
||||
const position = change.range.start.translate(0, change.text.length);
|
||||
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position)) return;
|
||||
|
||||
const snippet = await client.sendRequest("wrn/tagComplete", {
|
||||
textDocument: { uri: event.document.uri.toString() },
|
||||
position: { line: position.line, character: position.character },
|
||||
});
|
||||
if (typeof snippet !== "string" || !snippet) return;
|
||||
|
||||
// The user may have kept typing during the round-trip; re-validate everything the
|
||||
// insertion depends on before touching the document, since a stale offset would
|
||||
// silently corrupt it.
|
||||
if (vscode.window.activeTextEditor !== editor) return;
|
||||
if (editor.document !== event.document) return;
|
||||
if (editor.document.version !== documentVersion) return;
|
||||
if (!editor.selection.isEmpty || !editor.selection.active.isEqual(position)) return;
|
||||
|
||||
await editor.insertSnippet(new vscode.SnippetString(snippet), position);
|
||||
});
|
||||
|
||||
context.subscriptions.push(listener);
|
||||
}
|
||||
|
||||
/** @param {vscode.ExtensionContext} context */
|
||||
async function activate(context) {
|
||||
for (const document of vscode.workspace.textDocuments) void recoverWrnLanguage(document);
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
"use strict";
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { test } = require("node:test");
|
||||
const { installVsCodeHost } = require("./vscode-host.js");
|
||||
|
||||
class Position {
|
||||
constructor(line, character) {
|
||||
this.line = line;
|
||||
this.character = character;
|
||||
}
|
||||
translate(lineDelta, characterDelta) {
|
||||
return new Position(this.line + lineDelta, this.character + characterDelta);
|
||||
}
|
||||
isEqual(other) {
|
||||
return this.line === other.line && this.character === other.character;
|
||||
}
|
||||
}
|
||||
|
||||
class Selection {
|
||||
constructor(active) {
|
||||
this.active = active;
|
||||
this.anchor = active;
|
||||
this.isEmpty = true;
|
||||
}
|
||||
}
|
||||
|
||||
class SnippetString {
|
||||
constructor(value) {
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
let changeListener = null;
|
||||
const host = {
|
||||
Position,
|
||||
Selection,
|
||||
SnippetString,
|
||||
workspace: {
|
||||
onDidChangeTextDocument(listener) {
|
||||
changeListener = listener;
|
||||
return { dispose() {} };
|
||||
},
|
||||
getConfiguration() {
|
||||
return { get: (_key, fallback) => fallback };
|
||||
},
|
||||
},
|
||||
window: { activeTextEditor: null },
|
||||
};
|
||||
|
||||
const restoreHost = installVsCodeHost(host);
|
||||
const { registerAutoCloseTags } = require("../src/auto-close-tags.js");
|
||||
restoreHost();
|
||||
|
||||
/**
|
||||
* Drive the handler the way VS Code does: the document has already been
|
||||
* updated and the carets moved by the time the change event fires.
|
||||
*/
|
||||
function scenario({ carets, snippetFor, typed = ">" }) {
|
||||
const inserted = [];
|
||||
const asked = [];
|
||||
const document = { languageId: "wrn", version: 1, uri: { toString: () => "file:///a.wrn" } };
|
||||
const editor = {
|
||||
document,
|
||||
selections: carets.map((caret) => new Selection(caret)),
|
||||
insertSnippet(snippet, positions) {
|
||||
inserted.push({ value: snippet.value, positions });
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
};
|
||||
editor.selection = editor.selections[0];
|
||||
host.window.activeTextEditor = editor;
|
||||
|
||||
const client = {
|
||||
sendRequest(_method, params) {
|
||||
asked.push(params.position);
|
||||
return Promise.resolve(snippetFor(params.position));
|
||||
},
|
||||
};
|
||||
|
||||
registerAutoCloseTags({ subscriptions: [] }, client);
|
||||
|
||||
return {
|
||||
inserted,
|
||||
asked,
|
||||
fire: () =>
|
||||
changeListener({
|
||||
document,
|
||||
// Pre-edit coordinates, deliberately not usable as caret positions.
|
||||
contentChanges: carets.map(() => ({
|
||||
text: typed,
|
||||
rangeLength: 0,
|
||||
range: { start: new Position(0, 0) },
|
||||
})),
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
test("closes the tag at a single caret", async () => {
|
||||
const run = scenario({ carets: [new Position(1, 8)], snippetFor: () => "$0</div>" });
|
||||
await run.fire();
|
||||
|
||||
assert.equal(run.inserted.length, 1);
|
||||
assert.equal(run.inserted[0].value, "$0</div>");
|
||||
assert.deepEqual(
|
||||
run.inserted[0].positions.map((p) => [p.line, p.character]),
|
||||
[[1, 8]],
|
||||
);
|
||||
});
|
||||
|
||||
test("closes the tag at every caret in one insertion", async () => {
|
||||
// One insertSnippet call is what keeps all the carets alive: inserting
|
||||
// sequentially would collapse the selection to the first snippet.
|
||||
const run = scenario({
|
||||
carets: [new Position(1, 8), new Position(2, 8), new Position(3, 8)],
|
||||
snippetFor: () => "$0</div>",
|
||||
});
|
||||
await run.fire();
|
||||
|
||||
assert.equal(run.asked.length, 3);
|
||||
assert.equal(run.inserted.length, 1);
|
||||
assert.deepEqual(
|
||||
run.inserted[0].positions.map((p) => [p.line, p.character]),
|
||||
[
|
||||
[1, 8],
|
||||
[2, 8],
|
||||
[3, 8],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("asks about each caret's own position rather than the change ranges", async () => {
|
||||
// Every contentChange above reports (0, 0). Using those would query and
|
||||
// insert at the wrong offsets once more than one caret is on a line.
|
||||
const run = scenario({
|
||||
carets: [new Position(4, 12), new Position(9, 3)],
|
||||
snippetFor: () => "$0</p>",
|
||||
});
|
||||
await run.fire();
|
||||
|
||||
assert.deepEqual(
|
||||
run.asked.map((p) => [p.line, p.character]),
|
||||
[
|
||||
[4, 12],
|
||||
[9, 3],
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("declines when the carets want different closing tags", async () => {
|
||||
const run = scenario({
|
||||
carets: [new Position(1, 8), new Position(2, 8)],
|
||||
snippetFor: (position) => (position.line === 1 ? "$0</div>" : "$0</span>"),
|
||||
});
|
||||
await run.fire();
|
||||
|
||||
assert.equal(run.inserted.length, 0);
|
||||
});
|
||||
|
||||
test("declines when any caret has no tag to close", async () => {
|
||||
const run = scenario({
|
||||
carets: [new Position(1, 8), new Position(2, 8)],
|
||||
snippetFor: (position) => (position.line === 1 ? "$0</br>" : null),
|
||||
});
|
||||
await run.fire();
|
||||
|
||||
assert.equal(run.inserted.length, 0);
|
||||
});
|
||||
|
||||
test("declines a replaced selection", async () => {
|
||||
const run = scenario({ carets: [new Position(1, 8)], snippetFor: () => "$0</div>" });
|
||||
await changeListener({
|
||||
document: { languageId: "wrn", version: 1, uri: { toString: () => "file:///a.wrn" } },
|
||||
contentChanges: [{ text: ">", rangeLength: 3, range: { start: new Position(1, 5) } }],
|
||||
});
|
||||
|
||||
assert.equal(run.inserted.length, 0);
|
||||
});
|
||||
@@ -2,54 +2,47 @@
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert");
|
||||
const Module = require("node:module");
|
||||
const { installVsCodeHost } = require("./vscode-host.js");
|
||||
|
||||
// Mock the vscode module for unit tests
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function load(request, parent, isMain) {
|
||||
if (request === "vscode") {
|
||||
return {
|
||||
Position: class Position {
|
||||
constructor(line, character) {
|
||||
this.line = line;
|
||||
this.character = character;
|
||||
}
|
||||
},
|
||||
Range: class Range {
|
||||
constructor(start, end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
},
|
||||
CompletionItem: class CompletionItem {
|
||||
constructor(label, kind) {
|
||||
this.label = label;
|
||||
this.kind = kind;
|
||||
}
|
||||
},
|
||||
CompletionItemKind: {
|
||||
Event: 23,
|
||||
Property: 10,
|
||||
Function: 12,
|
||||
Keyword: 14,
|
||||
Variable: 13,
|
||||
},
|
||||
SnippetString: class SnippetString {
|
||||
constructor(text) {
|
||||
this.value = text;
|
||||
}
|
||||
},
|
||||
MarkdownString: class MarkdownString {
|
||||
constructor(text) {
|
||||
this.value = text;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
const restoreHost = installVsCodeHost({
|
||||
Position: class Position {
|
||||
constructor(line, character) {
|
||||
this.line = line;
|
||||
this.character = character;
|
||||
}
|
||||
},
|
||||
Range: class Range {
|
||||
constructor(start, end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
},
|
||||
CompletionItem: class CompletionItem {
|
||||
constructor(label, kind) {
|
||||
this.label = label;
|
||||
this.kind = kind;
|
||||
}
|
||||
},
|
||||
CompletionItemKind: {
|
||||
Event: 23,
|
||||
Property: 10,
|
||||
Function: 12,
|
||||
Keyword: 14,
|
||||
Variable: 13,
|
||||
},
|
||||
SnippetString: class SnippetString {
|
||||
constructor(text) {
|
||||
this.value = text;
|
||||
}
|
||||
},
|
||||
MarkdownString: class MarkdownString {
|
||||
constructor(text) {
|
||||
this.value = text;
|
||||
}
|
||||
},
|
||||
});
|
||||
const { isInsideViewBlock, provideCompletionItems } = require("../src/completion.js");
|
||||
Module._load = originalLoad;
|
||||
restoreHost();
|
||||
|
||||
const PAGE = `page Home {
|
||||
view {
|
||||
|
||||
@@ -2,17 +2,13 @@
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { test } = require("node:test");
|
||||
const Module = require("node:module");
|
||||
const { installVsCodeHost } = require("./vscode-host.js");
|
||||
|
||||
// These extraction helpers are pure, but their module also registers VS Code
|
||||
// providers at runtime. Supply a minimal host shim for unit tests.
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function load(request, parent, isMain) {
|
||||
if (request === "vscode") return {};
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
const restoreHost = installVsCodeHost({});
|
||||
const { extractRouteParams, extractStates } = require("../src/completion");
|
||||
Module._load = originalLoad;
|
||||
restoreHost();
|
||||
|
||||
test("extracts dynamic route params from filename", () => {
|
||||
const document = {
|
||||
|
||||
@@ -2,30 +2,24 @@
|
||||
|
||||
const assert = require("node:assert");
|
||||
const { test } = require("node:test");
|
||||
const Module = require("node:module");
|
||||
const { installVsCodeHost } = require("./vscode-host.js");
|
||||
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function load(request, parent, isMain) {
|
||||
if (request === "vscode") {
|
||||
return {
|
||||
Diagnostic: class Diagnostic {
|
||||
constructor(range, message, severity) {
|
||||
this.range = range;
|
||||
this.message = message;
|
||||
this.severity = severity;
|
||||
}
|
||||
},
|
||||
DiagnosticSeverity: { Error: 0, Warning: 1 },
|
||||
Range: class Range {
|
||||
constructor(start, end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
const restoreHost = installVsCodeHost({
|
||||
Diagnostic: class Diagnostic {
|
||||
constructor(range, message, severity) {
|
||||
this.range = range;
|
||||
this.message = message;
|
||||
this.severity = severity;
|
||||
}
|
||||
},
|
||||
DiagnosticSeverity: { Error: 0, Warning: 1 },
|
||||
Range: class Range {
|
||||
constructor(start, end) {
|
||||
this.start = start;
|
||||
this.end = end;
|
||||
}
|
||||
},
|
||||
});
|
||||
const {
|
||||
findTopLevelDeclaration,
|
||||
maskLeadingTrivia,
|
||||
@@ -34,7 +28,7 @@ const {
|
||||
validateLayoutUsage,
|
||||
validateRootMembers,
|
||||
} = require("../src/diagnostics");
|
||||
Module._load = originalLoad;
|
||||
restoreHost();
|
||||
|
||||
function mockDocument() {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Supply a stub `vscode` host so extension sources can be unit tested.
|
||||
*
|
||||
* These files run under `node --test` (see the package's test script), where
|
||||
* patching `Module._load` is enough. A bare `bun test` from the repository
|
||||
* root also picks them up by filename, and Bun resolves `require` through its
|
||||
* own resolver without consulting `Module._load` -- so under Bun the same
|
||||
* files failed with "Cannot find package 'vscode'". Registering a virtual
|
||||
* module covers that case, leaving one shim that works under both runners.
|
||||
*
|
||||
* Returns a function restoring the original loader.
|
||||
*/
|
||||
function installVsCodeHost(stub) {
|
||||
const Module = require("node:module");
|
||||
|
||||
if (typeof Bun !== "undefined") {
|
||||
require("bun").plugin({
|
||||
name: "vscode-host-stub",
|
||||
setup(build) {
|
||||
build.module("vscode", () => ({ exports: stub, loader: "object" }));
|
||||
},
|
||||
});
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const originalLoad = Module._load;
|
||||
Module._load = function load(request, parent, isMain) {
|
||||
if (request === "vscode") return stub;
|
||||
return originalLoad.call(this, request, parent, isMain);
|
||||
};
|
||||
|
||||
return () => {
|
||||
Module._load = originalLoad;
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = { installVsCodeHost };
|
||||
Reference in New Issue
Block a user