From e47d760ebaa74516e6c8150cb42d4575fd23b93d Mon Sep 17 00:00:00 2001 From: Guilhem Lemouel Date: Mon, 20 Jul 2026 12:38:04 +0200 Subject: [PATCH] refactor(editor): extract ATA setup and lsp token root Headless linting needs the same acquired types as the editor, npm declarations and relative-import models alike. Move the ATA delegate and token root to lint/typescriptAta.ts; the editor keeps its trigger logic and supplies the model-revalidation nudge via onLocalFileRegistered. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/src/lib/components/Editor.svelte | 101 +++--------------- .../src/lib/components/lint/typescriptAta.ts | 99 +++++++++++++++++ 2 files changed, 113 insertions(+), 87 deletions(-) create mode 100644 frontend/src/lib/components/lint/typescriptAta.ts diff --git a/frontend/src/lib/components/Editor.svelte b/frontend/src/lib/components/Editor.svelte index 886f766e4f..d704b71f38 100644 --- a/frontend/src/lib/components/Editor.svelte +++ b/frontend/src/lib/components/Editor.svelte @@ -33,7 +33,6 @@ dbSchemas, type DBSchema, codeCompletionSessionEnabled, - lspTokenStore, formatOnSave, vimMode, relativeLineNumbers @@ -43,7 +42,7 @@ import { editorFontSize } from '$lib/editorFontSize.svelte' import { workspaceStore } from '$lib/stores' import DdlMigrationGuard from './DdlMigrationGuard.svelte' - import { type Preview, type ScriptLang, UserService } from '$lib/gen' + import { type Preview, type ScriptLang } from '$lib/gen' import type { Text } from 'yjs' import { initializeVscode, @@ -74,11 +73,10 @@ POSTGRES_TYPES, SNOWFLAKE_TYPES } from '$lib/consts' - import { setupTypeAcquisition, type DepsToGet } from '$lib/ata/index' - import { initWasmTs, type InferAssetsSqlQueryDetails } from '$lib/infer' + import { type DepsToGet } from '$lib/ata/index' + import { type InferAssetsSqlQueryDetails } from '$lib/infer' import { initVim } from './monaco_keybindings' import { updateSqlQueriesInWorker, waitForWorkerInitialization } from './sqlTypeService' - import { parseTypescriptDeps } from '$lib/relative_imports' import { scriptLangToEditorLang } from '$lib/scripts' import { @@ -108,6 +106,7 @@ ensureResourceTypeNamespace, fetchCustomWmillTypesData } from './lint/typescriptExtraLibs' + import { createWindmillAta, genAtaRoot } from './lint/typescriptAta' import { aiChatManager } from './copilot/chat/AIChatManager.svelte' import type { Selection } from 'monaco-editor' import { canHavePreprocessor, getPreprocessorModuleCode } from '$lib/script_helpers' @@ -1149,14 +1148,12 @@ } } - const hostname = getHostname() - let encodedImportMap = '' if (useWebsockets) { if (lang == 'typescript' && scriptLang === 'deno') { ata = undefined - let root = await genRoot(hostname) + let root = await genAtaRoot($workspaceStore ?? '') const importMap = { imports: { 'file:///': root + '/' @@ -1334,10 +1331,6 @@ let yPadding = MONACO_Y_PADDING - function getHostname() { - return BROWSER ? window.location.protocol + '//' + window.location.host : 'SSR' - } - function handlePathChange() { console.log('path changed, reloading language server', initialPath, path) initialPath = path @@ -1756,72 +1749,21 @@ ) { absolutePathExtraLibs.forEach((d) => d.dispose()) absolutePathExtraLibs.clear() - const hostname = getHostname() - const addLibraryToRuntime = async (code: string, _path: string) => { - const path = 'file://' + _path - let uri = mUri.parse(path) - console.log('adding library to runtime', path) - typescriptDefaults.addExtraLib(code, path) - try { - await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(code)) - } catch (e) { - console.log('error writing file', e) - } - } - - const addLocalFile = async (code: string, _path: string) => { - if (destroyed) return - let p = new URL(_path, uri).href - let nuri = mUri.parse(p) - console.log('adding local file', _path, nuri.toString()) - // Monaco's TS service resolves relative imports against the importer's URI (finding the - // model), but absolute paths like "/u/admin/foo" are looked up as raw paths and miss the - // `file://` model. Register them as extra libs so TS can resolve them. - if (_path.startsWith('/')) { - absolutePathExtraLibs.get(_path)?.dispose() - absolutePathExtraLibs.set(_path, typescriptDefaults.addExtraLib(code, _path)) - } - if (editor) { - let localModel = meditor.getModel(nuri) - if (localModel) { - localModel.setValue(code) - } else { - meditor.createModel(code, 'typescript', nuri) - } + ata = await createWindmillAta({ + root: await genAtaRoot($workspaceStore ?? ''), + scriptPath: path, + modelUri: uri, + absolutePathExtraLibs, + isCancelled: () => destroyed, + onLocalFileRegistered: () => { + if (!editor) return try { - if (model) { - model?.setValue(model.getValue()) - } + model?.setValue(model.getValue()) } catch (e) { console.log('error resetting model', e) } } - } - await initWasmTs() - const root = await genRoot(hostname) - console.log('SETUP TYPE ACQUISITION', { root, path }) - ata = setupTypeAcquisition({ - projectName: 'Windmill', - depsParser: (c) => { - return parseTypescriptDeps(c) - }, - root, - scriptPath: path, - logger: console, - delegate: { - receivedFile: addLibraryToRuntime, - localFile: addLocalFile, - progress: (downloaded: number, total: number) => { - // console.log({ dl, ttl }) - }, - started: () => { - console.log('ATA start') - }, - finished: (f) => { - console.log('ATA done') - } - } }) if (scriptLang == 'bun') { ata?.('import "bun-types"') @@ -1915,21 +1857,6 @@ absolutePathExtraLibs.clear() }) - async function genRoot(hostname: string) { - let token = $lspTokenStore - if (!token) { - let expiration = new Date() - expiration.setHours(expiration.getHours() + 72) - const newToken = await UserService.createToken({ - requestBody: { label: 'Ephemeral lsp token', expiration: expiration.toISOString() } - }) - $lspTokenStore = newToken - token = newToken - } - let root = hostname + '/api/scripts_u/tokened_raw/' + $workspaceStore + '/' + token - return root - } - function acceptCodeChanges() { const mode = aiChatEditorHandler?.getReviewMode?.() if (mode === 'revert') { diff --git a/frontend/src/lib/components/lint/typescriptAta.ts b/frontend/src/lib/components/lint/typescriptAta.ts new file mode 100644 index 0000000000..a868ac5608 --- /dev/null +++ b/frontend/src/lib/components/lint/typescriptAta.ts @@ -0,0 +1,99 @@ +import { BROWSER } from 'esm-env' +import * as vscode from 'vscode' +import { editor as meditor, Uri as mUri } from 'monaco-editor' +import { typescriptDefaults } from '@codingame/monaco-vscode-standalone-typescript-language-features' +import { get } from 'svelte/store' +import { setupTypeAcquisition, type DepsToGet } from '$lib/ata/index' +import { initWasmTs } from '$lib/infer' +import { parseTypescriptDeps } from '$lib/relative_imports' +import { UserService } from '$lib/gen' +import { lspTokenStore } from '$lib/stores' + +// Automatic Type Acquisition: fetches npm and relative-import declarations into the +// global typescriptDefaults. Without it, TypeScript reports every third-party import +// as unresolved, so headless linting and the editor must acquire types the same way. + +export async function genAtaRoot(workspace: string): Promise { + let token = get(lspTokenStore) + if (!token) { + const expiration = new Date() + expiration.setHours(expiration.getHours() + 72) + const newToken = await UserService.createToken({ + requestBody: { label: 'Ephemeral lsp token', expiration: expiration.toISOString() } + }) + lspTokenStore.set(newToken) + token = newToken + } + const hostname = BROWSER ? window.location.protocol + '//' + window.location.host : 'SSR' + return hostname + '/api/scripts_u/tokened_raw/' + workspace + '/' + token +} + +export interface WindmillAtaOptions { + root: string + /** Script path passed to ATA for resolving relative imports server-side. */ + scriptPath: string | undefined + /** URI of the model being typed; relative import paths resolve against it. */ + modelUri: string + absolutePathExtraLibs: Map void }> + isCancelled?: () => boolean + /** Called after a relative-import model is registered, to nudge revalidation. */ + onLocalFileRegistered?: () => void +} + +export async function createWindmillAta( + opts: WindmillAtaOptions +): Promise<(source: string | DepsToGet) => Promise> { + const addLibraryToRuntime = async (code: string, _path: string) => { + const path = 'file://' + _path + const uri = mUri.parse(path) + console.log('adding library to runtime', path) + typescriptDefaults.addExtraLib(code, path) + try { + await vscode.workspace.fs.writeFile(uri, new TextEncoder().encode(code)) + } catch (e) { + console.log('error writing file', e) + } + } + + const addLocalFile = async (code: string, _path: string) => { + if (opts.isCancelled?.()) return + const p = new URL(_path, opts.modelUri).href + const nuri = mUri.parse(p) + console.log('adding local file', _path, nuri.toString()) + // Monaco's TS service resolves relative imports against the importer's URI (finding the + // model), but absolute paths like "/u/admin/foo" are looked up as raw paths and miss the + // `file://` model. Register them as extra libs so TS can resolve them. + if (_path.startsWith('/')) { + opts.absolutePathExtraLibs.get(_path)?.dispose() + opts.absolutePathExtraLibs.set(_path, typescriptDefaults.addExtraLib(code, _path)) + } + const localModel = meditor.getModel(nuri) + if (localModel) { + localModel.setValue(code) + } else { + meditor.createModel(code, 'typescript', nuri) + } + opts.onLocalFileRegistered?.() + } + + await initWasmTs() + console.log('SETUP TYPE ACQUISITION', { root: opts.root, path: opts.scriptPath }) + return setupTypeAcquisition({ + projectName: 'Windmill', + depsParser: (c) => parseTypescriptDeps(c), + root: opts.root, + scriptPath: opts.scriptPath, + logger: console, + delegate: { + receivedFile: addLibraryToRuntime, + localFile: addLocalFile, + progress: (_downloaded: number, _total: number) => {}, + started: () => { + console.log('ATA start') + }, + finished: (_f) => { + console.log('ATA done') + } + } + }) +}