diff --git a/apps/website/src/vite-env.d.ts b/apps/website/src/vite-env.d.ts index 7636b5d..fdf2e41 100644 --- a/apps/website/src/vite-env.d.ts +++ b/apps/website/src/vite-env.d.ts @@ -2,3 +2,8 @@ declare module '*.wasm?url' { const url: string; export default url; } + +declare module '*.d.ts?raw' { + const source: string; + export default source; +} diff --git a/apps/website/src/workers/tempblotLanguageServer.worker.ts b/apps/website/src/workers/tempblotLanguageServer.worker.ts index cdc301e..9d1e2e0 100644 --- a/apps/website/src/workers/tempblotLanguageServer.worker.ts +++ b/apps/website/src/workers/tempblotLanguageServer.worker.ts @@ -2,6 +2,7 @@ import { createConnection, createServer, createTypeScriptProject, + FileType, type InitializeParams, type InitializeResult, } from '@volar/language-server/browser'; @@ -14,17 +15,24 @@ import { create as createTypeScriptServicePlugins } from 'volar-service-typescri const connection = createConnection(); const server = createServer(connection); +const typeScriptLibPath = '/node_modules/typescript/lib'; +const nodeTypesPath = '/playground/node_modules/@types/node/index.d.ts'; connection.onInitialize(async (parameters: InitializeParams): Promise => { return server.initialize( parameters, createTypeScriptProject( - typescript, + playgroundTypescript, undefined, - () => ({ languagePlugins: [createTempblotLanguagePlugin()] }), + () => ({ + languagePlugins: [createTempblotLanguagePlugin()], + setup({ project }) { + patchPlaygroundTypeScriptHost(project.typescript?.languageServiceHost); + }, + }), ), [ - ...createTypeScriptServicePlugins(typescript), + ...createTypeScriptServicePlugins(playgroundTypescript), createTempblotServicePlugin(), ], ); @@ -35,3 +43,225 @@ connection.onInitialized(() => { }); connection.listen(); + +function installPlaygroundFileSystem() { + server.fileSystem.install('file', { + readFile(uri) { + return virtualTypeFilesByUri.get(toFileUriString(uri)); + }, + stat(uri) { + const uriString = toFileUriString(uri); + if (virtualTypeFilesByUri.has(uriString)) { + return { type: FileType.File, ctime: 0, mtime: 0, size: virtualTypeFilesByUri.get(uriString)!.length }; + } + if (hasVirtualChild(virtualTypeFilesByUri, uriString)) { + return { type: FileType.Directory, ctime: 0, mtime: 0, size: 0 }; + } + }, + readDirectory(uri) { + const uriString = toFileUriString(uri); + const prefix = uriString.endsWith('/') ? uriString : `${uriString}/`; + const entries = new Map(); + + for (const file of virtualTypeFilesByUri.keys()) { + if (!file.startsWith(prefix)) { + continue; + } + + const [name, rest] = file.slice(prefix.length).split('/', 2); + entries.set(name, rest ? FileType.Directory : FileType.File); + } + + return [...entries]; + }, + }); +} + +function patchPlaygroundTypeScriptHost(host: typescript.LanguageServiceHost | undefined) { + if (!host) { + return; + } + + const originalReadFile = host.readFile?.bind(host); + const originalFileExists = host.fileExists?.bind(host); + const originalDirectoryExists = host.directoryExists?.bind(host); + const originalReadDirectory = host.readDirectory?.bind(host); + const originalGetScriptSnapshot = host.getScriptSnapshot.bind(host); + const originalGetScriptVersion = host.getScriptVersion.bind(host); + const originalGetDefaultLibFileName = host.getDefaultLibFileName?.bind(host); + const originalResolveLibrary = host.resolveLibrary?.bind(host); + const originalResolveTypeReferenceDirectiveReferences = host.resolveTypeReferenceDirectiveReferences?.bind(host); + + host.readFile = (fileName) => { + const normalized = normalizeFileName(fileName); + const source = virtualTypeFilesByPath.get(normalized); + return source ?? originalReadFile?.(fileName); + }; + host.fileExists = (fileName) => { + const normalized = normalizeFileName(fileName); + return virtualTypeFilesByPath.has(normalized) || originalFileExists?.(fileName) || false; + }; + host.directoryExists = (directoryName) => { + const normalized = normalizeFileName(directoryName); + return hasVirtualChild(virtualTypeFilesByPath, normalized) + || originalDirectoryExists?.(directoryName) + || false; + }; + host.readDirectory = (rootDir, extensions, excludes, includes, depth) => { + const normalizedRoot = normalizeFileName(rootDir); + const virtualFiles = [...virtualTypeFilesByPath.keys()].filter((file) => file.startsWith(`${normalizedRoot}/`)); + return [...new Set([ + ...virtualFiles, + ...(originalReadDirectory?.(rootDir, extensions, excludes, includes, depth) ?? []), + ])]; + }; + host.getScriptSnapshot = (fileName) => { + const normalized = normalizeFileName(fileName); + const source = virtualTypeFilesByPath.get(normalized); + if (source !== undefined) { + return playgroundTypescript.ScriptSnapshot.fromString(source); + } + return originalGetScriptSnapshot(fileName); + }; + host.getScriptVersion = (fileName) => { + if (virtualTypeFilesByPath.has(normalizeFileName(fileName))) { + return '0'; + } + return originalGetScriptVersion(fileName); + }; + host.getDefaultLibFileName = (options) => { + const libName = playgroundTypescript.getDefaultLibFileName(options).split('/').pop()!; + const virtualPath = `${typeScriptLibPath}/${libName}`; + return virtualTypeFilesByPath.has(virtualPath) ? virtualPath : originalGetDefaultLibFileName?.(options) ?? virtualPath; + }; + host.getDefaultLibLocation = () => typeScriptLibPath; + host.resolveLibrary = (libraryName, resolveFrom, options, libFileName) => { + const virtualPath = `${typeScriptLibPath}/${libFileName}`; + if (virtualTypeFilesByPath.has(virtualPath)) { + return { + resolvedModule: { + resolvedFileName: virtualPath, + extension: playgroundTypescript.Extension.Dts, + isExternalLibraryImport: false, + }, + }; + } + return originalResolveLibrary?.(libraryName, resolveFrom, options, libFileName) ?? { resolvedModule: undefined }; + }; + host.resolveTypeReferenceDirectiveReferences = (typeDirectiveReferences, containingFile, redirectedReference, options, containingSourceFile, reusedNames) => { + if (originalResolveTypeReferenceDirectiveReferences) { + const originalResults = originalResolveTypeReferenceDirectiveReferences( + typeDirectiveReferences, + containingFile, + redirectedReference, + options, + containingSourceFile, + reusedNames, + ); + + return originalResults.map((result, index) => result.resolvedTypeReferenceDirective ? result : resolvePlaygroundTypeReference(typeDirectiveReferences[index])); + } + + return typeDirectiveReferences.map(resolvePlaygroundTypeReference); + }; + + function resolvePlaygroundTypeReference(typeDirectiveReference: string | typescript.FileReference) { + const typeName = typeof typeDirectiveReference === 'string' ? typeDirectiveReference : typeDirectiveReference.fileName; + const virtualPath = `/playground/node_modules/@types/${typeName}/index.d.ts`; + + if (!virtualTypeFilesByPath.has(virtualPath)) { + return { resolvedTypeReferenceDirective: undefined }; + } + + return { + resolvedTypeReferenceDirective: { + primary: true, + resolvedFileName: virtualPath, + typeName, + }, + }; + } +} + +function createVirtualTypeFiles() { + return new Map([ + ['/playground/tsconfig.json', JSON.stringify({ + compilerOptions: { + target: 'ES2021', + lib: ['ES2021'], + types: ['node'], + strict: true, + module: 'NodeNext', + moduleResolution: 'NodeNext', + allowSyntheticDefaultImports: true, + skipLibCheck: true, + }, + include: ['**/*.blot'], + })], + ['/playground/node_modules/@types/node/package.json', JSON.stringify({ + name: '@types/node', + version: '0.0.0-playground', + types: 'index.d.ts', + })], + [nodeTypesPath, ` +declare var __dirname: string; +declare var __filename: string; +declare var global: typeof globalThis; +declare function setImmediate(callback: (...args: unknown[]) => void, ...args: unknown[]): unknown; +declare function clearImmediate(immediate: unknown): void; +declare var Buffer: { + from(input: string | ArrayBuffer | ArrayBufferView, encoding?: string): Uint8Array; + isBuffer(value: unknown): value is Uint8Array; +}; +declare function require(id: string): unknown; +declare namespace NodeJS { + interface ProcessEnv { + [key: string]: string | undefined; + } + interface Process { + argv: string[]; + cwd(): string; + env: ProcessEnv; + exit(code?: number): never; + nextTick(callback: (...args: unknown[]) => void, ...args: unknown[]): void; + } +} +declare var process: NodeJS.Process; +`], + ...Object.entries(typeScriptLibSources).map(([name, source]) => [`${typeScriptLibPath}/${name}`, source] as const), + ]); +} + +function toFileUriString(uri: { scheme: string; path: string; toString(): string }) { + return uri.scheme === 'file' ? `file://${uri.path}` : uri.toString(); +} + +function normalizeFileName(fileName: string) { + return fileName.replace(/\\/g, '/'); +} + +function hasVirtualChild(files: Map, path: string) { + return [...files.keys()].some((file) => file.startsWith(`${path}/`)); +} + +const typeScriptLibSources = Object.fromEntries( + Object.entries(import.meta.glob('/node_modules/typescript/lib/lib*.d.ts', { + eager: true, + query: '?raw', + import: 'default', + })).map(([path, source]) => [path.split('/').pop()!, source as string]), +); + +const playgroundTypescript = { + ...typescript, + getDefaultLibFilePath(options: typescript.CompilerOptions) { + return `${typeScriptLibPath}/${typescript.getDefaultLibFileName(options)}`; + }, +} satisfies typeof typescript; + +const virtualTypeFilesByPath = createVirtualTypeFiles(); +const virtualTypeFilesByUri = new Map( + [...virtualTypeFilesByPath].map(([path, source]) => [`file://${path}`, source]), +); + +installPlaygroundFileSystem(); -- 2.51.2 From 9160e77c72d47bed99c6388b7eb6c43f1a261514 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 14 May 2026 00:06:28 -0400 Subject: [PATCH 2/5] chore: fix node types --- apps/website/package.json | 1 + .../workers/tempblotLanguageServer.worker.ts | 43 ++++++++----------- pnpm-lock.yaml | 3 ++ 3 files changed, 21 insertions(+), 26 deletions(-) diff --git a/apps/website/package.json b/apps/website/package.json index 5408196..9760776 100644 --- a/apps/website/package.json +++ b/apps/website/package.json @@ -32,6 +32,7 @@ "vscode-uri": "^3.1.0" }, "devDependencies": { + "@types/node": "^24.5.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3" } diff --git a/apps/website/src/workers/tempblotLanguageServer.worker.ts b/apps/website/src/workers/tempblotLanguageServer.worker.ts index 9d1e2e0..828bd69 100644 --- a/apps/website/src/workers/tempblotLanguageServer.worker.ts +++ b/apps/website/src/workers/tempblotLanguageServer.worker.ts @@ -16,7 +16,7 @@ import { create as createTypeScriptServicePlugins } from 'volar-service-typescri const connection = createConnection(); const server = createServer(connection); const typeScriptLibPath = '/node_modules/typescript/lib'; -const nodeTypesPath = '/playground/node_modules/@types/node/index.d.ts'; +const nodeTypesPath = '/playground/node_modules/@types/node'; connection.onInitialize(async (parameters: InitializeParams): Promise => { return server.initialize( @@ -86,6 +86,7 @@ function patchPlaygroundTypeScriptHost(host: typescript.LanguageServiceHost | un const originalFileExists = host.fileExists?.bind(host); const originalDirectoryExists = host.directoryExists?.bind(host); const originalReadDirectory = host.readDirectory?.bind(host); + const originalGetScriptFileNames = host.getScriptFileNames.bind(host); const originalGetScriptSnapshot = host.getScriptSnapshot.bind(host); const originalGetScriptVersion = host.getScriptVersion.bind(host); const originalGetDefaultLibFileName = host.getDefaultLibFileName?.bind(host); @@ -115,6 +116,10 @@ function patchPlaygroundTypeScriptHost(host: typescript.LanguageServiceHost | un ...(originalReadDirectory?.(rootDir, extensions, excludes, includes, depth) ?? []), ])]; }; + host.getScriptFileNames = () => [...new Set([ + ...originalGetScriptFileNames(), + ...nodeTypeRootFiles, + ])]; host.getScriptSnapshot = (fileName) => { const normalized = normalizeFileName(fileName); const source = virtualTypeFilesByPath.get(normalized); @@ -203,31 +208,7 @@ function createVirtualTypeFiles() { version: '0.0.0-playground', types: 'index.d.ts', })], - [nodeTypesPath, ` -declare var __dirname: string; -declare var __filename: string; -declare var global: typeof globalThis; -declare function setImmediate(callback: (...args: unknown[]) => void, ...args: unknown[]): unknown; -declare function clearImmediate(immediate: unknown): void; -declare var Buffer: { - from(input: string | ArrayBuffer | ArrayBufferView, encoding?: string): Uint8Array; - isBuffer(value: unknown): value is Uint8Array; -}; -declare function require(id: string): unknown; -declare namespace NodeJS { - interface ProcessEnv { - [key: string]: string | undefined; - } - interface Process { - argv: string[]; - cwd(): string; - env: ProcessEnv; - exit(code?: number): never; - nextTick(callback: (...args: unknown[]) => void, ...args: unknown[]): void; - } -} -declare var process: NodeJS.Process; -`], + ...Object.entries(nodeTypeSources).map(([path, source]) => [`${nodeTypesPath}/${path}`, source] as const), ...Object.entries(typeScriptLibSources).map(([name, source]) => [`${typeScriptLibPath}/${name}`, source] as const), ]); } @@ -252,6 +233,14 @@ const typeScriptLibSources = Object.fromEntries( })).map(([path, source]) => [path.split('/').pop()!, source as string]), ); +const nodeTypeSources = Object.fromEntries( + Object.entries(import.meta.glob('/node_modules/@types/node/**/*.d.ts', { + eager: true, + query: '?raw', + import: 'default', + })).map(([path, source]) => [path.split('/node/')[1], source as string]), +); + const playgroundTypescript = { ...typescript, getDefaultLibFilePath(options: typescript.CompilerOptions) { @@ -260,6 +249,8 @@ const playgroundTypescript = { } satisfies typeof typescript; const virtualTypeFilesByPath = createVirtualTypeFiles(); +const nodeTypeRootFiles = [...virtualTypeFilesByPath.keys()] + .filter((fileName) => fileName.startsWith(`${nodeTypesPath}/`) && fileName.endsWith('.d.ts')); const virtualTypeFilesByUri = new Map( [...virtualTypeFilesByPath].map(([path, source]) => [`file://${path}`, source]), ); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 73fc779..4a4a3c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -90,6 +90,9 @@ importers: specifier: ^3.1.0 version: 3.1.0 devDependencies: + '@types/node': + specifier: ^24.5.2 + version: 24.12.4 '@types/react': specifier: ^19.2.14 version: 19.2.14 -- 2.51.2 From d29691b4c764f90527aba254363abf1400b6bc8d Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 14 May 2026 00:19:06 -0400 Subject: [PATCH 3/5] fix: issues module resolution --- apps/website/src/workers/tempblotLanguageServer.worker.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/website/src/workers/tempblotLanguageServer.worker.ts b/apps/website/src/workers/tempblotLanguageServer.worker.ts index 828bd69..1932fd2 100644 --- a/apps/website/src/workers/tempblotLanguageServer.worker.ts +++ b/apps/website/src/workers/tempblotLanguageServer.worker.ts @@ -86,6 +86,7 @@ function patchPlaygroundTypeScriptHost(host: typescript.LanguageServiceHost | un const originalFileExists = host.fileExists?.bind(host); const originalDirectoryExists = host.directoryExists?.bind(host); const originalReadDirectory = host.readDirectory?.bind(host); + const originalGetCompilationSettings = host.getCompilationSettings.bind(host); const originalGetScriptFileNames = host.getScriptFileNames.bind(host); const originalGetScriptSnapshot = host.getScriptSnapshot.bind(host); const originalGetScriptVersion = host.getScriptVersion.bind(host); @@ -116,6 +117,11 @@ function patchPlaygroundTypeScriptHost(host: typescript.LanguageServiceHost | un ...(originalReadDirectory?.(rootDir, extensions, excludes, includes, depth) ?? []), ])]; }; + host.getCompilationSettings = () => ({ + ...originalGetCompilationSettings(), + module: playgroundTypescript.ModuleKind.NodeNext, + moduleResolution: playgroundTypescript.ModuleResolutionKind.NodeNext, + }); host.getScriptFileNames = () => [...new Set([ ...originalGetScriptFileNames(), ...nodeTypeRootFiles, -- 2.51.2 From c61b1633f0aa714112894f8a6f2a70219eda3809 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 14 May 2026 00:33:22 -0400 Subject: [PATCH 4/5] chore: fix handle files as ESM --- .../workers/tempblotLanguageServer.worker.ts | 1 + .../language-service/src/language-plugin.ts | 22 ++++++------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/apps/website/src/workers/tempblotLanguageServer.worker.ts b/apps/website/src/workers/tempblotLanguageServer.worker.ts index 1932fd2..d231433 100644 --- a/apps/website/src/workers/tempblotLanguageServer.worker.ts +++ b/apps/website/src/workers/tempblotLanguageServer.worker.ts @@ -196,6 +196,7 @@ function patchPlaygroundTypeScriptHost(host: typescript.LanguageServiceHost | un function createVirtualTypeFiles() { return new Map([ + ['/playground/package.json', JSON.stringify({ type: 'module' })], ['/playground/tsconfig.json', JSON.stringify({ compilerOptions: { target: 'ES2021', diff --git a/packages/language-service/src/language-plugin.ts b/packages/language-service/src/language-plugin.ts index 1199747..0e3e423 100644 --- a/packages/language-service/src/language-plugin.ts +++ b/packages/language-service/src/language-plugin.ts @@ -39,27 +39,19 @@ export function createTempblotLanguagePlugin(): LanguagePlugin< }, ], getServiceScript(root) { + return undefined; + }, + getExtraServiceScripts(fileName, root) { const code = root.embeddedCodes?.find( (code) => code.id === "combined_context", ); - if (!code) { - return undefined; - } - return { - code, - extension: ".ts", - scriptKind: 3 satisfies ts.ScriptKind.TS, - preventLeadingOffset: true, - }; - }, - getExtraServiceScripts(fileName, root) { - if (root.embeddedCodes) { - const code = root.embeddedCodes[0]; + + if (code) { return [ { - fileName: fileName + "." + code.id + ".ts", + fileName: fileName + "." + code.id + ".mts", code, - extension: ".ts", + extension: ".mts", scriptKind: 3, }, ]; -- 2.51.2 From 25b3dd6c98ca3134a9fe8b9af9a8a40af3ba4df6 Mon Sep 17 00:00:00 2001 From: Corbin Crutchley Date: Thu, 14 May 2026 00:44:47 -0400 Subject: [PATCH 5/5] chore: fix vsc and __ recs --- .../workers/tempblotLanguageServer.worker.ts | 2 +- .../language-service/src/language-plugin.ts | 44 +++++++++++++++---- 2 files changed, 37 insertions(+), 9 deletions(-) diff --git a/apps/website/src/workers/tempblotLanguageServer.worker.ts b/apps/website/src/workers/tempblotLanguageServer.worker.ts index d231433..ce238aa 100644 --- a/apps/website/src/workers/tempblotLanguageServer.worker.ts +++ b/apps/website/src/workers/tempblotLanguageServer.worker.ts @@ -25,7 +25,7 @@ connection.onInitialize(async (parameters: InitializeParams): Promise ({ - languagePlugins: [createTempblotLanguagePlugin()], + languagePlugins: [createTempblotLanguagePlugin({ serviceScriptMode: 'extraMts' })], setup({ project }) { patchPlaygroundTypeScriptHost(project.typescript?.languageServiceHost); }, diff --git a/packages/language-service/src/language-plugin.ts b/packages/language-service/src/language-plugin.ts index 0e3e423..3cdabca 100644 --- a/packages/language-service/src/language-plugin.ts +++ b/packages/language-service/src/language-plugin.ts @@ -11,10 +11,16 @@ import { import type * as ts from "typescript"; import { URI } from "vscode-uri"; -export function createTempblotLanguagePlugin(): LanguagePlugin< +interface TempblotLanguagePluginOptions { + serviceScriptMode?: "primary" | "extraMts"; +} + +export function createTempblotLanguagePlugin(options: TempblotLanguagePluginOptions = {}): LanguagePlugin< URI, TempblotVirtualCode > { + const serviceScriptMode = options.serviceScriptMode ?? "primary"; + return { getLanguageId(fileNameOrUri) { if (String(fileNameOrUri).endsWith(".blot")) { @@ -39,12 +45,28 @@ export function createTempblotLanguagePlugin(): LanguagePlugin< }, ], getServiceScript(root) { - return undefined; + if (serviceScriptMode === "extraMts") { + return undefined; + } + + const code = getCombinedContextCode(root); + if (!code) { + return undefined; + } + + return { + code, + extension: ".ts", + scriptKind: 3 satisfies ts.ScriptKind.TS, + preventLeadingOffset: true, + }; }, getExtraServiceScripts(fileName, root) { - const code = root.embeddedCodes?.find( - (code) => code.id === "combined_context", - ); + if (serviceScriptMode !== "extraMts") { + return []; + } + + const code = getCombinedContextCode(root); if (code) { return [ @@ -62,6 +84,12 @@ export function createTempblotLanguagePlugin(): LanguagePlugin< }; } +function getCombinedContextCode(root: VirtualCode) { + return root.embeddedCodes?.find( + (code) => code.id === "combined_context", + ); +} + export class TempblotVirtualCode implements VirtualCode { id = "root"; languageId = "tempblot"; @@ -119,14 +147,14 @@ function* getTempblotEmbeddedCodes( let combinedText = `${base}${setupText}\n\n// Output interpolations:\n`; const tsInterpolationMappings: CodeMapping[] = []; - interpolationsData.forEach((interp, index) => { - const interpLine = `const __interp_${index} = ${interp.expression};\n`; + interpolationsData.forEach((interp) => { + const interpLine = `(${interp.expression});\n`; const interpStartOffset = combinedText.length; combinedText += interpLine; // Map the interpolation expression to the original source const expressionStart = - interpStartOffset + `const __interp_${index} = `.length; + interpStartOffset + `(`.length; tsInterpolationMappings.push({ sourceOffsets: [output.startTagEnd! + interp.sourceStart], generatedOffsets: [expressionStart],