From 7fbc2228a17453ea7df4e91807b6b30d3280ea0a Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 24 Mar 2026 15:43:40 -0700 Subject: [PATCH 01/16] save initial progress --- source/vscode/build.mjs | 138 ++++++++++++++++++++++++--------- source/vscode/package.json | 7 +- source/vscode/src/telemetry.ts | 6 ++ 3 files changed, 112 insertions(+), 39 deletions(-) diff --git a/source/vscode/build.mjs b/source/vscode/build.mjs index 29d8d12c5d..60f3cb19b0 100644 --- a/source/vscode/build.mjs +++ b/source/vscode/build.mjs @@ -12,24 +12,17 @@ const thisDir = dirname(fileURLToPath(import.meta.url)); const libsDir = join(thisDir, "..", "..", "node_modules"); /** @type {import("esbuild").BuildOptions} */ -const buildOptions = { +const commonBuildOptions = { entryPoints: [ join(thisDir, "src", "extension.ts"), join(thisDir, "src", "compilerWorker.ts"), join(thisDir, "src", "debugger/debug-service-worker.ts"), - join(thisDir, "src", "webview/webview.tsx"), - join(thisDir, "src", "webview/editor.tsx"), ], - outdir: join(thisDir, "out"), bundle: true, - // minify: true, - mainFields: ["browser", "module", "main"], external: ["vscode"], format: "cjs", - platform: "browser", target: ["es2020"], sourcemap: "linked", - //logLevel: "debug", define: { "import.meta.url": "undefined" }, }; @@ -57,15 +50,14 @@ const inlineStateComputeWorkerPlugin = { ); const result = await esbuildBuild({ + ...commonBuildOptions, entryPoints: [workerEntry], bundle: true, write: false, platform: "browser", // Blob workers are classic scripts by default (not ESM), so emit an IIFE. format: "iife", - target: buildOptions.target, sourcemap: false, - define: buildOptions.define, logLevel: "silent", }); @@ -81,29 +73,25 @@ const inlineStateComputeWorkerPlugin = { }, }; -function getTimeStr() { - const now = new Date(); - - const hh = now.getHours().toString().padStart(2, "0"); - const mm = now.getMinutes().toString().padStart(2, "0"); - const ss = now.getSeconds().toString().padStart(2, "0"); - const mil = now.getMilliseconds().toString().padStart(3, "0"); - - return `${hh}:${mm}:${ss}.${mil}`; -} - -export function copyWasmToVsCode() { +/** + * + * @param {string} [platform] + */ +export function copyWasmToVsCode(platform) { + if (platform !== "browser" && platform !== "node") { + throw new Error(`Invalid platform: ${platform}`); + } // Copy the wasm module into the extension directory - let qsharpWasm = join( + const qsharpWasm = join( thisDir, "..", "npm", "qsharp", "lib", - "web", + platform === "browser" ? "web" : "nodejs", "qsc_wasm_bg.wasm", ); - let qsharpDest = join(thisDir, `wasm`); + const qsharpDest = join(thisDir, platform, `wasm`); console.log("Copying the wasm file to VS Code from: " + qsharpWasm); mkdirSync(qsharpDest, { recursive: true }); @@ -115,8 +103,8 @@ export function copyWasmToVsCode() { * @param {string} [destDir] */ export function copyKatex(destDir) { - let katexBase = join(libsDir, `katex/dist`); - let katexDest = destDir ?? join(thisDir, `out/katex`); + const katexBase = join(libsDir, `katex/dist`); + const katexDest = destDir ?? join(thisDir, `out/katex`); console.log("Copying the Katex files over from: " + katexBase); mkdirSync(katexDest, { recursive: true }); @@ -167,15 +155,80 @@ export function copyKatex(destDir) { } } -function buildBundle() { - console.log("Running esbuild"); +/** + * @param {boolean} [onlyUI] + * @param {string} [platform] + * @returns {import("esbuild").BuildOptions} + */ +function getBuildOptions(onlyUI, platform) { + if (onlyUI) { + return { + ...commonBuildOptions, + platform: "browser", + outdir: join(thisDir, "out", "browser"), + entryPoints: [ + join(thisDir, "src", "webview/webview.tsx"), + join(thisDir, "src", "webview/editor.tsx"), + ], + plugins: [inlineStateComputeWorkerPlugin], + }; + } else if (platform === "browser") { + return { + ...commonBuildOptions, + platform: "browser", + outdir: join(thisDir, "out", "browser"), + }; + } else if (platform === "node") { + return { + ...commonBuildOptions, + platform: "node", + outdir: join(thisDir, "out", "nodejs"), + }; + } else { + throw new Error(`Invalid platform: ${platform}`); + } +} - esbuildBuild({ - ...buildOptions, - plugins: [inlineStateComputeWorkerPlugin], - }).then(() => console.log(`Built bundle to ${join(thisDir, "out")}`)); +/** + * @param {boolean} [onlyUI] + * @param {string} [platform] + */ +function buildBundle(onlyUI, platform) { + console.log("Running esbuild for platform: " + platform); + const buildOptions = getBuildOptions(onlyUI, platform); + esbuildBuild(buildOptions) + .catch((err) => { + console.error("Build failed:", err); + process.exit(1); + }) + .then(() => console.log(`Built bundle to ${buildOptions.outdir}`)); +} + +function buildUI() { + copyKatex(); + buildBundle(true, "browser"); +} + +/** + * @param {string} [platform] + */ +function buildExtensionHost(platform) { + copyWasmToVsCode(platform); + buildBundle(false, platform); +} + +function getTimeStr() { + const now = new Date(); + + const hh = now.getHours().toString().padStart(2, "0"); + const mm = now.getMinutes().toString().padStart(2, "0"); + const ss = now.getSeconds().toString().padStart(2, "0"); + const mil = now.getMilliseconds().toString().padStart(3, "0"); + + return `${hh}:${mm}:${ss}.${mil}`; } +// This only watches for platform = "browser" for the sake of simplicity, so make sure to run a full build first to catch any errors in the node build before pushing code changes export async function watchVsCode() { console.log("Building vscode extension in watch mode"); @@ -192,8 +245,17 @@ export async function watchVsCode() { ); }, }; - let ctx = await context({ - ...buildOptions, + const ctx = await context({ + ...commonBuildOptions, + entryPoints: [ + join(thisDir, "src", "extension.ts"), + join(thisDir, "src", "compilerWorker.ts"), + join(thisDir, "src", "debugger/debug-service-worker.ts"), + join(thisDir, "src", "webview/webview.tsx"), + join(thisDir, "src", "webview/editor.tsx"), + ], + platform: "browser", + outdir: join(thisDir, "out", "web"), plugins: [inlineStateComputeWorkerPlugin, buildPlugin], color: false, }); @@ -208,8 +270,8 @@ if (thisFilePath === resolve(process.argv[1])) { if (isWatch) { watchVsCode(); } else { - copyWasmToVsCode(); - copyKatex(); - buildBundle(); + buildUI(); + buildExtensionHost("browser"); + buildExtensionHost("node"); } } diff --git a/source/vscode/package.json b/source/vscode/package.json index dbc1337adc..50c9122ab5 100644 --- a/source/vscode/package.json +++ b/source/vscode/package.json @@ -17,7 +17,8 @@ "Programming Languages", "Notebooks" ], - "browser": "./out/extension.js", + "browser": "./out/browser/extension.js", + "main": "./out/main/extension.js", "virtualWorkspaces": true, "activationEvents": [ "onNotebook:jupyter-notebook", @@ -27,6 +28,10 @@ "onFileSystem:qsharp-vfs", "onWebviewPanel:qsharp-webview" ], + "extensionKind": [ + "workspace", + "ui" + ], "contributes": { "walkthroughs": [ { diff --git a/source/vscode/src/telemetry.ts b/source/vscode/src/telemetry.ts index 4a50fb2384..ad1383cc14 100644 --- a/source/vscode/src/telemetry.ts +++ b/source/vscode/src/telemetry.ts @@ -402,6 +402,9 @@ export function sendTelemetryEvent( } function getBrowserRelease(): string { + if (typeof navigator === "undefined") { + return `Node/${process.versions.node}`; + } if (navigator.userAgentData?.brands) { const browser = navigator.userAgentData.brands[navigator.userAgentData.brands.length - 1]; @@ -412,6 +415,9 @@ function getBrowserRelease(): string { } export function getUserAgent(): string { + if (typeof navigator === "undefined") { + return userAgentString || `Node/${process.versions.node}`; + } return userAgentString || navigator.userAgent; } From 2050f1aa74a7e4f1794b226a171102adec61cf33 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Mon, 30 Mar 2026 16:19:02 -0700 Subject: [PATCH 02/16] unify common exports in single file --- source/npm/qsharp/src/browser.ts | 34 +++---------------------- source/npm/qsharp/src/common-exports.ts | 28 ++++++++++++++++++++ source/npm/qsharp/src/main.ts | 6 +---- 3 files changed, 33 insertions(+), 35 deletions(-) create mode 100644 source/npm/qsharp/src/common-exports.ts diff --git a/source/npm/qsharp/src/browser.ts b/source/npm/qsharp/src/browser.ts index 83b7a117d1..62faf42d7a 100644 --- a/source/npm/qsharp/src/browser.ts +++ b/source/npm/qsharp/src/browser.ts @@ -28,15 +28,11 @@ import { ILanguageServiceWorker, QSharpLanguageService, languageServiceProtocol, - qsharpGithubUriScheme, - qsharpLibraryUriScheme, } from "./language-service/language-service.js"; -import { LogLevel, log } from "./log.js"; +import { log } from "./log.js"; import { ProjectLoader } from "./project.js"; import { createProxy } from "./workers/browser.js"; -export { qsharpGithubUriScheme, qsharpLibraryUriScheme }; - // Create once. A module is stateless and can be efficiently passed to WebWorkers. let wasmModule: WebAssembly.Module | null = null; let wasmModulePromise: Promise | null = null; @@ -190,31 +186,9 @@ export type { IStackFrame, IStructStepResult, ITestDescriptor, - IVariable, - IVariableChild, IWorkspaceEdit, VSDiagnostic, } from "../lib/web/qsc_wasm.js"; -export { type Dump, type ShotResult } from "./compiler/common.js"; -export { type CompilerState, type ProgramConfig } from "./compiler/compiler.js"; -export { QscEventTarget } from "./compiler/events.js"; -export { QdkDiagnostics } from "./diagnostics.js"; -export type { - LanguageServiceDiagnosticEvent, - LanguageServiceEvent, - LanguageServiceTestCallablesEvent, -} from "./language-service/language-service.js"; -export { default as openqasm_samples } from "./openqasm-samples.generated.js"; -export type { ProjectLoader } from "./project.js"; -export { default as samples } from "./samples.generated.js"; -export type { CircuitGroup as CircuitData } from "./data-structures/circuit.js"; -export * as utils from "./utils.js"; -export { log, type LogLevel, type ProjectType, type TargetProfile }; -export type { - ICompiler, - ICompilerWorker, - IDebugService, - IDebugServiceWorker, - ILanguageService, - ILanguageServiceWorker, -}; +export type { ProjectType, TargetProfile }; + +export * from "./common-exports.js"; diff --git a/source/npm/qsharp/src/common-exports.ts b/source/npm/qsharp/src/common-exports.ts new file mode 100644 index 0000000000..3230988d37 --- /dev/null +++ b/source/npm/qsharp/src/common-exports.ts @@ -0,0 +1,28 @@ +export type { IVariable, IVariableChild } from "../lib/web/qsc_wasm.js"; +export * as utils from "./utils.js"; +export { log } from "./log.js"; +export { QscEventTarget } from "./compiler/events.js"; +export { QdkDiagnostics } from "./diagnostics.js"; +export { default as samples } from "./samples.generated.js"; +export { default as openqasm_samples } from "./openqasm-samples.generated.js"; +export { + qsharpGithubUriScheme, + qsharpLibraryUriScheme, +} from "./language-service/language-service.js"; +export type { Dump, ShotResult } from "./compiler/common.js"; +export type { CompilerState, ProgramConfig } from "./compiler/compiler.js"; +export type { ICompiler, ICompilerWorker } from "./compiler/compiler.js"; +export type { + IDebugService, + IDebugServiceWorker, +} from "./debug-service/debug-service.js"; +export type { + ILanguageService, + ILanguageServiceWorker, + LanguageServiceDiagnosticEvent, + LanguageServiceEvent, + LanguageServiceTestCallablesEvent, +} from "./language-service/language-service.js"; +export type { ProjectLoader } from "./project.js"; +export type { CircuitGroup as CircuitData } from "./data-structures/circuit.js"; +export type { LogLevel } from "./log.js"; diff --git a/source/npm/qsharp/src/main.ts b/source/npm/qsharp/src/main.ts index 2256270dcf..93bba99432 100644 --- a/source/npm/qsharp/src/main.ts +++ b/source/npm/qsharp/src/main.ts @@ -22,15 +22,12 @@ import { ILanguageServiceWorker, QSharpLanguageService, languageServiceProtocol, - qsharpLibraryUriScheme, } from "./language-service/language-service.js"; import { log } from "./log.js"; import { createProxy } from "./workers/node.js"; import { ProjectLoader } from "./project.js"; import type { IProjectHost } from "./browser.js"; -export { qsharpLibraryUriScheme }; - // Only load the Wasm module when first needed, as it may only be used in a Worker, // and not in the main thread. @@ -91,5 +88,4 @@ export function getLanguageServiceWorker(): ILanguageServiceWorker { ); } -export * as utils from "./utils.js"; -export type { IVariable, IVariableChild } from "./browser.js"; +export * from "./common-exports.js"; From adf2adfe4691baaddaeca73b78849f36475a0e16 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Thu, 2 Apr 2026 17:34:29 -0700 Subject: [PATCH 03/16] first complete working version --- source/npm/qsharp/package.json | 18 ++- source/npm/qsharp/src/compiler/worker-node.ts | 2 +- .../qsharp/src/debug-service/worker-node.ts | 2 +- source/npm/qsharp/src/main.ts | 136 +++++++++++++----- source/npm/qsharp/src/workers/node.ts | 20 +-- source/vscode/build.mjs | 36 ++--- source/vscode/package.json | 2 +- source/vscode/src/circuit.ts | 3 +- source/vscode/src/common.ts | 3 +- source/vscode/src/compilerWorker.ts | 6 +- source/vscode/src/debugger/activate.ts | 4 +- .../src/debugger/debug-service-worker.ts | 6 +- source/vscode/src/documentation.ts | 4 +- source/vscode/src/qirGeneration.ts | 4 +- source/vscode/src/telemetry.ts | 4 +- source/vscode/src/webviewPanel.ts | 4 +- 16 files changed, 179 insertions(+), 75 deletions(-) diff --git a/source/npm/qsharp/package.json b/source/npm/qsharp/package.json index c3bb4400d2..f4bf3a21a8 100644 --- a/source/npm/qsharp/package.json +++ b/source/npm/qsharp/package.json @@ -17,9 +17,21 @@ "node": "./dist/main.js", "default": "./dist/browser.js" }, - "./compiler-worker": "./dist/compiler/worker-browser.js", - "./language-service-worker": "./dist/language-service/worker-browser.js", - "./debug-service-worker": "./dist/debug-service/worker-browser.js", + "./compiler-worker": { + "browser": "./dist/compiler/worker-browser.js", + "node": "./dist/compiler/worker-node.js", + "default": "./dist/compiler/worker-browser.js" + }, + "./language-service-worker": { + "browser": "./dist/language-service/worker-browser.js", + "node": "./dist/language-service/worker-node.js", + "default": "./dist/language-service/worker-browser.js" + }, + "./debug-service-worker": { + "browser": "./dist/debug-service/worker-browser.js", + "node": "./dist/debug-service/worker-node.js", + "default": "./dist/debug-service/worker-browser.js" + }, "./katas": "./dist/katas.js", "./katas-md": "./dist/katas-md.js", "./state-viz": "./ux/circuit-vis/state-viz/worker/index.ts", diff --git a/source/npm/qsharp/src/compiler/worker-node.ts b/source/npm/qsharp/src/compiler/worker-node.ts index 218eb3d303..36ce386eb2 100644 --- a/source/npm/qsharp/src/compiler/worker-node.ts +++ b/source/npm/qsharp/src/compiler/worker-node.ts @@ -4,4 +4,4 @@ import { createWorker } from "../workers/node.js"; import { compilerProtocol } from "./compiler.js"; -createWorker(compilerProtocol); +export const messageHandler = createWorker(compilerProtocol); diff --git a/source/npm/qsharp/src/debug-service/worker-node.ts b/source/npm/qsharp/src/debug-service/worker-node.ts index 6ec5e3b4b0..4992e94ffc 100644 --- a/source/npm/qsharp/src/debug-service/worker-node.ts +++ b/source/npm/qsharp/src/debug-service/worker-node.ts @@ -4,4 +4,4 @@ import { createWorker } from "../workers/node.js"; import { debugServiceProtocol } from "./debug-service.js"; -createWorker(debugServiceProtocol); +export const messageHandler = createWorker(debugServiceProtocol); diff --git a/source/npm/qsharp/src/main.ts b/source/npm/qsharp/src/main.ts index 93bba99432..86b90b4ad1 100644 --- a/source/npm/qsharp/src/main.ts +++ b/source/npm/qsharp/src/main.ts @@ -4,7 +4,8 @@ // This module is the main entry point for use in Node.js environments. For browser environments, // the "./browser.js" file is the entry point module. -import { createRequire } from "node:module"; +import * as wasm from "../lib/web/qsc_wasm.js"; +import initWasm from "../lib/web/qsc_wasm.js"; import { Compiler, ICompiler, @@ -17,6 +18,7 @@ import { QSharpDebugService, debugServiceProtocol, } from "./debug-service/debug-service.js"; +import { callAndTransformExceptions } from "./diagnostics.js"; import { ILanguageService, ILanguageServiceWorker, @@ -28,64 +30,128 @@ import { createProxy } from "./workers/node.js"; import { ProjectLoader } from "./project.js"; import type { IProjectHost } from "./browser.js"; -// Only load the Wasm module when first needed, as it may only be used in a Worker, -// and not in the main thread. +// Create once. A module is stateless and can be efficiently passed to WebWorkers. +let wasmModule: WebAssembly.Module; +let wasmModulePromise: Promise | null = null; -// Use the types from the web version for... reasons. -type Wasm = typeof import("../lib/web/qsc_wasm.js"); -let wasm: Wasm | null = null; +// Used to track if an instance is already instantiated +let wasmInstancePromise: Promise | null = null; -function ensureWasm() { - if (!wasm) { - wasm = require("../lib/nodejs/qsc_wasm.cjs") as Wasm; - // Set up logging and telemetry as soon as possible after instantiating - wasm.initLogging(log.logWithLevel, log.getLogLevel()); - log.onLevelChanged = (level) => wasm?.setLogLevel(level); +async function wasmLoader(uriOrBuffer: string | ArrayBuffer) { + if (typeof uriOrBuffer === "string") { + log.info("Fetching wasm module from %s", uriOrBuffer); + performance.mark("fetch-wasm-start"); + const wasmRequst = await fetch(uriOrBuffer); + const wasmBuffer = await wasmRequst.arrayBuffer(); + const fetchTiming = performance.measure("fetch-wasm", "fetch-wasm-start"); + log.logTelemetry({ + id: "fetch-wasm", + data: { + duration: fetchTiming.duration, + uri: uriOrBuffer, + }, + }); + + wasmModule = await WebAssembly.compile(wasmBuffer); + } else { + log.info("Compiling wasm module from provided buffer"); + wasmModule = await WebAssembly.compile(uriOrBuffer); + } +} + +export function loadWasmModule( + uriOrBuffer: string | ArrayBuffer, +): Promise { + // Only initiate if not already in flight, to avoid race conditions + if (!wasmModulePromise) { + wasmModulePromise = wasmLoader(uriOrBuffer); } + return wasmModulePromise; } -const require = createRequire(import.meta.url); +async function instantiateWasm() { + // Ensure loading the module has been initiated, and wait for it. + if (!wasmModulePromise) throw "Wasm module must be loaded first"; + await wasmModulePromise; + if (!wasmModule) throw "Wasm module failed to load"; + + if (wasmInstancePromise) { + // Either in flight or already complete. The prior request will do the init, + // so just wait on that. + await wasmInstancePromise; + return; + } + + // Set the promise to signal this is in flight, then wait on the result. + wasmInstancePromise = initWasm(wasmModule); + await wasmInstancePromise; + + // Once ready, set up logging and telemetry as soon as possible after instantiating + wasm.initLogging(log.logWithLevel, log.getLogLevel()); + log.onLevelChanged = (level) => wasm.setLogLevel(level); +} export async function getLibrarySourceContent( path: string, ): Promise { - ensureWasm(); - return wasm!.get_library_source_content(path); + await instantiateWasm(); + return wasm.get_library_source_content(path); +} + +export async function getCompiler(): Promise { + await instantiateWasm(); + return new Compiler(wasm); } -export function getCompiler(): ICompiler { - ensureWasm(); - return new Compiler(wasm!); +export async function getProjectLoader( + host: IProjectHost, +): Promise { + await instantiateWasm(); + return new ProjectLoader(wasm, host); } -export function getProjectLoader(host: IProjectHost): ProjectLoader { - ensureWasm(); - return new ProjectLoader(wasm!, host); +export function getCompilerWorker(worker: string): ICompilerWorker { + return createProxy(worker, wasmModule, compilerProtocol); } -export function getCompilerWorker(): ICompilerWorker { - return createProxy("../compiler/worker-node.js", compilerProtocol); +export async function getDebugService(): Promise { + await instantiateWasm(); + return new QSharpDebugService(wasm); } -export function getDebugService(): IDebugService { - ensureWasm(); - return new QSharpDebugService(wasm!); +export function getDebugServiceWorker(worker: string): IDebugServiceWorker { + return createProxy(worker, wasmModule, debugServiceProtocol); } -export function getDebugServiceWorker(): IDebugServiceWorker { - return createProxy("../debug-service/worker-node.js", debugServiceProtocol); +export async function getLanguageService( + host?: IProjectHost, +): Promise { + await instantiateWasm(); + return new QSharpLanguageService(wasm, host); } -export function getLanguageService(host?: IProjectHost): ILanguageService { - ensureWasm(); - return new QSharpLanguageService(wasm!, host); +export async function getLanguageServiceWorker( + worker: string, +): Promise { + return createProxy(worker, wasmModule, languageServiceProtocol); } -export function getLanguageServiceWorker(): ILanguageServiceWorker { - return createProxy( - "../language-service/worker-node.js", - languageServiceProtocol, +/// Extracts the target profile from a Q# source file's entry point. +/// Scans the provided source code for an EntryPoint argument specifying +/// a profile and returns the corresponding TargetProfile value, if found. +/// Returns undefined if no profile is specified or if the profile is not recognized. +export async function getTargetProfileFromEntryPoint( + fileName: string, + source: string, +): Promise { + await instantiateWasm(); + return callAndTransformExceptions( + async () => + wasm.get_target_profile_from_entry_point(fileName, source) as + | wasm.TargetProfile + | undefined, ); } +export { StepResultId } from "../lib/web/qsc_wasm.js"; export * from "./common-exports.js"; diff --git a/source/npm/qsharp/src/workers/node.ts b/source/npm/qsharp/src/workers/node.ts index f7be988fff..10967b9e95 100644 --- a/source/npm/qsharp/src/workers/node.ts +++ b/source/npm/qsharp/src/workers/node.ts @@ -9,7 +9,8 @@ import { parentPort, workerData, } from "node:worker_threads"; -import * as wasm from "../../lib/nodejs/qsc_wasm.cjs"; +import * as wasm from "../../lib/web/qsc_wasm.js"; +import { initSync } from "../../lib/web/qsc_wasm.js"; import { log } from "../log.js"; import { IServiceEventMessage, @@ -29,21 +30,21 @@ import { export function createWorker< TService extends ServiceMethods, TServiceEventMsg extends IServiceEventMessage, ->(protocol: ServiceProtocol): void { +>(protocol: ServiceProtocol): (data: any) => void { if (isMainThread) throw "Worker script should be loaded in a Worker thread only"; const port = parentPort!; + const { wasmModule, qscLogLevel } = workerData || {}; + initSync(wasmModule); const postMessage = port.postMessage.bind(port); const invokeService = initService( postMessage, protocol, - wasm as any, // Need to cast due to difference in web and node wasm types - workerData && typeof workerData.qscLogLevel === "number" - ? workerData.qscLogLevel - : undefined, + wasm, + qscLogLevel === "number" ? workerData.qscLogLevel : undefined, ); function messageHandler(data: any) { @@ -56,6 +57,7 @@ export function createWorker< } port.addListener("message", messageHandler); + return messageHandler; } /** @@ -74,11 +76,11 @@ export function createProxy< TServiceEventMsg extends IServiceEventMessage, >( workerArg: string, + wasmModule: WebAssembly.Module, serviceProtocol: ServiceProtocol, ): TService & IServiceProxy { - const thisDir = dirname(fileURLToPath(import.meta.url)); - const worker = new Worker(join(thisDir, workerArg), { - workerData: { qscLogLevel: log.getLogLevel() }, + const worker = new Worker(new URL(workerArg), { + workerData: { wasmModule, qscLogLevel: log.getLogLevel() }, }); // Create the proxy which will forward method calls to the worker diff --git a/source/vscode/build.mjs b/source/vscode/build.mjs index 60f3cb19b0..d7f69fcdd3 100644 --- a/source/vscode/build.mjs +++ b/source/vscode/build.mjs @@ -23,7 +23,10 @@ const commonBuildOptions = { format: "cjs", target: ["es2020"], sourcemap: "linked", - define: { "import.meta.url": "undefined" }, + define: { + "import.meta.url": "undefined", + __PLATFORM_DIR__: JSON.stringify("browser"), + }, }; /** @type {import("esbuild").Plugin} */ @@ -73,14 +76,7 @@ const inlineStateComputeWorkerPlugin = { }, }; -/** - * - * @param {string} [platform] - */ -export function copyWasmToVsCode(platform) { - if (platform !== "browser" && platform !== "node") { - throw new Error(`Invalid platform: ${platform}`); - } +export function copyWasmToVsCode() { // Copy the wasm module into the extension directory const qsharpWasm = join( thisDir, @@ -88,12 +84,13 @@ export function copyWasmToVsCode(platform) { "npm", "qsharp", "lib", - platform === "browser" ? "web" : "nodejs", + "web", "qsc_wasm_bg.wasm", ); - const qsharpDest = join(thisDir, platform, `wasm`); + const qsharpDest = join(thisDir, "wasm"); console.log("Copying the wasm file to VS Code from: " + qsharpWasm); + console.log("Destination: " + qsharpDest); mkdirSync(qsharpDest, { recursive: true }); copyFileSync(qsharpWasm, join(qsharpDest, "qsc_wasm_bg.wasm")); } @@ -165,7 +162,7 @@ function getBuildOptions(onlyUI, platform) { return { ...commonBuildOptions, platform: "browser", - outdir: join(thisDir, "out", "browser"), + outdir: join(thisDir, "out", "webview"), entryPoints: [ join(thisDir, "src", "webview/webview.tsx"), join(thisDir, "src", "webview/editor.tsx"), @@ -175,14 +172,21 @@ function getBuildOptions(onlyUI, platform) { } else if (platform === "browser") { return { ...commonBuildOptions, - platform: "browser", + platform, outdir: join(thisDir, "out", "browser"), }; } else if (platform === "node") { return { ...commonBuildOptions, - platform: "node", - outdir: join(thisDir, "out", "nodejs"), + platform, + outdir: join(thisDir, "out", "node"), + banner: { + js: 'const _importMetaUrl = require("url").pathToFileURL(__filename).href;', + }, + define: { + "import.meta.url": "_importMetaUrl", + __PLATFORM_DIR__: JSON.stringify("node"), + }, }; } else { throw new Error(`Invalid platform: ${platform}`); @@ -213,7 +217,6 @@ function buildUI() { * @param {string} [platform] */ function buildExtensionHost(platform) { - copyWasmToVsCode(platform); buildBundle(false, platform); } @@ -271,6 +274,7 @@ if (thisFilePath === resolve(process.argv[1])) { watchVsCode(); } else { buildUI(); + copyWasmToVsCode(); buildExtensionHost("browser"); buildExtensionHost("node"); } diff --git a/source/vscode/package.json b/source/vscode/package.json index 50c9122ab5..eec77c80c3 100644 --- a/source/vscode/package.json +++ b/source/vscode/package.json @@ -18,7 +18,7 @@ "Notebooks" ], "browser": "./out/browser/extension.js", - "main": "./out/main/extension.js", + "main": "./out/node/extension.js", "virtualWorkspaces": true, "activationEvents": [ "onNotebook:jupyter-notebook", diff --git a/source/vscode/src/circuit.ts b/source/vscode/src/circuit.ts index 5e71a2df77..9ae1bc7db1 100644 --- a/source/vscode/src/circuit.ts +++ b/source/vscode/src/circuit.ts @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +declare const __PLATFORM_DIR__: string; import { escapeHtml } from "markdown-it/lib/common/utils.mjs"; import { @@ -280,7 +281,7 @@ export async function getCircuitOrErrorWithTimeout( const compilerWorkerScriptPath = Uri.joinPath( extensionUri, - "./out/compilerWorker.js", + `./out/${__PLATFORM_DIR__}/compilerWorker.js`, ).toString(); const worker = getCompilerWorker(compilerWorkerScriptPath); diff --git a/source/vscode/src/common.ts b/source/vscode/src/common.ts index 0ae2e14e56..bc8c887bc8 100644 --- a/source/vscode/src/common.ts +++ b/source/vscode/src/common.ts @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +declare const __PLATFORM_DIR__: string; import { TextDocument, Uri, Range, Location } from "vscode"; import { @@ -147,7 +148,7 @@ export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { export function loadCompilerWorker(extensionUri: vscode.Uri): ICompilerWorker { const compilerWorkerScriptPath = vscode.Uri.joinPath( extensionUri, - "./out/compilerWorker.js", + `./out/${__PLATFORM_DIR__}/compilerWorker.js`, ).toString(); return getCompilerWorker(compilerWorkerScriptPath); } diff --git a/source/vscode/src/compilerWorker.ts b/source/vscode/src/compilerWorker.ts index 9659220212..b77c05af9d 100644 --- a/source/vscode/src/compilerWorker.ts +++ b/source/vscode/src/compilerWorker.ts @@ -1,6 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +declare const __PLATFORM_DIR__: string; + import { messageHandler } from "qsharp-lang/compiler-worker"; -self.onmessage = messageHandler; +if (__PLATFORM_DIR__ === "browser") { + self.onmessage = messageHandler; +} diff --git a/source/vscode/src/debugger/activate.ts b/source/vscode/src/debugger/activate.ts index 3143ccb8f9..a656a2c33b 100644 --- a/source/vscode/src/debugger/activate.ts +++ b/source/vscode/src/debugger/activate.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +declare const __PLATFORM_DIR__: string; + /* eslint-disable @typescript-eslint/no-unused-vars */ import { IDebugServiceWorker, getDebugServiceWorker, log } from "qsharp-lang"; @@ -22,7 +24,7 @@ export async function activateDebugger( ): Promise { const debugWorkerScriptPath = vscode.Uri.joinPath( context.extensionUri, - "./out/debugger/debug-service-worker.js", + `./out/${__PLATFORM_DIR__}/debugger/debug-service-worker.js`, ); debugServiceWorkerFactory = () => diff --git a/source/vscode/src/debugger/debug-service-worker.ts b/source/vscode/src/debugger/debug-service-worker.ts index 80fd66c4fd..f74912fdf7 100644 --- a/source/vscode/src/debugger/debug-service-worker.ts +++ b/source/vscode/src/debugger/debug-service-worker.ts @@ -1,6 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +declare const __PLATFORM_DIR__: string; + import { messageHandler } from "qsharp-lang/debug-service-worker"; -self.onmessage = messageHandler; +if (__PLATFORM_DIR__ === "browser") { + self.onmessage = messageHandler; +} diff --git a/source/vscode/src/documentation.ts b/source/vscode/src/documentation.ts index b644aa00c3..72b45a2d82 100644 --- a/source/vscode/src/documentation.ts +++ b/source/vscode/src/documentation.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +declare const __PLATFORM_DIR__: string; + import { getCompilerWorker, IDocFile } from "qsharp-lang"; import { Uri } from "vscode"; import { sendMessageToPanel } from "./webviewPanel"; @@ -22,7 +24,7 @@ export async function showDocumentationCommand(extensionUri: Uri) { // Get API documentation from compiler. const compilerWorkerScriptPath = Uri.joinPath( extensionUri, - "./out/compilerWorker.js", + `./out/${__PLATFORM_DIR__}/compilerWorker.js`, ).toString(); const worker = getCompilerWorker(compilerWorkerScriptPath); const docFiles = await worker.getDocumentation(program.programConfig); diff --git a/source/vscode/src/qirGeneration.ts b/source/vscode/src/qirGeneration.ts index 20f9ddea48..9294bad451 100644 --- a/source/vscode/src/qirGeneration.ts +++ b/source/vscode/src/qirGeneration.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +declare const __PLATFORM_DIR__: string; + import { getCompilerWorker, log, @@ -208,7 +210,7 @@ async function getQirForActiveWindowCommand() { export function initCodegen(context: vscode.ExtensionContext) { compilerWorkerScriptPath = vscode.Uri.joinPath( context.extensionUri, - "./out/compilerWorker.js", + `./out/${__PLATFORM_DIR__}/compilerWorker.js`, ).toString(); context.subscriptions.push( diff --git a/source/vscode/src/telemetry.ts b/source/vscode/src/telemetry.ts index ad1383cc14..e24ce3b2fb 100644 --- a/source/vscode/src/telemetry.ts +++ b/source/vscode/src/telemetry.ts @@ -3,6 +3,8 @@ /// +declare const __PLATFORM_DIR__: string; + import * as vscode from "vscode"; import TelemetryReporter from "@vscode/extension-telemetry"; import { log } from "qsharp-lang"; @@ -402,7 +404,7 @@ export function sendTelemetryEvent( } function getBrowserRelease(): string { - if (typeof navigator === "undefined") { + if (__PLATFORM_DIR__ === "node") { return `Node/${process.versions.node}`; } if (navigator.userAgentData?.brands) { diff --git a/source/vscode/src/webviewPanel.ts b/source/vscode/src/webviewPanel.ts index 7c6cfe9135..f02100c902 100644 --- a/source/vscode/src/webviewPanel.ts +++ b/source/vscode/src/webviewPanel.ts @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +declare const __PLATFORM_DIR__: string; + import * as vscode from "vscode"; import { IOperationInfo, @@ -47,7 +49,7 @@ export function registerWebViewCommands(context: ExtensionContext) { const compilerWorkerScriptPath = Uri.joinPath( context.extensionUri, - "./out/compilerWorker.js", + `./out/${__PLATFORM_DIR__}/compilerWorker.js`, ).toString(); context.subscriptions.push( From c482e3eb96ab633a3aff8abccc2876a27323f475 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 3 Apr 2026 11:56:55 -0700 Subject: [PATCH 04/16] remove redundant code from main.ts entrypoint --- source/npm/qsharp/src/browser.ts | 9 +- source/npm/qsharp/src/common-exports.ts | 1 + source/npm/qsharp/src/main.ts | 154 ++++-------------------- 3 files changed, 31 insertions(+), 133 deletions(-) diff --git a/source/npm/qsharp/src/browser.ts b/source/npm/qsharp/src/browser.ts index 62faf42d7a..eaa17a700e 100644 --- a/source/npm/qsharp/src/browser.ts +++ b/source/npm/qsharp/src/browser.ts @@ -37,6 +37,13 @@ import { createProxy } from "./workers/browser.js"; let wasmModule: WebAssembly.Module | null = null; let wasmModulePromise: Promise | null = null; +// Getter for wasmModule that works across CJS/ESM boundaries. +// Direct `export let` live bindings don't survive CJS bundling. +export function getWasmModule(): WebAssembly.Module { + if (!wasmModule) throw "Wasm module must be loaded first"; + return wasmModule; +} + // Used to track if an instance is already instantiated let wasmInstancePromise: Promise | null = null; @@ -72,7 +79,7 @@ export function loadWasmModule( return wasmModulePromise; } -async function instantiateWasm() { +export async function instantiateWasm() { // Ensure loading the module has been initiated, and wait for it. if (!wasmModulePromise) throw "Wasm module must be loaded first"; await wasmModulePromise; diff --git a/source/npm/qsharp/src/common-exports.ts b/source/npm/qsharp/src/common-exports.ts index 3230988d37..1205ada4b8 100644 --- a/source/npm/qsharp/src/common-exports.ts +++ b/source/npm/qsharp/src/common-exports.ts @@ -26,3 +26,4 @@ export type { export type { ProjectLoader } from "./project.js"; export type { CircuitGroup as CircuitData } from "./data-structures/circuit.js"; export type { LogLevel } from "./log.js"; +export { StepResultId } from "../lib/web/qsc_wasm.js"; diff --git a/source/npm/qsharp/src/main.ts b/source/npm/qsharp/src/main.ts index 86b90b4ad1..475c10e9da 100644 --- a/source/npm/qsharp/src/main.ts +++ b/source/npm/qsharp/src/main.ts @@ -3,155 +3,45 @@ // This module is the main entry point for use in Node.js environments. For browser environments, // the "./browser.js" file is the entry point module. +// +// Most functionality is shared with browser.ts. This module only provides +// Node.js-specific worker creation (using node:worker_threads) and +// re-exports everything else from browser.ts. -import * as wasm from "../lib/web/qsc_wasm.js"; -import initWasm from "../lib/web/qsc_wasm.js"; +import { type ICompilerWorker, compilerProtocol } from "./compiler/compiler.js"; import { - Compiler, - ICompiler, - ICompilerWorker, - compilerProtocol, -} from "./compiler/compiler.js"; -import { - IDebugService, - IDebugServiceWorker, - QSharpDebugService, + type IDebugServiceWorker, debugServiceProtocol, } from "./debug-service/debug-service.js"; -import { callAndTransformExceptions } from "./diagnostics.js"; import { - ILanguageService, - ILanguageServiceWorker, - QSharpLanguageService, + type ILanguageServiceWorker, languageServiceProtocol, } from "./language-service/language-service.js"; -import { log } from "./log.js"; +import { getWasmModule } from "./browser.js"; import { createProxy } from "./workers/node.js"; -import { ProjectLoader } from "./project.js"; -import type { IProjectHost } from "./browser.js"; - -// Create once. A module is stateless and can be efficiently passed to WebWorkers. -let wasmModule: WebAssembly.Module; -let wasmModulePromise: Promise | null = null; - -// Used to track if an instance is already instantiated -let wasmInstancePromise: Promise | null = null; - -async function wasmLoader(uriOrBuffer: string | ArrayBuffer) { - if (typeof uriOrBuffer === "string") { - log.info("Fetching wasm module from %s", uriOrBuffer); - performance.mark("fetch-wasm-start"); - const wasmRequst = await fetch(uriOrBuffer); - const wasmBuffer = await wasmRequst.arrayBuffer(); - const fetchTiming = performance.measure("fetch-wasm", "fetch-wasm-start"); - log.logTelemetry({ - id: "fetch-wasm", - data: { - duration: fetchTiming.duration, - uri: uriOrBuffer, - }, - }); - - wasmModule = await WebAssembly.compile(wasmBuffer); - } else { - log.info("Compiling wasm module from provided buffer"); - wasmModule = await WebAssembly.compile(uriOrBuffer); - } -} - -export function loadWasmModule( - uriOrBuffer: string | ArrayBuffer, -): Promise { - // Only initiate if not already in flight, to avoid race conditions - if (!wasmModulePromise) { - wasmModulePromise = wasmLoader(uriOrBuffer); - } - return wasmModulePromise; -} - -async function instantiateWasm() { - // Ensure loading the module has been initiated, and wait for it. - if (!wasmModulePromise) throw "Wasm module must be loaded first"; - await wasmModulePromise; - if (!wasmModule) throw "Wasm module failed to load"; - - if (wasmInstancePromise) { - // Either in flight or already complete. The prior request will do the init, - // so just wait on that. - await wasmInstancePromise; - return; - } - - // Set the promise to signal this is in flight, then wait on the result. - wasmInstancePromise = initWasm(wasmModule); - await wasmInstancePromise; - - // Once ready, set up logging and telemetry as soon as possible after instantiating - wasm.initLogging(log.logWithLevel, log.getLogLevel()); - log.onLevelChanged = (level) => wasm.setLogLevel(level); -} - -export async function getLibrarySourceContent( - path: string, -): Promise { - await instantiateWasm(); - return wasm.get_library_source_content(path); -} -export async function getCompiler(): Promise { - await instantiateWasm(); - return new Compiler(wasm); -} - -export async function getProjectLoader( - host: IProjectHost, -): Promise { - await instantiateWasm(); - return new ProjectLoader(wasm, host); -} +export { + loadWasmModule, + getLibrarySourceContent, + getCompiler, + getProjectLoader, + getDebugService, + getLanguageService, + getTargetProfileFromEntryPoint, +} from "./browser.js"; export function getCompilerWorker(worker: string): ICompilerWorker { - return createProxy(worker, wasmModule, compilerProtocol); -} - -export async function getDebugService(): Promise { - await instantiateWasm(); - return new QSharpDebugService(wasm); + return createProxy(worker, getWasmModule(), compilerProtocol); } export function getDebugServiceWorker(worker: string): IDebugServiceWorker { - return createProxy(worker, wasmModule, debugServiceProtocol); -} - -export async function getLanguageService( - host?: IProjectHost, -): Promise { - await instantiateWasm(); - return new QSharpLanguageService(wasm, host); + return createProxy(worker, getWasmModule(), debugServiceProtocol); } -export async function getLanguageServiceWorker( +export function getLanguageServiceWorker( worker: string, -): Promise { - return createProxy(worker, wasmModule, languageServiceProtocol); -} - -/// Extracts the target profile from a Q# source file's entry point. -/// Scans the provided source code for an EntryPoint argument specifying -/// a profile and returns the corresponding TargetProfile value, if found. -/// Returns undefined if no profile is specified or if the profile is not recognized. -export async function getTargetProfileFromEntryPoint( - fileName: string, - source: string, -): Promise { - await instantiateWasm(); - return callAndTransformExceptions( - async () => - wasm.get_target_profile_from_entry_point(fileName, source) as - | wasm.TargetProfile - | undefined, - ); +): ILanguageServiceWorker { + return createProxy(worker, getWasmModule(), languageServiceProtocol); } -export { StepResultId } from "../lib/web/qsc_wasm.js"; export * from "./common-exports.js"; From 600f6ab69a996e496b04000dadd839d5abb9b5cf Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 3 Apr 2026 12:53:05 -0700 Subject: [PATCH 05/16] fix deprecated parameters --- source/npm/qsharp/src/browser.ts | 2 +- source/npm/qsharp/src/workers/node.ts | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/source/npm/qsharp/src/browser.ts b/source/npm/qsharp/src/browser.ts index eaa17a700e..eeb19e6dc0 100644 --- a/source/npm/qsharp/src/browser.ts +++ b/source/npm/qsharp/src/browser.ts @@ -93,7 +93,7 @@ export async function instantiateWasm() { } // Set the promise to signal this is in flight, then wait on the result. - wasmInstancePromise = initWasm(wasmModule); + wasmInstancePromise = initWasm({ module_or_path: wasmModule }); await wasmInstancePromise; // Once ready, set up logging and telemetry as soon as possible after instantiating diff --git a/source/npm/qsharp/src/workers/node.ts b/source/npm/qsharp/src/workers/node.ts index 10967b9e95..a8479f1b28 100644 --- a/source/npm/qsharp/src/workers/node.ts +++ b/source/npm/qsharp/src/workers/node.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { dirname, join } from "node:path"; -import { fileURLToPath } from "node:url"; import { Worker, isMainThread, @@ -36,7 +34,7 @@ export function createWorker< const port = parentPort!; const { wasmModule, qscLogLevel } = workerData || {}; - initSync(wasmModule); + initSync({ module: wasmModule }); const postMessage = port.postMessage.bind(port); From a074ed35ae22966d4f5f941108b8489862cffcec Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Fri, 3 Apr 2026 12:53:21 -0700 Subject: [PATCH 06/16] fix qsharp-lang tests --- source/npm/qsharp/test/basics.js | 126 +++++++++++++--------- source/npm/qsharp/test/circuits.js | 10 +- source/npm/qsharp/test/diagnostics.js | 15 ++- source/npm/qsharp/test/languageService.js | 9 +- 4 files changed, 100 insertions(+), 60 deletions(-) diff --git a/source/npm/qsharp/test/basics.js b/source/npm/qsharp/test/basics.js index 07c7a35b7d..024ee52213 100644 --- a/source/npm/qsharp/test/basics.js +++ b/source/npm/qsharp/test/basics.js @@ -12,6 +12,7 @@ import { getLanguageService, getLanguageServiceWorker, getDebugServiceWorker, + loadWasmModule, utils, } from "../dist/main.js"; @@ -19,6 +20,21 @@ import { QscEventTarget } from "../dist/compiler/events.js"; import { getAllKatas, getExerciseSources, getKata } from "../dist/katas.js"; import samples from "../dist/samples.generated.js"; +import { readFileSync } from "node:fs"; + +const distDir = new URL("../dist/", import.meta.url); +const compilerWorkerPath = new URL("compiler/worker-node.js", distDir).href; +const languageServiceWorkerPath = new URL( + "language-service/worker-node.js", + distDir, +).href; +const debugServiceWorkerPath = new URL("debug-service/worker-node.js", distDir) + .href; + +// Load the wasm module before running any tests +const wasmPath = new URL("../lib/web/qsc_wasm_bg.wasm", import.meta.url); +await loadWasmModule(readFileSync(wasmPath).buffer); + /** @type {import("../dist/log.js").TelemetryEvent[]} */ const telemetryEvents = []; log.setLogLevel("warn"); @@ -31,27 +47,31 @@ log.setTelemetryCollector((event) => telemetryEvents.push(event)); * @param {boolean} useWorker * @returns {Promise} */ -export function runSingleShot(code, expr, useWorker) { - return new Promise((resolve, reject) => { - const resultsHandler = new QscEventTarget(true); - const compiler = useWorker ? getCompilerWorker() : getCompiler(); +export async function runSingleShot(code, expr, useWorker) { + const resultsHandler = new QscEventTarget(true); + const compiler = useWorker + ? getCompilerWorker(compilerWorkerPath) + : await getCompiler(); - compiler - .run( - { sources: [["test.qs", code]], languageFeatures: [] }, - expr, - 1, - resultsHandler, - ) - .then(() => resolve(resultsHandler.getResults()[0])) - .catch((err) => reject(err)) - /* @ts-expect-error: ICompiler does not include 'terminate' */ - .finally(() => (useWorker ? compiler.terminate() : null)); - }); + try { + await compiler.run( + { sources: [["test.qs", code]], languageFeatures: [] }, + expr, + 1, + resultsHandler, + ); + return resultsHandler.getResults()[0]; + } catch (err) { + console.error("Error during runSingleShot:", err); + throw err; + } finally { + /* @ts-expect-error: ICompiler does not include 'terminate' */ + if (useWorker) compiler.terminate(); + } } test("autogenerated documentation", async () => { - const compiler = getCompiler(); + const compiler = await getCompiler(); const regex = new RegExp("^qsharp.namespace: (.+)$", "m"); const docFiles = await compiler.getDocumentation(); var numberOfGoodFiles = 0; @@ -85,7 +105,7 @@ test("autogenerated documentation", async () => { }); test("library summaries slim docs", async () => { - const compiler = getCompiler(); + const compiler = await getCompiler(); const summaries = await compiler.getLibrarySummaries(); assert(typeof summaries === "string", "Summaries should be a string"); assert(summaries.length > 0, "Summaries should not be empty"); @@ -107,12 +127,12 @@ test("library summaries slim docs", async () => { }); test("basic eval", async () => { - let code = `namespace Test { + const code = `namespace Test { function Answer() : Int { return 42; } }`; - let expr = `Test.Answer()`; + const expr = `Test.Answer()`; const result = await runSingleShot(code, expr, false); assert(result.success); @@ -134,7 +154,7 @@ namespace Test { }); test("one syntax error", async () => { - const compiler = getCompiler(); + const compiler = await getCompiler(); const diags = await compiler.checkCode("namespace Foo []"); assert.equal(diags.length, 1); @@ -143,7 +163,7 @@ test("one syntax error", async () => { }); test("error with newlines", async () => { - const compiler = getCompiler(); + const compiler = await getCompiler(); const diags = await compiler.checkCode( "namespace input { operation Foo(a) : Unit {} }", @@ -164,27 +184,27 @@ test("error with newlines", async () => { }); test("dump and message output", async () => { - let code = `namespace Test { + const code = `namespace Test { function Answer() : Int { Microsoft.Quantum.Diagnostics.DumpMachine(); Message("hello, qsharp"); return 42; } }`; - let expr = `Test.Answer()`; + const expr = `Test.Answer()`; const result = await runSingleShot(code, expr, true); assert(result.success); - assert(result.events.length == 2); - assert(result.events[0].type == "DumpMachine"); - assert(result.events[0].state["|0⟩"].length == 2); - assert(result.events[1].type == "Message"); - assert(result.events[1].message == "hello, qsharp"); + assert(result.events.length === 2); + assert(result.events[0].type === "DumpMachine"); + assert(result.events[0].state["|0⟩"].length === 2); + assert(result.events[1].type === "Message"); + assert(result.events[1].message === "hello, qsharp"); }); async function runExerciseSolutionCheck(exercise, solution) { const evtTarget = new QscEventTarget(true); - const compiler = getCompiler(); + const compiler = await getCompiler(); const sources = await getExerciseSources(exercise); const success = await compiler.checkExerciseSolution( solution, @@ -362,7 +382,7 @@ test("worker 100 shots", async () => { let expr = `Test.Answer()`; const resultsHandler = new QscEventTarget(true); - const compiler = getCompilerWorker(); + const compiler = getCompilerWorker(compilerWorkerPath); await compiler.run( { sources: [["test.qs", code]], languageFeatures: [] }, expr, @@ -382,7 +402,7 @@ test("worker 100 shots", async () => { }); test("Run samples", async () => { - const compiler = getCompilerWorker(); + const compiler = getCompilerWorker(compilerWorkerPath); const resultsHandler = new QscEventTarget(true); const testCases = samples.filter((x) => !x.omitFromTests); @@ -403,7 +423,7 @@ test("Run samples", async () => { }); test("state change", async () => { - const compiler = getCompilerWorker(); + const compiler = getCompilerWorker(compilerWorkerPath); const resultsHandler = new QscEventTarget(false); const stateChanges = []; @@ -446,7 +466,7 @@ test("cancel worker", () => { }`; const cancelledArray = []; - const compiler = getCompilerWorker(); + const compiler = getCompilerWorker(compilerWorkerPath); const resultsHandler = new QscEventTarget(false); // Queue some tasks that will never complete @@ -473,7 +493,7 @@ test("cancel worker", () => { compiler.terminate(); // Start a new compiler and ensure that works fine - const compiler2 = getCompilerWorker(); + const compiler2 = getCompilerWorker(compilerWorkerPath); const result = await compiler2.getHir(code, []); compiler2.terminate(); @@ -490,7 +510,7 @@ test("cancel worker", () => { }); test("check code", async () => { - const compiler = getCompiler(); + const compiler = await getCompiler(); const diags = await compiler.checkCode("namespace Foo []"); assert.equal(diags.length, 1); @@ -499,7 +519,7 @@ test("check code", async () => { }); test("language service diagnostics", async () => { - const languageService = getLanguageService(); + const languageService = await getLanguageService(); let gotDiagnostics = false; languageService.addEventListener("diagnostics", (event) => { gotDiagnostics = true; @@ -530,7 +550,7 @@ test("language service diagnostics", async () => { }); test("test callable discovery", async () => { - const languageService = getLanguageService(); + const languageService = await getLanguageService(); let gotTests = false; languageService.addEventListener("testCallables", (event) => { gotTests = true; @@ -567,7 +587,7 @@ test("test callable discovery", async () => { }); test("multiple test callable discovery", async () => { - const languageService = getLanguageService(); + const languageService = await getLanguageService(); let gotTests = false; languageService.addEventListener("testCallables", (event) => { gotTests = true; @@ -606,7 +626,7 @@ namespace Sample2 { }); test("diagnostics with related spans", async () => { - const languageService = getLanguageService(); + const languageService = await getLanguageService(); let gotDiagnostics = false; languageService.addEventListener("diagnostics", (event) => { gotDiagnostics = true; @@ -690,7 +710,7 @@ test("diagnostics with related spans", async () => { }); test("language service diagnostics - web worker", async () => { - const languageService = getLanguageServiceWorker(); + const languageService = getLanguageServiceWorker(languageServiceWorkerPath); let gotDiagnostics = false; languageService.addEventListener("diagnostics", (event) => { gotDiagnostics = true; @@ -722,7 +742,7 @@ test("language service diagnostics - web worker", async () => { }); test("language service configuration update", async () => { - const languageService = getLanguageServiceWorker(); + const languageService = getLanguageServiceWorker(languageServiceWorkerPath); // Set the configuration to expect an entry point. await languageService.updateConfiguration({ packageType: "exe" }); @@ -772,7 +792,7 @@ test("language service configuration update", async () => { }); test("language service in notebook", async () => { - const languageService = getLanguageServiceWorker(); + const languageService = getLanguageServiceWorker(languageServiceWorkerPath); let actualMessages = []; languageService.addEventListener("diagnostics", (event) => { actualMessages.push({ @@ -813,7 +833,9 @@ test("language service in notebook", async () => { }); async function testCompilerError(useWorker) { - const compiler = useWorker ? getCompilerWorker() : getCompiler(); + const compiler = useWorker + ? getCompilerWorker(compilerWorkerPath) + : await getCompiler(); if (useWorker) { // @ts-expect-error onstatechange only exists on the worker compiler.onstatechange = (state) => { @@ -854,7 +876,7 @@ test("compiler error on run", () => testCompilerError(false)); test("compiler error on run - worker", () => testCompilerError(true)); test("debug service loading source without entry point attr fails - web worker", async () => { - const debugService = getDebugServiceWorker(); + const debugService = getDebugServiceWorker(debugServiceWorkerPath); try { const result = await debugService.loadProgram( { @@ -883,7 +905,7 @@ test("debug service loading source without entry point attr fails - web worker", }); test("debug service loading source with syntax error fails - web worker", async () => { - const debugService = getDebugServiceWorker(); + const debugService = getDebugServiceWorker(debugServiceWorkerPath); try { const result = await debugService.loadProgram( { @@ -908,7 +930,7 @@ test("debug service loading source with syntax error fails - web worker", async }); test("debug service loading source with bad entry expr fails - web worker", async () => { - const debugService = getDebugServiceWorker(); + const debugService = getDebugServiceWorker(debugServiceWorkerPath); try { const result = await debugService.loadProgram( { @@ -927,7 +949,7 @@ test("debug service loading source with bad entry expr fails - web worker", asyn }); test("debug service loading source that doesn't match profile fails - web worker", async () => { - const debugService = getDebugServiceWorker(); + const debugService = getDebugServiceWorker(debugServiceWorkerPath); try { const result = await debugService.loadProgram( { @@ -949,7 +971,7 @@ test("debug service loading source that doesn't match profile fails - web worker }); test("debug service loading source with good entry expr succeeds - web worker", async () => { - const debugService = getDebugServiceWorker(); + const debugService = getDebugServiceWorker(debugServiceWorkerPath); try { const result = await debugService.loadProgram( { @@ -969,7 +991,7 @@ test("debug service loading source with good entry expr succeeds - web worker", }); test("debug service loading source with entry point attr succeeds - web worker", async () => { - const debugService = getDebugServiceWorker(); + const debugService = getDebugServiceWorker(debugServiceWorkerPath); try { const result = await debugService.loadProgram( { @@ -1000,7 +1022,7 @@ test("debug service loading source with entry point attr succeeds - web worker", }); test("debug service getting breakpoints after loaded source succeeds when file names match - web worker", async () => { - const debugService = getDebugServiceWorker(); + const debugService = getDebugServiceWorker(debugServiceWorkerPath); try { const result = await debugService.loadProgram( { @@ -1032,7 +1054,7 @@ test("debug service getting breakpoints after loaded source succeeds when file n }); test("debug service compiling multiple sources - web worker", async () => { - const debugService = getDebugServiceWorker(); + const debugService = getDebugServiceWorker(debugServiceWorkerPath); try { const result = await debugService.loadProgram( { diff --git a/source/npm/qsharp/test/circuits.js b/source/npm/qsharp/test/circuits.js index ca7fddadba..4097fcff5e 100644 --- a/source/npm/qsharp/test/circuits.js +++ b/source/npm/qsharp/test/circuits.js @@ -9,15 +9,19 @@ // @ts-check import { JSDOM } from "jsdom"; -import fs from "node:fs"; +import fs, { readFileSync } from "node:fs"; import path from "node:path"; import { afterEach, beforeEach, test } from "node:test"; import { fileURLToPath } from "node:url"; import prettier from "prettier"; import { log } from "../dist/log.js"; -import { getCompiler } from "../dist/main.js"; +import { getCompiler, loadWasmModule } from "../dist/main.js"; import { draw } from "../dist/ux/circuit-vis/index.js"; +// Load the wasm module before running any tests +const wasmPath = new URL("../lib/web/qsc_wasm_bg.wasm", import.meta.url); +await loadWasmModule(readFileSync(wasmPath).buffer); + /** @type {import("../dist/log.js").TelemetryEvent[]} */ const telemetryEvents = []; log.setLogLevel("warn"); @@ -273,7 +277,7 @@ async function generateAndDrawCircuit( generationMethod, renderDepth, ) { - const compiler = getCompiler(); + const compiler = await getCompiler(); const title = document.createElement("div"); title.innerHTML = `

${id}

`; document.body.appendChild(title); diff --git a/source/npm/qsharp/test/diagnostics.js b/source/npm/qsharp/test/diagnostics.js index a801c79d83..2cca4fa2df 100644 --- a/source/npm/qsharp/test/diagnostics.js +++ b/source/npm/qsharp/test/diagnostics.js @@ -4,6 +4,7 @@ //@ts-check import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; import { test } from "node:test"; import { QdkDiagnostics } from "../dist/diagnostics.js"; import { log } from "../dist/log.js"; @@ -11,8 +12,16 @@ import { getCompiler, getCompilerWorker, getProjectLoader, + loadWasmModule, } from "../dist/main.js"; +const distDir = new URL("../dist/", import.meta.url); +const compilerWorkerPath = new URL("compiler/worker-node.js", distDir).href; + +// Load the wasm module before running any tests +const wasmPath = new URL("../lib/web/qsc_wasm_bg.wasm", import.meta.url); +await loadWasmModule(readFileSync(wasmPath).buffer); + /** @type {import("../dist/log.js").TelemetryEvent[]} */ const telemetryEvents = []; log.setLogLevel("warn"); @@ -34,7 +43,7 @@ function getInvalidQirProgramConfig() { } test("getQir throws QdkDiagnostics", async () => { - const compiler = getCompiler(); + const compiler = await getCompiler(); const invalidConfig = getInvalidQirProgramConfig(); await assert.rejects( () => compiler.getQir(invalidConfig), @@ -49,7 +58,7 @@ test("getQir throws QdkDiagnostics", async () => { }); test("getQir throws QdkDiagnostics - worker", async () => { - const compiler = getCompilerWorker(); + const compiler = getCompilerWorker(compilerWorkerPath); const invalidConfig = getInvalidQirProgramConfig(); try { await assert.rejects( @@ -77,7 +86,7 @@ const dummyHost = { }; test("loadQSharpProject throws QdkDiagnostics", async () => { - const loader = getProjectLoader(dummyHost); + const loader = await getProjectLoader(dummyHost); await assert.rejects( () => loader.loadQSharpProject("/not/a/real/dir"), (err) => { diff --git a/source/npm/qsharp/test/languageService.js b/source/npm/qsharp/test/languageService.js index b265097149..60e0988d29 100644 --- a/source/npm/qsharp/test/languageService.js +++ b/source/npm/qsharp/test/languageService.js @@ -4,9 +4,14 @@ // @ts-check import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; import { test } from "node:test"; import { log } from "../dist/log.js"; -import { getLanguageService } from "../dist/main.js"; +import { getLanguageService, loadWasmModule } from "../dist/main.js"; + +// Load the wasm module before running any tests +const wasmPath = new URL("../lib/web/qsc_wasm_bg.wasm", import.meta.url); +await loadWasmModule(readFileSync(wasmPath).buffer); log.setLogLevel("warn"); @@ -20,7 +25,7 @@ const dummyHost = { }; test("devDiagnostics configuration works", async () => { - const languageService = getLanguageService(dummyHost); + const languageService = await getLanguageService(dummyHost); try { // Collect diagnostics events as they are raised From 1db22da3d04de4b4f49d6e3d44582ddcc9e493c8 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 7 Apr 2026 17:23:58 -0700 Subject: [PATCH 07/16] make generate_docs depend on web version of wasm --- source/npm/qsharp/generate_docs.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/source/npm/qsharp/generate_docs.js b/source/npm/qsharp/generate_docs.js index c01fb9074a..2f00bb50c8 100644 --- a/source/npm/qsharp/generate_docs.js +++ b/source/npm/qsharp/generate_docs.js @@ -3,11 +3,11 @@ // @ts-check -import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; -import { generate_docs } from "./lib/nodejs/qsc_wasm.cjs"; +import initWasm, { generate_docs } from "./lib/web/qsc_wasm.js"; const scriptDirPath = dirname(fileURLToPath(import.meta.url)); const docsDirPath = join(scriptDirPath, "docs"); @@ -16,6 +16,11 @@ if (!existsSync(docsDirPath)) { mkdirSync(docsDirPath); } +// Initialize wasm before calling any exported functions +const wasmPath = join(scriptDirPath, "lib", "web", "qsc_wasm_bg.wasm"); +const wasmBytes = readFileSync(wasmPath); +await initWasm({ module_or_path: wasmBytes }); + // 'filename' will be of the format 'namespace/api.md' (except for 'toc.yaml') // 'metadata' will be the metadata that will appear at the top of the file // 'contents' will contain the non-metadata markdown expected From 65adfc7d16a3d863cfe86fce59fd2b2d009859e3 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 7 Apr 2026 17:25:56 -0700 Subject: [PATCH 08/16] use web-worker npm package to make worker code agnostic of platform (browser vs node) --- build.py | 2 +- package-lock.json | 9 + source/npm/qsharp/package.json | 29 +-- source/npm/qsharp/src/browser.ts | 201 ---------------- .../npm/qsharp/src/compiler/worker-browser.ts | 8 - source/npm/qsharp/src/compiler/worker-node.ts | 7 - source/npm/qsharp/src/compiler/worker.ts | 8 + .../qsharp/src/debug-service/debug-service.ts | 2 +- .../src/debug-service/worker-browser.ts | 8 - .../qsharp/src/debug-service/worker-node.ts | 7 - source/npm/qsharp/src/debug-service/worker.ts | 8 + .../src/language-service/language-service.ts | 2 +- .../src/language-service/worker-browser.ts | 8 - .../src/language-service/worker-node.ts | 7 - .../npm/qsharp/src/language-service/worker.ts | 8 + source/npm/qsharp/src/main.ts | 218 +++++++++++++++--- source/npm/qsharp/src/workers/main.ts | 68 ++++++ source/npm/qsharp/src/workers/node.ts | 96 -------- .../src/workers/{browser.ts => worker.ts} | 48 ---- 19 files changed, 303 insertions(+), 441 deletions(-) delete mode 100644 source/npm/qsharp/src/browser.ts delete mode 100644 source/npm/qsharp/src/compiler/worker-browser.ts delete mode 100644 source/npm/qsharp/src/compiler/worker-node.ts create mode 100644 source/npm/qsharp/src/compiler/worker.ts delete mode 100644 source/npm/qsharp/src/debug-service/worker-browser.ts delete mode 100644 source/npm/qsharp/src/debug-service/worker-node.ts create mode 100644 source/npm/qsharp/src/debug-service/worker.ts delete mode 100644 source/npm/qsharp/src/language-service/worker-browser.ts delete mode 100644 source/npm/qsharp/src/language-service/worker-node.ts create mode 100644 source/npm/qsharp/src/language-service/worker.ts create mode 100644 source/npm/qsharp/src/workers/main.ts delete mode 100644 source/npm/qsharp/src/workers/node.ts rename source/npm/qsharp/src/workers/{browser.ts => worker.ts} (51%) diff --git a/build.py b/build.py index 619658e014..caa8738e07 100755 --- a/build.py +++ b/build.py @@ -135,7 +135,7 @@ npm_cmd = "npm.cmd" if platform.system() == "Windows" else "npm" build_type = "debug" if args.debug else "release" -wasm_targets = ["web", "nodejs"] if not args.web_only else ["web"] +wasm_targets = ["web"] if not args.web_only else ["web"] run_tests = args.test root_dir = os.path.dirname(os.path.abspath(__file__)) diff --git a/package-lock.json b/package-lock.json index e61c868d7e..3ed194e7d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6424,6 +6424,12 @@ "node": ">= 14" } }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "license": "Apache-2.0" + }, "node_modules/webidl-conversions": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", @@ -6684,6 +6690,9 @@ "name": "qsharp-lang", "version": "0.0.0", "license": "MIT", + "dependencies": { + "web-worker": "^1.5.0" + }, "engines": { "node": ">=16.17.0" } diff --git a/source/npm/qsharp/package.json b/source/npm/qsharp/package.json index f4bf3a21a8..66ddb79ff8 100644 --- a/source/npm/qsharp/package.json +++ b/source/npm/qsharp/package.json @@ -12,26 +12,10 @@ "directory": "npm" }, "exports": { - ".": { - "browser": "./dist/browser.js", - "node": "./dist/main.js", - "default": "./dist/browser.js" - }, - "./compiler-worker": { - "browser": "./dist/compiler/worker-browser.js", - "node": "./dist/compiler/worker-node.js", - "default": "./dist/compiler/worker-browser.js" - }, - "./language-service-worker": { - "browser": "./dist/language-service/worker-browser.js", - "node": "./dist/language-service/worker-node.js", - "default": "./dist/language-service/worker-browser.js" - }, - "./debug-service-worker": { - "browser": "./dist/debug-service/worker-browser.js", - "node": "./dist/debug-service/worker-node.js", - "default": "./dist/debug-service/worker-browser.js" - }, + ".": "./dist/main.js", + "./compiler-worker": "./dist/compiler/worker.js", + "./language-service-worker": "./dist/language-service/worker.js", + "./debug-service-worker": "./dist/debug-service/worker.js", "./katas": "./dist/katas.js", "./katas-md": "./dist/katas-md.js", "./state-viz": "./ux/circuit-vis/state-viz/worker/index.ts", @@ -56,5 +40,8 @@ "docs", "lib", "ux" - ] + ], + "dependencies": { + "web-worker": "^1.5.0" + } } diff --git a/source/npm/qsharp/src/browser.ts b/source/npm/qsharp/src/browser.ts deleted file mode 100644 index eeb19e6dc0..0000000000 --- a/source/npm/qsharp/src/browser.ts +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -// This module is the entry point for browser environments. For Node.js environment, -// the "./main.js" module is the entry point. - -import * as wasm from "../lib/web/qsc_wasm.js"; -import initWasm, { - IProjectHost, - ProjectType, - TargetProfile, -} from "../lib/web/qsc_wasm.js"; -import { - Compiler, - ICompiler, - ICompilerWorker, - compilerProtocol, -} from "./compiler/compiler.js"; -import { - IDebugService, - IDebugServiceWorker, - QSharpDebugService, - debugServiceProtocol, -} from "./debug-service/debug-service.js"; -import { callAndTransformExceptions } from "./diagnostics.js"; -import { - ILanguageService, - ILanguageServiceWorker, - QSharpLanguageService, - languageServiceProtocol, -} from "./language-service/language-service.js"; -import { log } from "./log.js"; -import { ProjectLoader } from "./project.js"; -import { createProxy } from "./workers/browser.js"; - -// Create once. A module is stateless and can be efficiently passed to WebWorkers. -let wasmModule: WebAssembly.Module | null = null; -let wasmModulePromise: Promise | null = null; - -// Getter for wasmModule that works across CJS/ESM boundaries. -// Direct `export let` live bindings don't survive CJS bundling. -export function getWasmModule(): WebAssembly.Module { - if (!wasmModule) throw "Wasm module must be loaded first"; - return wasmModule; -} - -// Used to track if an instance is already instantiated -let wasmInstancePromise: Promise | null = null; - -async function wasmLoader(uriOrBuffer: string | ArrayBuffer) { - if (typeof uriOrBuffer === "string") { - log.info("Fetching wasm module from %s", uriOrBuffer); - performance.mark("fetch-wasm-start"); - const wasmRequst = await fetch(uriOrBuffer); - const wasmBuffer = await wasmRequst.arrayBuffer(); - const fetchTiming = performance.measure("fetch-wasm", "fetch-wasm-start"); - log.logTelemetry({ - id: "fetch-wasm", - data: { - duration: fetchTiming.duration, - uri: uriOrBuffer, - }, - }); - - wasmModule = await WebAssembly.compile(wasmBuffer); - } else { - log.info("Compiling wasm module from provided buffer"); - wasmModule = await WebAssembly.compile(uriOrBuffer); - } -} - -export function loadWasmModule( - uriOrBuffer: string | ArrayBuffer, -): Promise { - // Only initiate if not already in flight, to avoid race conditions - if (!wasmModulePromise) { - wasmModulePromise = wasmLoader(uriOrBuffer); - } - return wasmModulePromise; -} - -export async function instantiateWasm() { - // Ensure loading the module has been initiated, and wait for it. - if (!wasmModulePromise) throw "Wasm module must be loaded first"; - await wasmModulePromise; - if (!wasmModule) throw "Wasm module failed to load"; - - if (wasmInstancePromise) { - // Either in flight or already complete. The prior request will do the init, - // so just wait on that. - await wasmInstancePromise; - return; - } - - // Set the promise to signal this is in flight, then wait on the result. - wasmInstancePromise = initWasm({ module_or_path: wasmModule }); - await wasmInstancePromise; - - // Once ready, set up logging and telemetry as soon as possible after instantiating - wasm.initLogging(log.logWithLevel, log.getLogLevel()); - log.onLevelChanged = (level) => wasm.setLogLevel(level); -} - -export async function getLibrarySourceContent( - path: string, -): Promise { - await instantiateWasm(); - return wasm.get_library_source_content(path); -} - -export async function getDebugService(): Promise { - await instantiateWasm(); - return new QSharpDebugService(wasm); -} - -export async function getProjectLoader( - host: IProjectHost, -): Promise { - await instantiateWasm(); - return new ProjectLoader(wasm, host); -} - -// Create the debugger inside a WebWorker and proxy requests. -// If the Worker was already created via other means and is ready to receive -// messages, then the worker may be passed in and it will be initialized. -export function getDebugServiceWorker( - worker: string | Worker, -): IDebugServiceWorker { - if (!wasmModule) throw "Wasm module must be loaded first"; - return createProxy(worker, wasmModule, debugServiceProtocol); -} - -export async function getCompiler(): Promise { - await instantiateWasm(); - return new Compiler(wasm); -} - -// Create the compiler inside a WebWorker and proxy requests. -// If the Worker was already created via other means and is ready to receive -// messages, then the worker may be passed in and it will be initialized. -export function getCompilerWorker(worker: string | Worker): ICompilerWorker { - if (!wasmModule) throw "Wasm module must be loaded first"; - return createProxy(worker, wasmModule, compilerProtocol); -} - -export async function getLanguageService( - host?: IProjectHost, -): Promise { - await instantiateWasm(); - return new QSharpLanguageService(wasm, host); -} - -// Create the compiler inside a WebWorker and proxy requests. -// If the Worker was already created via other means and is ready to receive -// messages, then the worker may be passed in and it will be initialized. -export function getLanguageServiceWorker( - worker: string | Worker, -): ILanguageServiceWorker { - if (!wasmModule) throw "Wasm module must be loaded first"; - return createProxy(worker, wasmModule, languageServiceProtocol); -} - -/// Extracts the target profile from a Q# source file's entry point. -/// Scans the provided source code for an EntryPoint argument specifying -/// a profile and returns the corresponding TargetProfile value, if found. -/// Returns undefined if no profile is specified or if the profile is not recognized. -export async function getTargetProfileFromEntryPoint( - fileName: string, - source: string, -): Promise { - await instantiateWasm(); - return callAndTransformExceptions( - async () => - wasm.get_target_profile_from_entry_point(fileName, source) as - | wasm.TargetProfile - | undefined, - ); -} - -export { StepResultId } from "../lib/web/qsc_wasm.js"; -export type { - IBreakpointSpan, - ICodeAction, - ICodeLens, - IDocFile, - ILocation, - IOperationInfo, - IPosition, - IProjectConfig, - IProjectHost, - IQSharpError, - IRange, - IStackFrame, - IStructStepResult, - ITestDescriptor, - IWorkspaceEdit, - VSDiagnostic, -} from "../lib/web/qsc_wasm.js"; -export type { ProjectType, TargetProfile }; - -export * from "./common-exports.js"; diff --git a/source/npm/qsharp/src/compiler/worker-browser.ts b/source/npm/qsharp/src/compiler/worker-browser.ts deleted file mode 100644 index dc34d6509b..0000000000 --- a/source/npm/qsharp/src/compiler/worker-browser.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { createWorker } from "../workers/browser.js"; -import { compilerProtocol } from "./compiler.js"; - -// This export should be assigned to 'self.onmessage' in a WebWorker -export const messageHandler = createWorker(compilerProtocol); diff --git a/source/npm/qsharp/src/compiler/worker-node.ts b/source/npm/qsharp/src/compiler/worker-node.ts deleted file mode 100644 index 36ce386eb2..0000000000 --- a/source/npm/qsharp/src/compiler/worker-node.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { createWorker } from "../workers/node.js"; -import { compilerProtocol } from "./compiler.js"; - -export const messageHandler = createWorker(compilerProtocol); diff --git a/source/npm/qsharp/src/compiler/worker.ts b/source/npm/qsharp/src/compiler/worker.ts new file mode 100644 index 0000000000..b9ff887669 --- /dev/null +++ b/source/npm/qsharp/src/compiler/worker.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { createWorker } from "../workers/worker.js"; +import { compilerProtocol } from "./compiler.js"; + +const messageHandler = createWorker(compilerProtocol); +addEventListener("message", messageHandler); diff --git a/source/npm/qsharp/src/debug-service/debug-service.ts b/source/npm/qsharp/src/debug-service/debug-service.ts index 96633ed8cf..ea17399bad 100644 --- a/source/npm/qsharp/src/debug-service/debug-service.ts +++ b/source/npm/qsharp/src/debug-service/debug-service.ts @@ -13,7 +13,7 @@ import type { IStructStepResult, IVariable, } from "../../lib/web/qsc_wasm.js"; -import { ProgramConfig } from "../browser.js"; +import { ProgramConfig } from "../main.js"; import { eventStringToMsg } from "../compiler/common.js"; import { IQscEventTarget, diff --git a/source/npm/qsharp/src/debug-service/worker-browser.ts b/source/npm/qsharp/src/debug-service/worker-browser.ts deleted file mode 100644 index 371397f2be..0000000000 --- a/source/npm/qsharp/src/debug-service/worker-browser.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { createWorker } from "../workers/browser.js"; -import { debugServiceProtocol } from "./debug-service.js"; - -// This export should be assigned to 'self.onmessage' in a WebWorker -export const messageHandler = createWorker(debugServiceProtocol); diff --git a/source/npm/qsharp/src/debug-service/worker-node.ts b/source/npm/qsharp/src/debug-service/worker-node.ts deleted file mode 100644 index 4992e94ffc..0000000000 --- a/source/npm/qsharp/src/debug-service/worker-node.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { createWorker } from "../workers/node.js"; -import { debugServiceProtocol } from "./debug-service.js"; - -export const messageHandler = createWorker(debugServiceProtocol); diff --git a/source/npm/qsharp/src/debug-service/worker.ts b/source/npm/qsharp/src/debug-service/worker.ts new file mode 100644 index 0000000000..c9e1e1ee2f --- /dev/null +++ b/source/npm/qsharp/src/debug-service/worker.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { createWorker } from "../workers/worker.js"; +import { debugServiceProtocol } from "./debug-service.js"; + +const messageHandler = createWorker(debugServiceProtocol); +addEventListener("message", messageHandler); diff --git a/source/npm/qsharp/src/language-service/language-service.ts b/source/npm/qsharp/src/language-service/language-service.ts index 3f1415007b..db33acddf0 100644 --- a/source/npm/qsharp/src/language-service/language-service.ts +++ b/source/npm/qsharp/src/language-service/language-service.ts @@ -18,7 +18,7 @@ import type { VSDiagnostic, ITestDescriptor, } from "../../lib/web/qsc_wasm.js"; -import { IProjectHost } from "../browser.js"; +import { IProjectHost } from "../main.js"; import { log } from "../log.js"; import { IServiceEventTarget, diff --git a/source/npm/qsharp/src/language-service/worker-browser.ts b/source/npm/qsharp/src/language-service/worker-browser.ts deleted file mode 100644 index 1f414b8110..0000000000 --- a/source/npm/qsharp/src/language-service/worker-browser.ts +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { createWorker } from "../workers/browser.js"; -import { languageServiceProtocol } from "./language-service.js"; - -// This export should be assigned to 'self.onmessage' in a WebWorker -export const messageHandler = createWorker(languageServiceProtocol); diff --git a/source/npm/qsharp/src/language-service/worker-node.ts b/source/npm/qsharp/src/language-service/worker-node.ts deleted file mode 100644 index 95eddd488b..0000000000 --- a/source/npm/qsharp/src/language-service/worker-node.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { createWorker } from "../workers/node.js"; -import { languageServiceProtocol } from "./language-service.js"; - -createWorker(languageServiceProtocol); diff --git a/source/npm/qsharp/src/language-service/worker.ts b/source/npm/qsharp/src/language-service/worker.ts new file mode 100644 index 0000000000..13d58ed4f4 --- /dev/null +++ b/source/npm/qsharp/src/language-service/worker.ts @@ -0,0 +1,8 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { createWorker } from "../workers/worker.js"; +import { languageServiceProtocol } from "./language-service.js"; + +const messageHandler = createWorker(languageServiceProtocol); +addEventListener("message", messageHandler); diff --git a/source/npm/qsharp/src/main.ts b/source/npm/qsharp/src/main.ts index 475c10e9da..d8e04412e4 100644 --- a/source/npm/qsharp/src/main.ts +++ b/source/npm/qsharp/src/main.ts @@ -1,47 +1,211 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// This module is the main entry point for use in Node.js environments. For browser environments, -// the "./browser.js" file is the entry point module. -// -// Most functionality is shared with browser.ts. This module only provides -// Node.js-specific worker creation (using node:worker_threads) and -// re-exports everything else from browser.ts. - -import { type ICompilerWorker, compilerProtocol } from "./compiler/compiler.js"; +// This module is the default entry point for browser and node environments, still named this way for +// backward compatibility. + +import * as wasm from "../lib/web/qsc_wasm.js"; +import initWasm, { + IProjectHost, + ProjectType, + TargetProfile, +} from "../lib/web/qsc_wasm.js"; +import { + Compiler, + ICompiler, + ICompilerWorker, + compilerProtocol, +} from "./compiler/compiler.js"; import { - type IDebugServiceWorker, + IDebugService, + IDebugServiceWorker, + QSharpDebugService, debugServiceProtocol, } from "./debug-service/debug-service.js"; +import { callAndTransformExceptions } from "./diagnostics.js"; import { - type ILanguageServiceWorker, + ILanguageService, + ILanguageServiceWorker, + QSharpLanguageService, languageServiceProtocol, } from "./language-service/language-service.js"; -import { getWasmModule } from "./browser.js"; -import { createProxy } from "./workers/node.js"; +import { log } from "./log.js"; +import { ProjectLoader } from "./project.js"; +import { createProxy } from "./workers/main.js"; + +// Create once. A module is stateless and can be efficiently passed to WebWorkers. +let wasmModule: WebAssembly.Module | null = null; +let wasmModulePromise: Promise | null = null; + +// Getter for wasmModule that works across CJS/ESM boundaries. +// Direct `export let` live bindings don't survive CJS bundling. +export function getWasmModule(): WebAssembly.Module { + if (!wasmModule) throw "Wasm module must be loaded first"; + return wasmModule; +} + +// Used to track if an instance is already instantiated +let wasmInstancePromise: Promise | null = null; + +async function wasmLoader(uriOrBuffer: string | ArrayBuffer) { + if (typeof uriOrBuffer === "string") { + log.info("Fetching wasm module from %s", uriOrBuffer); + performance.mark("fetch-wasm-start"); + const wasmRequst = await fetch(uriOrBuffer); + const wasmBuffer = await wasmRequst.arrayBuffer(); + const fetchTiming = performance.measure("fetch-wasm", "fetch-wasm-start"); + log.logTelemetry({ + id: "fetch-wasm", + data: { + duration: fetchTiming.duration, + uri: uriOrBuffer, + }, + }); + + wasmModule = await WebAssembly.compile(wasmBuffer); + } else { + log.info("Compiling wasm module from provided buffer"); + wasmModule = await WebAssembly.compile(uriOrBuffer); + } +} + +export function loadWasmModule( + uriOrBuffer: string | ArrayBuffer, +): Promise { + // Only initiate if not already in flight, to avoid race conditions + if (!wasmModulePromise) { + wasmModulePromise = wasmLoader(uriOrBuffer); + } + return wasmModulePromise; +} + +export async function instantiateWasm() { + // Ensure loading the module has been initiated, and wait for it. + if (!wasmModulePromise) throw "Wasm module must be loaded first"; + await wasmModulePromise; + if (!wasmModule) throw "Wasm module failed to load"; + + if (wasmInstancePromise) { + // Either in flight or already complete. The prior request will do the init, + // so just wait on that. + await wasmInstancePromise; + return; + } + + // Set the promise to signal this is in flight, then wait on the result. + wasmInstancePromise = initWasm({ module_or_path: wasmModule }); + await wasmInstancePromise; -export { - loadWasmModule, - getLibrarySourceContent, - getCompiler, - getProjectLoader, - getDebugService, - getLanguageService, - getTargetProfileFromEntryPoint, -} from "./browser.js"; + // Once ready, set up logging and telemetry as soon as possible after instantiating + wasm.initLogging(log.logWithLevel, log.getLogLevel()); + log.onLevelChanged = (level) => wasm.setLogLevel(level); +} + +export async function getLibrarySourceContent( + path: string, +): Promise { + await instantiateWasm(); + return wasm.get_library_source_content(path); +} + +export async function getDebugService(): Promise { + await instantiateWasm(); + return new QSharpDebugService(wasm); +} + +export async function getProjectLoader( + host: IProjectHost, +): Promise { + await instantiateWasm(); + return new ProjectLoader(wasm, host); +} -export function getCompilerWorker(worker: string): ICompilerWorker { - return createProxy(worker, getWasmModule(), compilerProtocol); +// Create the debugger inside a WebWorker and proxy requests. +// If the Worker was already created via other means and is ready to receive +// messages, then the worker may be passed in and it will be initialized. +export function getDebugServiceWorker( + worker: string | Worker, + isWorkerModule = false, +): IDebugServiceWorker { + if (!wasmModule) throw "Wasm module must be loaded first"; + return createProxy(worker, wasmModule, debugServiceProtocol, isWorkerModule); } -export function getDebugServiceWorker(worker: string): IDebugServiceWorker { - return createProxy(worker, getWasmModule(), debugServiceProtocol); +export async function getCompiler(): Promise { + await instantiateWasm(); + return new Compiler(wasm); } +// Create the compiler inside a WebWorker and proxy requests. +// If the Worker was already created via other means and is ready to receive +// messages, then the worker may be passed in and it will be initialized. +export function getCompilerWorker( + worker: string | Worker, + isWorkerModule = false, +): ICompilerWorker { + if (!wasmModule) throw "Wasm module must be loaded first"; + return createProxy(worker, wasmModule, compilerProtocol, isWorkerModule); +} + +export async function getLanguageService( + host?: IProjectHost, +): Promise { + await instantiateWasm(); + return new QSharpLanguageService(wasm, host); +} + +// Create the compiler inside a WebWorker and proxy requests. +// If the Worker was already created via other means and is ready to receive +// messages, then the worker may be passed in and it will be initialized. export function getLanguageServiceWorker( - worker: string, + worker: string | Worker, + isWorkerModule = false, ): ILanguageServiceWorker { - return createProxy(worker, getWasmModule(), languageServiceProtocol); + if (!wasmModule) throw "Wasm module must be loaded first"; + return createProxy( + worker, + wasmModule, + languageServiceProtocol, + isWorkerModule, + ); } +/// Extracts the target profile from a Q# source file's entry point. +/// Scans the provided source code for an EntryPoint argument specifying +/// a profile and returns the corresponding TargetProfile value, if found. +/// Returns undefined if no profile is specified or if the profile is not recognized. +export async function getTargetProfileFromEntryPoint( + fileName: string, + source: string, +): Promise { + await instantiateWasm(); + return callAndTransformExceptions( + async () => + wasm.get_target_profile_from_entry_point(fileName, source) as + | wasm.TargetProfile + | undefined, + ); +} + +export { StepResultId } from "../lib/web/qsc_wasm.js"; +export type { + IBreakpointSpan, + ICodeAction, + ICodeLens, + IDocFile, + ILocation, + IOperationInfo, + IPosition, + IProjectConfig, + IProjectHost, + IQSharpError, + IRange, + IStackFrame, + IStructStepResult, + ITestDescriptor, + IWorkspaceEdit, + VSDiagnostic, +} from "../lib/web/qsc_wasm.js"; +export type { ProjectType, TargetProfile }; + export * from "./common-exports.js"; diff --git a/source/npm/qsharp/src/workers/main.ts b/source/npm/qsharp/src/workers/main.ts new file mode 100644 index 0000000000..02848819d4 --- /dev/null +++ b/source/npm/qsharp/src/workers/main.ts @@ -0,0 +1,68 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { log } from "../log.js"; +import { + IServiceEventMessage, + IServiceProxy, + ServiceMethods, + ServiceProtocol, + createProxyInternal, +} from "./common.js"; +import Worker from "web-worker"; + +/** + * Creates and initializes a service in a web worker, and returns a proxy for the service + * to be used from the main thread. + * + * @param workerArg The service web worker or the URL of the web worker script. + * @param wasmModule The wasm module to initialize the service with + * @param serviceProtocol An object that describes the service: its constructor, methods and events + * @returns A proxy object that implements the service interface. + * This interface can now be used as if calling into the real service, + * and the calls will be proxied to the web worker. + */ +export function createProxy< + TService extends ServiceMethods, + TServiceEventMsg extends IServiceEventMessage, +>( + workerArg: string | Worker, + wasmModule: WebAssembly.Module, + serviceProtocol: ServiceProtocol, + isWorkerModule = false, +): TService & IServiceProxy { + // Create or use the WebWorker + const worker = + typeof workerArg === "string" + ? new Worker(workerArg, { type: isWorkerModule ? "module" : "classic" }) + : workerArg; + + // Log any errors from the worker + worker.addEventListener("error", (ev: Event) => { + log.error("Worker error:", ev); + }); + + // Send it the Wasm module to instantiate + worker.postMessage({ + type: "init", + wasmModule, + qscLogLevel: log.getLogLevel(), + }); + + // If you lose the 'this' binding, some environments have issues + const postMessage = worker.postMessage.bind(worker); + const onTerminate = () => worker.terminate(); + + // Create the proxy which will forward method calls to the worker + const proxy = createProxyInternal( + postMessage, + onTerminate, + serviceProtocol.methods, + ); + + // Let proxy handle response and event messages from the worker + worker.addEventListener("message", (ev: MessageEvent) => { + proxy.onMsgFromWorker(ev.data); + }); + return proxy; +} diff --git a/source/npm/qsharp/src/workers/node.ts b/source/npm/qsharp/src/workers/node.ts deleted file mode 100644 index a8479f1b28..0000000000 --- a/source/npm/qsharp/src/workers/node.ts +++ /dev/null @@ -1,96 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -import { - Worker, - isMainThread, - parentPort, - workerData, -} from "node:worker_threads"; -import * as wasm from "../../lib/web/qsc_wasm.js"; -import { initSync } from "../../lib/web/qsc_wasm.js"; -import { log } from "../log.js"; -import { - IServiceEventMessage, - IServiceProxy, - ServiceMethods, - ServiceProtocol, - createProxyInternal, - initService, -} from "./common.js"; - -/** - * Creates an initializes a service, setting it up to receive requests. - * This function to be is used in the worker. - * - * @param serviceProtocol An object that describes the service: its constructor, methods and events - */ -export function createWorker< - TService extends ServiceMethods, - TServiceEventMsg extends IServiceEventMessage, ->(protocol: ServiceProtocol): (data: any) => void { - if (isMainThread) - throw "Worker script should be loaded in a Worker thread only"; - - const port = parentPort!; - const { wasmModule, qscLogLevel } = workerData || {}; - initSync({ module: wasmModule }); - - const postMessage = port.postMessage.bind(port); - - const invokeService = initService( - postMessage, - protocol, - wasm, - qscLogLevel === "number" ? workerData.qscLogLevel : undefined, - ); - - function messageHandler(data: any) { - if (!data.type || typeof data.type !== "string") { - log.error(`Unrecognized msg: %O"`, data); - return; - } - - invokeService(data); - } - - port.addListener("message", messageHandler); - return messageHandler; -} - -/** - * Creates and initializes a service in a worker thread, and returns a proxy for the service - * to be used from the main thread. - * - * @param workerArg The the URL of the service web worker script. - * @param wasmModule The wasm module to initialize the service with - * @param serviceProtocol An object that describes the service: its constructor, methods and events - * @returns A proxy object that implements the service interface. - * This interface can now be used as if calling into the real service, - * and the calls will be proxied to the web worker. - */ -export function createProxy< - TService extends ServiceMethods, - TServiceEventMsg extends IServiceEventMessage, ->( - workerArg: string, - wasmModule: WebAssembly.Module, - serviceProtocol: ServiceProtocol, -): TService & IServiceProxy { - const worker = new Worker(new URL(workerArg), { - workerData: { wasmModule, qscLogLevel: log.getLogLevel() }, - }); - - // Create the proxy which will forward method calls to the worker - const proxy = createProxyInternal( - // If you lose the 'this' binding, some environments have issues. - worker.postMessage.bind(worker), - () => worker.terminate(), - serviceProtocol.methods, - ); - - // Let proxy handle response and event messages from the worker - worker.addListener("message", proxy.onMsgFromWorker); - - return proxy; -} diff --git a/source/npm/qsharp/src/workers/browser.ts b/source/npm/qsharp/src/workers/worker.ts similarity index 51% rename from source/npm/qsharp/src/workers/browser.ts rename to source/npm/qsharp/src/workers/worker.ts index 2703d30c29..d96cadc2e5 100644 --- a/source/npm/qsharp/src/workers/browser.ts +++ b/source/npm/qsharp/src/workers/worker.ts @@ -5,11 +5,9 @@ import * as wasm from "../../lib/web/qsc_wasm.js"; import { log } from "../log.js"; import { IServiceEventMessage, - IServiceProxy, RequestMessage, ServiceMethods, ServiceProtocol, - createProxyInternal, initService, } from "./common.js"; @@ -63,49 +61,3 @@ export function createWorker< } }; } - -/** - * Creates and initializes a service in a web worker, and returns a proxy for the service - * to be used from the main thread. - * - * @param workerArg The service web worker or the URL of the web worker script. - * @param wasmModule The wasm module to initialize the service with - * @param serviceProtocol An object that describes the service: its constructor, methods and events - * @returns A proxy object that implements the service interface. - * This interface can now be used as if calling into the real service, - * and the calls will be proxied to the web worker. - */ -export function createProxy< - TService extends ServiceMethods, - TServiceEventMsg extends IServiceEventMessage, ->( - workerArg: string | Worker, - wasmModule: WebAssembly.Module, - serviceProtocol: ServiceProtocol, -): TService & IServiceProxy { - // Create or use the WebWorker - const worker = - typeof workerArg === "string" ? new Worker(workerArg) : workerArg; - - // Send it the Wasm module to instantiate - worker.postMessage({ - type: "init", - wasmModule, - qscLogLevel: log.getLogLevel(), - }); - - // If you lose the 'this' binding, some environments have issues - const postMessage = worker.postMessage.bind(worker); - const onTerminate = () => worker.terminate(); - - // Create the proxy which will forward method calls to the worker - const proxy = createProxyInternal( - postMessage, - onTerminate, - serviceProtocol.methods, - ); - - // Let proxy handle response and event messages from the worker - worker.onmessage = (ev) => proxy.onMsgFromWorker(ev.data); - return proxy; -} From 642a244641ec61cb5a04eb067b172dde96a51d1c Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 7 Apr 2026 17:27:01 -0700 Subject: [PATCH 09/16] update vscode extension to match new worker api --- source/vscode/src/compilerWorker.ts | 8 +------- source/vscode/src/debugger/debug-service-worker.ts | 8 +------- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/source/vscode/src/compilerWorker.ts b/source/vscode/src/compilerWorker.ts index b77c05af9d..44600e108c 100644 --- a/source/vscode/src/compilerWorker.ts +++ b/source/vscode/src/compilerWorker.ts @@ -1,10 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare const __PLATFORM_DIR__: string; - -import { messageHandler } from "qsharp-lang/compiler-worker"; - -if (__PLATFORM_DIR__ === "browser") { - self.onmessage = messageHandler; -} +import "qsharp-lang/compiler-worker"; diff --git a/source/vscode/src/debugger/debug-service-worker.ts b/source/vscode/src/debugger/debug-service-worker.ts index f74912fdf7..b61d2f0bca 100644 --- a/source/vscode/src/debugger/debug-service-worker.ts +++ b/source/vscode/src/debugger/debug-service-worker.ts @@ -1,10 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare const __PLATFORM_DIR__: string; - -import { messageHandler } from "qsharp-lang/debug-service-worker"; - -if (__PLATFORM_DIR__ === "browser") { - self.onmessage = messageHandler; -} +import "qsharp-lang/debug-service-worker"; From 59663b7b4d63de69631d285ee2999932a287b483 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 7 Apr 2026 17:27:24 -0700 Subject: [PATCH 10/16] update playground to match new worker api --- source/playground/src/compiler-worker.ts | 4 +--- source/playground/src/language-service-worker.ts | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/source/playground/src/compiler-worker.ts b/source/playground/src/compiler-worker.ts index 9659220212..44600e108c 100644 --- a/source/playground/src/compiler-worker.ts +++ b/source/playground/src/compiler-worker.ts @@ -1,6 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { messageHandler } from "qsharp-lang/compiler-worker"; - -self.onmessage = messageHandler; +import "qsharp-lang/compiler-worker"; diff --git a/source/playground/src/language-service-worker.ts b/source/playground/src/language-service-worker.ts index 2b4e4ca155..d492b8da55 100644 --- a/source/playground/src/language-service-worker.ts +++ b/source/playground/src/language-service-worker.ts @@ -1,6 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import { messageHandler } from "qsharp-lang/language-service-worker"; - -self.onmessage = messageHandler; +import "qsharp-lang/language-service-worker"; From d1d30827b673d2ff606a386f6386cb0078a08e42 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 7 Apr 2026 17:28:27 -0700 Subject: [PATCH 11/16] build node-workers separately, keep web-worker external for the node environment --- source/vscode/build.mjs | 38 +++++++++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/source/vscode/build.mjs b/source/vscode/build.mjs index d7f69fcdd3..61b87dab04 100644 --- a/source/vscode/build.mjs +++ b/source/vscode/build.mjs @@ -3,7 +3,7 @@ //@ts-check -import { copyFileSync, mkdirSync, readdirSync } from "node:fs"; +import { copyFileSync, cpSync, mkdirSync, readdirSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { build as esbuildBuild, context } from "esbuild"; @@ -172,14 +172,16 @@ function getBuildOptions(onlyUI, platform) { } else if (platform === "browser") { return { ...commonBuildOptions, - platform, + platform: "browser", outdir: join(thisDir, "out", "browser"), }; } else if (platform === "node") { return { ...commonBuildOptions, - platform, + platform: "node", outdir: join(thisDir, "out", "node"), + entryPoints: [join(thisDir, "src", "extension.ts")], + external: ["vscode", "web-worker"], banner: { js: 'const _importMetaUrl = require("url").pathToFileURL(__filename).href;', }, @@ -188,6 +190,20 @@ function getBuildOptions(onlyUI, platform) { __PLATFORM_DIR__: JSON.stringify("node"), }, }; + } else if (platform === "node-worker") { + return { + ...commonBuildOptions, + platform: "node", + outdir: join(thisDir, "out", "node"), + entryPoints: [ + join(thisDir, "src", "compilerWorker.ts"), + join(thisDir, "src", "debugger/debug-service-worker.ts"), + ], + define: { + "import.meta.url": "undefined", + __PLATFORM_DIR__: JSON.stringify("node"), + }, + }; } else { throw new Error(`Invalid platform: ${platform}`); } @@ -220,6 +236,20 @@ function buildExtensionHost(platform) { buildBundle(false, platform); } +/** + * Copy external node dependencies into node_modules/ under the extension + * directory so they can be resolved at runtime (e.g. when installed as a VSIX). + */ +function copyNodeExternals() { + const nodeExternals = ["web-worker"]; + for (const pkg of nodeExternals) { + const src = join(libsDir, pkg); + const dest = join(thisDir, "node_modules", pkg); + console.log(`Copying external dependency ${pkg} to ${dest}`); + cpSync(src, dest, { recursive: true }); + } +} + function getTimeStr() { const now = new Date(); @@ -277,5 +307,7 @@ if (thisFilePath === resolve(process.argv[1])) { copyWasmToVsCode(); buildExtensionHost("browser"); buildExtensionHost("node"); + buildExtensionHost("node-worker"); + copyNodeExternals(); } } From cb2566da7c1f15988fe48d0e9e8be53d568203af Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 7 Apr 2026 17:28:56 -0700 Subject: [PATCH 12/16] fix qsharp-lang tests --- source/npm/qsharp/test/basics.js | 79 +++++++++++++++++++-------- source/npm/qsharp/test/diagnostics.js | 5 +- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/source/npm/qsharp/test/basics.js b/source/npm/qsharp/test/basics.js index 024ee52213..067e2549c5 100644 --- a/source/npm/qsharp/test/basics.js +++ b/source/npm/qsharp/test/basics.js @@ -23,13 +23,11 @@ import samples from "../dist/samples.generated.js"; import { readFileSync } from "node:fs"; const distDir = new URL("../dist/", import.meta.url); -const compilerWorkerPath = new URL("compiler/worker-node.js", distDir).href; -const languageServiceWorkerPath = new URL( - "language-service/worker-node.js", - distDir, -).href; -const debugServiceWorkerPath = new URL("debug-service/worker-node.js", distDir) +const compilerWorkerPath = new URL("compiler/worker.js", distDir).href; +const languageServiceWorkerPath = new URL("language-service/worker.js", distDir) .href; +const debugServiceWorkerPath = new URL("debug-service/worker.js", distDir).href; +const useWorkerModule = true; // Load the wasm module before running any tests const wasmPath = new URL("../lib/web/qsc_wasm_bg.wasm", import.meta.url); @@ -50,7 +48,7 @@ log.setTelemetryCollector((event) => telemetryEvents.push(event)); export async function runSingleShot(code, expr, useWorker) { const resultsHandler = new QscEventTarget(true); const compiler = useWorker - ? getCompilerWorker(compilerWorkerPath) + ? getCompilerWorker(compilerWorkerPath, useWorkerModule) : await getCompiler(); try { @@ -382,7 +380,7 @@ test("worker 100 shots", async () => { let expr = `Test.Answer()`; const resultsHandler = new QscEventTarget(true); - const compiler = getCompilerWorker(compilerWorkerPath); + const compiler = getCompilerWorker(compilerWorkerPath, useWorkerModule); await compiler.run( { sources: [["test.qs", code]], languageFeatures: [] }, expr, @@ -402,7 +400,7 @@ test("worker 100 shots", async () => { }); test("Run samples", async () => { - const compiler = getCompilerWorker(compilerWorkerPath); + const compiler = getCompilerWorker(compilerWorkerPath, useWorkerModule); const resultsHandler = new QscEventTarget(true); const testCases = samples.filter((x) => !x.omitFromTests); @@ -423,7 +421,7 @@ test("Run samples", async () => { }); test("state change", async () => { - const compiler = getCompilerWorker(compilerWorkerPath); + const compiler = getCompilerWorker(compilerWorkerPath, useWorkerModule); const resultsHandler = new QscEventTarget(false); const stateChanges = []; @@ -466,7 +464,7 @@ test("cancel worker", () => { }`; const cancelledArray = []; - const compiler = getCompilerWorker(compilerWorkerPath); + const compiler = getCompilerWorker(compilerWorkerPath, useWorkerModule); const resultsHandler = new QscEventTarget(false); // Queue some tasks that will never complete @@ -493,7 +491,7 @@ test("cancel worker", () => { compiler.terminate(); // Start a new compiler and ensure that works fine - const compiler2 = getCompilerWorker(compilerWorkerPath); + const compiler2 = getCompilerWorker(compilerWorkerPath, useWorkerModule); const result = await compiler2.getHir(code, []); compiler2.terminate(); @@ -710,7 +708,10 @@ test("diagnostics with related spans", async () => { }); test("language service diagnostics - web worker", async () => { - const languageService = getLanguageServiceWorker(languageServiceWorkerPath); + const languageService = getLanguageServiceWorker( + languageServiceWorkerPath, + useWorkerModule, + ); let gotDiagnostics = false; languageService.addEventListener("diagnostics", (event) => { gotDiagnostics = true; @@ -742,7 +743,10 @@ test("language service diagnostics - web worker", async () => { }); test("language service configuration update", async () => { - const languageService = getLanguageServiceWorker(languageServiceWorkerPath); + const languageService = getLanguageServiceWorker( + languageServiceWorkerPath, + useWorkerModule, + ); // Set the configuration to expect an entry point. await languageService.updateConfiguration({ packageType: "exe" }); @@ -792,7 +796,10 @@ test("language service configuration update", async () => { }); test("language service in notebook", async () => { - const languageService = getLanguageServiceWorker(languageServiceWorkerPath); + const languageService = getLanguageServiceWorker( + languageServiceWorkerPath, + useWorkerModule, + ); let actualMessages = []; languageService.addEventListener("diagnostics", (event) => { actualMessages.push({ @@ -834,7 +841,7 @@ test("language service in notebook", async () => { async function testCompilerError(useWorker) { const compiler = useWorker - ? getCompilerWorker(compilerWorkerPath) + ? getCompilerWorker(compilerWorkerPath, useWorkerModule) : await getCompiler(); if (useWorker) { // @ts-expect-error onstatechange only exists on the worker @@ -876,7 +883,10 @@ test("compiler error on run", () => testCompilerError(false)); test("compiler error on run - worker", () => testCompilerError(true)); test("debug service loading source without entry point attr fails - web worker", async () => { - const debugService = getDebugServiceWorker(debugServiceWorkerPath); + const debugService = getDebugServiceWorker( + debugServiceWorkerPath, + useWorkerModule, + ); try { const result = await debugService.loadProgram( { @@ -905,7 +915,10 @@ test("debug service loading source without entry point attr fails - web worker", }); test("debug service loading source with syntax error fails - web worker", async () => { - const debugService = getDebugServiceWorker(debugServiceWorkerPath); + const debugService = getDebugServiceWorker( + debugServiceWorkerPath, + useWorkerModule, + ); try { const result = await debugService.loadProgram( { @@ -930,7 +943,10 @@ test("debug service loading source with syntax error fails - web worker", async }); test("debug service loading source with bad entry expr fails - web worker", async () => { - const debugService = getDebugServiceWorker(debugServiceWorkerPath); + const debugService = getDebugServiceWorker( + debugServiceWorkerPath, + useWorkerModule, + ); try { const result = await debugService.loadProgram( { @@ -949,7 +965,10 @@ test("debug service loading source with bad entry expr fails - web worker", asyn }); test("debug service loading source that doesn't match profile fails - web worker", async () => { - const debugService = getDebugServiceWorker(debugServiceWorkerPath); + const debugService = getDebugServiceWorker( + debugServiceWorkerPath, + useWorkerModule, + ); try { const result = await debugService.loadProgram( { @@ -971,7 +990,10 @@ test("debug service loading source that doesn't match profile fails - web worker }); test("debug service loading source with good entry expr succeeds - web worker", async () => { - const debugService = getDebugServiceWorker(debugServiceWorkerPath); + const debugService = getDebugServiceWorker( + debugServiceWorkerPath, + useWorkerModule, + ); try { const result = await debugService.loadProgram( { @@ -991,7 +1013,10 @@ test("debug service loading source with good entry expr succeeds - web worker", }); test("debug service loading source with entry point attr succeeds - web worker", async () => { - const debugService = getDebugServiceWorker(debugServiceWorkerPath); + const debugService = getDebugServiceWorker( + debugServiceWorkerPath, + useWorkerModule, + ); try { const result = await debugService.loadProgram( { @@ -1022,7 +1047,10 @@ test("debug service loading source with entry point attr succeeds - web worker", }); test("debug service getting breakpoints after loaded source succeeds when file names match - web worker", async () => { - const debugService = getDebugServiceWorker(debugServiceWorkerPath); + const debugService = getDebugServiceWorker( + debugServiceWorkerPath, + useWorkerModule, + ); try { const result = await debugService.loadProgram( { @@ -1054,7 +1082,10 @@ test("debug service getting breakpoints after loaded source succeeds when file n }); test("debug service compiling multiple sources - web worker", async () => { - const debugService = getDebugServiceWorker(debugServiceWorkerPath); + const debugService = getDebugServiceWorker( + debugServiceWorkerPath, + useWorkerModule, + ); try { const result = await debugService.loadProgram( { diff --git a/source/npm/qsharp/test/diagnostics.js b/source/npm/qsharp/test/diagnostics.js index 2cca4fa2df..38bfe71bbf 100644 --- a/source/npm/qsharp/test/diagnostics.js +++ b/source/npm/qsharp/test/diagnostics.js @@ -16,7 +16,8 @@ import { } from "../dist/main.js"; const distDir = new URL("../dist/", import.meta.url); -const compilerWorkerPath = new URL("compiler/worker-node.js", distDir).href; +const compilerWorkerPath = new URL("compiler/worker.js", distDir).href; +const useWorkerModule = true; // Load the wasm module before running any tests const wasmPath = new URL("../lib/web/qsc_wasm_bg.wasm", import.meta.url); @@ -58,7 +59,7 @@ test("getQir throws QdkDiagnostics", async () => { }); test("getQir throws QdkDiagnostics - worker", async () => { - const compiler = getCompilerWorker(compilerWorkerPath); + const compiler = getCompilerWorker(compilerWorkerPath, useWorkerModule); const invalidConfig = getInvalidQirProgramConfig(); try { await assert.rejects( From bf2bcf8e40c8c0c35ad0a8c88ec6f3afe5d9cae2 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Tue, 7 Apr 2026 17:29:55 -0700 Subject: [PATCH 13/16] log platform, UI, and remoteName during extension activation --- source/vscode/src/extension.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/source/vscode/src/extension.ts b/source/vscode/src/extension.ts index 5b8ac0fee5..236ad4971a 100644 --- a/source/vscode/src/extension.ts +++ b/source/vscode/src/extension.ts @@ -1,5 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +declare const __PLATFORM_DIR__: string; import { getLibrarySourceContent, @@ -50,6 +51,12 @@ export async function activate( initOutputWindowLogger(); } + log.info(`Platform: ${__PLATFORM_DIR__}`); + log.info( + `UI Kind: ${vscode.env.uiKind === vscode.UIKind.Web ? "Web" : "Desktop"}`, + ); + log.info(`Remote: ${vscode.env.remoteName ?? "local"}`); + log.info("Q# extension activating."); initTelemetry(context); From dbff9f75def0c4059a6917dfce8b7a1373bad675 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 8 Apr 2026 11:12:02 -0700 Subject: [PATCH 14/16] centralize platform env --- source/vscode/build.mjs | 6 +++--- source/vscode/src/circuit.ts | 5 ++--- source/vscode/src/common.ts | 8 ++++++-- source/vscode/src/debugger/activate.ts | 6 ++---- source/vscode/src/documentation.ts | 5 ++--- source/vscode/src/extension.ts | 5 ++--- source/vscode/src/qirGeneration.ts | 6 ++---- source/vscode/src/telemetry.ts | 5 ++--- source/vscode/src/webviewPanel.ts | 6 ++---- 9 files changed, 23 insertions(+), 29 deletions(-) diff --git a/source/vscode/build.mjs b/source/vscode/build.mjs index 61b87dab04..23897fdb41 100644 --- a/source/vscode/build.mjs +++ b/source/vscode/build.mjs @@ -25,7 +25,7 @@ const commonBuildOptions = { sourcemap: "linked", define: { "import.meta.url": "undefined", - __PLATFORM_DIR__: JSON.stringify("browser"), + __PLATFORM__: JSON.stringify("browser"), }, }; @@ -187,7 +187,7 @@ function getBuildOptions(onlyUI, platform) { }, define: { "import.meta.url": "_importMetaUrl", - __PLATFORM_DIR__: JSON.stringify("node"), + __PLATFORM__: JSON.stringify("node"), }, }; } else if (platform === "node-worker") { @@ -201,7 +201,7 @@ function getBuildOptions(onlyUI, platform) { ], define: { "import.meta.url": "undefined", - __PLATFORM_DIR__: JSON.stringify("node"), + __PLATFORM__: JSON.stringify("node"), }, }; } else { diff --git a/source/vscode/src/circuit.ts b/source/vscode/src/circuit.ts index 9ae1bc7db1..27964931bb 100644 --- a/source/vscode/src/circuit.ts +++ b/source/vscode/src/circuit.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare const __PLATFORM_DIR__: string; import { escapeHtml } from "markdown-it/lib/common/utils.mjs"; import { @@ -27,7 +26,7 @@ import { import { getRandomGuid } from "./utils"; import { sendMessageToPanel } from "./webviewPanel"; import { ICircuitConfig, IPosition } from "../../npm/qsharp/lib/web/qsc_wasm"; -import { basename } from "./common"; +import { basename, getPlatformEnv } from "./common"; const compilerRunTimeoutMs = 1000 * 60 * 5; // 5 minutes @@ -281,7 +280,7 @@ export async function getCircuitOrErrorWithTimeout( const compilerWorkerScriptPath = Uri.joinPath( extensionUri, - `./out/${__PLATFORM_DIR__}/compilerWorker.js`, + `./out/${getPlatformEnv()}/compilerWorker.js`, ).toString(); const worker = getCompilerWorker(compilerWorkerScriptPath); diff --git a/source/vscode/src/common.ts b/source/vscode/src/common.ts index bc8c887bc8..6f5accc525 100644 --- a/source/vscode/src/common.ts +++ b/source/vscode/src/common.ts @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare const __PLATFORM_DIR__: string; +declare const __PLATFORM__: string; import { TextDocument, Uri, Range, Location } from "vscode"; import { @@ -148,7 +148,11 @@ export function toVsCodeDiagnostic(d: VSDiagnostic): vscode.Diagnostic { export function loadCompilerWorker(extensionUri: vscode.Uri): ICompilerWorker { const compilerWorkerScriptPath = vscode.Uri.joinPath( extensionUri, - `./out/${__PLATFORM_DIR__}/compilerWorker.js`, + `./out/${__PLATFORM__}/compilerWorker.js`, ).toString(); return getCompilerWorker(compilerWorkerScriptPath); } + +export function getPlatformEnv(): string { + return __PLATFORM__; +} diff --git a/source/vscode/src/debugger/activate.ts b/source/vscode/src/debugger/activate.ts index a656a2c33b..33fd53c972 100644 --- a/source/vscode/src/debugger/activate.ts +++ b/source/vscode/src/debugger/activate.ts @@ -1,13 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare const __PLATFORM_DIR__: string; - /* eslint-disable @typescript-eslint/no-unused-vars */ import { IDebugServiceWorker, getDebugServiceWorker, log } from "qsharp-lang"; import * as vscode from "vscode"; -import { qsharpExtensionId } from "../common"; +import { getPlatformEnv, qsharpExtensionId } from "../common"; import { clearCommandDiagnostics } from "../diagnostics"; import { getActiveQdkDocumentUri, @@ -24,7 +22,7 @@ export async function activateDebugger( ): Promise { const debugWorkerScriptPath = vscode.Uri.joinPath( context.extensionUri, - `./out/${__PLATFORM_DIR__}/debugger/debug-service-worker.js`, + `./out/${getPlatformEnv()}/debugger/debug-service-worker.js`, ); debugServiceWorkerFactory = () => diff --git a/source/vscode/src/documentation.ts b/source/vscode/src/documentation.ts index 72b45a2d82..68212e337a 100644 --- a/source/vscode/src/documentation.ts +++ b/source/vscode/src/documentation.ts @@ -1,12 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare const __PLATFORM_DIR__: string; - import { getCompilerWorker, IDocFile } from "qsharp-lang"; import { Uri } from "vscode"; import { sendMessageToPanel } from "./webviewPanel"; import { getActiveProgram } from "./programConfig"; +import { getPlatformEnv } from "./common"; export async function showDocumentationCommand(extensionUri: Uri) { const program = await getActiveProgram(); @@ -24,7 +23,7 @@ export async function showDocumentationCommand(extensionUri: Uri) { // Get API documentation from compiler. const compilerWorkerScriptPath = Uri.joinPath( extensionUri, - `./out/${__PLATFORM_DIR__}/compilerWorker.js`, + `./out/${getPlatformEnv()}/compilerWorker.js`, ).toString(); const worker = getCompilerWorker(compilerWorkerScriptPath); const docFiles = await worker.getDocumentation(program.programConfig); diff --git a/source/vscode/src/extension.ts b/source/vscode/src/extension.ts index 236ad4971a..88a0133692 100644 --- a/source/vscode/src/extension.ts +++ b/source/vscode/src/extension.ts @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare const __PLATFORM_DIR__: string; import { getLibrarySourceContent, @@ -36,7 +35,7 @@ import { maybeShowChangelogPrompt, registerChangelogCommand, } from "./changelog.js"; -import { toVsCodeRange } from "./common.js"; +import { getPlatformEnv, toVsCodeRange } from "./common.js"; export async function activate( context: vscode.ExtensionContext, @@ -51,7 +50,7 @@ export async function activate( initOutputWindowLogger(); } - log.info(`Platform: ${__PLATFORM_DIR__}`); + log.info(`Platform: ${getPlatformEnv()}`); log.info( `UI Kind: ${vscode.env.uiKind === vscode.UIKind.Web ? "Web" : "Desktop"}`, ); diff --git a/source/vscode/src/qirGeneration.ts b/source/vscode/src/qirGeneration.ts index 9294bad451..185e648d33 100644 --- a/source/vscode/src/qirGeneration.ts +++ b/source/vscode/src/qirGeneration.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare const __PLATFORM_DIR__: string; - import { getCompilerWorker, log, @@ -10,7 +8,7 @@ import { TargetProfile, } from "qsharp-lang"; import * as vscode from "vscode"; -import { qsharpExtensionId } from "./common"; +import { getPlatformEnv, qsharpExtensionId } from "./common"; import { invokeAndReportCommandDiagnostics } from "./diagnostics"; import { FullProgramConfig, getActiveProgram } from "./programConfig"; import { @@ -210,7 +208,7 @@ async function getQirForActiveWindowCommand() { export function initCodegen(context: vscode.ExtensionContext) { compilerWorkerScriptPath = vscode.Uri.joinPath( context.extensionUri, - `./out/${__PLATFORM_DIR__}/compilerWorker.js`, + `./out/${getPlatformEnv()}/compilerWorker.js`, ).toString(); context.subscriptions.push( diff --git a/source/vscode/src/telemetry.ts b/source/vscode/src/telemetry.ts index e24ce3b2fb..53056568e0 100644 --- a/source/vscode/src/telemetry.ts +++ b/source/vscode/src/telemetry.ts @@ -3,8 +3,6 @@ /// -declare const __PLATFORM_DIR__: string; - import * as vscode from "vscode"; import TelemetryReporter from "@vscode/extension-telemetry"; import { log } from "qsharp-lang"; @@ -14,6 +12,7 @@ import { isOpenQasmDocument, isQdkNotebookCell, isQsharpDocument, + getPlatformEnv, } from "./common"; export enum EventType { @@ -404,7 +403,7 @@ export function sendTelemetryEvent( } function getBrowserRelease(): string { - if (__PLATFORM_DIR__ === "node") { + if (getPlatformEnv() === "node") { return `Node/${process.versions.node}`; } if (navigator.userAgentData?.brands) { diff --git a/source/vscode/src/webviewPanel.ts b/source/vscode/src/webviewPanel.ts index f02100c902..c1aa78f409 100644 --- a/source/vscode/src/webviewPanel.ts +++ b/source/vscode/src/webviewPanel.ts @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -declare const __PLATFORM_DIR__: string; - import * as vscode from "vscode"; import { IOperationInfo, @@ -33,7 +31,7 @@ import { } from "./telemetry"; import { getRandomGuid } from "./utils"; import { getPauliNoiseModel, getQubitLossSetting } from "./config"; -import { qsharpExtensionId } from "./common"; +import { getPlatformEnv, qsharpExtensionId } from "./common"; import { resourceEstimateCommand } from "./estimate"; const QSharpWebViewType = "qsharp-webview"; @@ -49,7 +47,7 @@ export function registerWebViewCommands(context: ExtensionContext) { const compilerWorkerScriptPath = Uri.joinPath( context.extensionUri, - `./out/${__PLATFORM_DIR__}/compilerWorker.js`, + `./out/${getPlatformEnv()}/compilerWorker.js`, ).toString(); context.subscriptions.push( From d4aa75ad3b5f679240b155b83a462539f4581fb1 Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 8 Apr 2026 11:41:23 -0700 Subject: [PATCH 15/16] export messageHandler for backwards compatibility --- source/npm/qsharp/src/compiler/worker.ts | 3 ++- source/npm/qsharp/src/debug-service/worker.ts | 3 ++- source/npm/qsharp/src/language-service/worker.ts | 3 ++- source/playground/src/compiler-worker.ts | 4 +++- source/playground/src/language-service-worker.ts | 4 +++- 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/source/npm/qsharp/src/compiler/worker.ts b/source/npm/qsharp/src/compiler/worker.ts index b9ff887669..16ac45bff1 100644 --- a/source/npm/qsharp/src/compiler/worker.ts +++ b/source/npm/qsharp/src/compiler/worker.ts @@ -4,5 +4,6 @@ import { createWorker } from "../workers/worker.js"; import { compilerProtocol } from "./compiler.js"; -const messageHandler = createWorker(compilerProtocol); +// message handler exported for backwards compatibility +export const messageHandler = createWorker(compilerProtocol); addEventListener("message", messageHandler); diff --git a/source/npm/qsharp/src/debug-service/worker.ts b/source/npm/qsharp/src/debug-service/worker.ts index c9e1e1ee2f..1e03081d8a 100644 --- a/source/npm/qsharp/src/debug-service/worker.ts +++ b/source/npm/qsharp/src/debug-service/worker.ts @@ -4,5 +4,6 @@ import { createWorker } from "../workers/worker.js"; import { debugServiceProtocol } from "./debug-service.js"; -const messageHandler = createWorker(debugServiceProtocol); +// message handler exported for backwards compatibility +export const messageHandler = createWorker(debugServiceProtocol); addEventListener("message", messageHandler); diff --git a/source/npm/qsharp/src/language-service/worker.ts b/source/npm/qsharp/src/language-service/worker.ts index 13d58ed4f4..dc25d12e7c 100644 --- a/source/npm/qsharp/src/language-service/worker.ts +++ b/source/npm/qsharp/src/language-service/worker.ts @@ -4,5 +4,6 @@ import { createWorker } from "../workers/worker.js"; import { languageServiceProtocol } from "./language-service.js"; -const messageHandler = createWorker(languageServiceProtocol); +// message handler exported for backwards compatibility +export const messageHandler = createWorker(languageServiceProtocol); addEventListener("message", messageHandler); diff --git a/source/playground/src/compiler-worker.ts b/source/playground/src/compiler-worker.ts index 44600e108c..9659220212 100644 --- a/source/playground/src/compiler-worker.ts +++ b/source/playground/src/compiler-worker.ts @@ -1,4 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import "qsharp-lang/compiler-worker"; +import { messageHandler } from "qsharp-lang/compiler-worker"; + +self.onmessage = messageHandler; diff --git a/source/playground/src/language-service-worker.ts b/source/playground/src/language-service-worker.ts index d492b8da55..2b4e4ca155 100644 --- a/source/playground/src/language-service-worker.ts +++ b/source/playground/src/language-service-worker.ts @@ -1,4 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -import "qsharp-lang/language-service-worker"; +import { messageHandler } from "qsharp-lang/language-service-worker"; + +self.onmessage = messageHandler; From 1de5b8930aa7831070f38cf3b443343b44af967e Mon Sep 17 00:00:00 2001 From: joao-boechat Date: Wed, 8 Apr 2026 12:13:42 -0700 Subject: [PATCH 16/16] update comments and README for qsharp-lang package --- source/npm/qsharp/README.md | 27 ++++------------------ source/npm/qsharp/src/compiler/compiler.ts | 2 -- source/npm/qsharp/src/log.ts | 2 +- source/npm/qsharp/src/main.ts | 3 +-- 4 files changed, 7 insertions(+), 27 deletions(-) diff --git a/source/npm/qsharp/README.md b/source/npm/qsharp/README.md index 814d2847b0..7939fbb65d 100644 --- a/source/npm/qsharp/README.md +++ b/source/npm/qsharp/README.md @@ -11,28 +11,11 @@ to it when calling the `loadWasmModule` method so it may be located and loaded. ## Node and browser support -wasm-bindgen generates different files for the browser and Node.js environments. The wasm is slightly -different, and the loader code is quite different. This can be seen in `./lib/web/qsc_wasm.cjs` -and `./lib/nodejs/qsc_wasm.js` files respectively. Specifically, the web environment loads the wasm -file using async web APIs such as `fetch` with a URI, and Node.js uses `require` to load the `fs` module -and calls to `readFileSync`. Once the wasm module is loaded however, the exported APIs are used -in a similar manner. - -To support using this npm package from both environments, the package uses "conditional exports" - to expose one -entry point for Node.js, and another for browsers. The distinct entry points uses their respective -loader to load the wasm module for the platform, and then expose functionality that uses the -loaded module via common code. - -When bundling for the web, bundlers such as esbuild will automatically use the default entry point, -whereas when loaded as a Node.js module, it will use the "node" entry point. - -Note that TypeScript seems to add the ['import', 'types', 'node'] conditions by default when -searching the Node.js `exports`, and so will always find the 'node' export before the 'default' -export. To resolve this, a 'browser' condition was added (which is same as 'default' but earlier -than 'node') and the tsconfig compiler option `"customConditions": ["browser"]` should be added -(requires TypeScript 5.0 or later). esbuild also adds the 'browser' condition when bundling for -the browser (see ). +This package is platform-agnostic, using a single entry point (`main.ts`) for both browser +and Node.js environments. The wasm module and JavaScript glue code can be found in `./lib/web/`. +Consumers must bundle the package for their target platform so that external +dependencies (such as the `web-worker` package) are resolved correctly for the +runtime environment. ## Design diff --git a/source/npm/qsharp/src/compiler/compiler.ts b/source/npm/qsharp/src/compiler/compiler.ts index 7d8115334c..1a175e7f0e 100644 --- a/source/npm/qsharp/src/compiler/compiler.ts +++ b/source/npm/qsharp/src/compiler/compiler.ts @@ -30,8 +30,6 @@ import { } from "./events.js"; import { callAndTransformExceptions } from "../diagnostics.js"; -// The wasm types generated for the node.js bundle are just the exported APIs, -// so use those as the set used by the shared compiler type Wasm = typeof import("../../lib/web/qsc_wasm.js"); // These need to be async/promise results for when communicating across a WebWorker, however diff --git a/source/npm/qsharp/src/log.ts b/source/npm/qsharp/src/log.ts index ab2f952db3..402acbf172 100644 --- a/source/npm/qsharp/src/log.ts +++ b/source/npm/qsharp/src/log.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// Logging infrastructure for JavaScript environments (e.g. browser and node.js) +// Logging infrastructure shared across browser and Node.js environments // // Ideally this should be the only module to have global side effects and run code // on module load (i.e. other modules should consist almost entirely of declarations diff --git a/source/npm/qsharp/src/main.ts b/source/npm/qsharp/src/main.ts index d8e04412e4..9e9f2abdfb 100644 --- a/source/npm/qsharp/src/main.ts +++ b/source/npm/qsharp/src/main.ts @@ -1,8 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -// This module is the default entry point for browser and node environments, still named this way for -// backward compatibility. +// This module is the single entry point for both browser and Node.js environments. import * as wasm from "../lib/web/qsc_wasm.js"; import initWasm, {