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) <noreply@anthropic.com>
This commit is contained in:
Guilhem Lemouel
2026-07-20 12:38:04 +02:00
parent 35f3196c4a
commit e47d760eba
2 changed files with 113 additions and 87 deletions
+14 -87
View File
@@ -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') {
@@ -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<string> {
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<string, { dispose: () => 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<void>> {
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')
}
}
})
}