val-town / deno-ata

support for type acquisition with url imports, npm, jsr prefixes
https://val-town.github.io/deno-ata/
7 stars 0 forks source link

Not working #5

Open nandorojo opened 2 months ago

nandorojo commented 2 months ago
Screenshot 2024-06-14 at 3 18 22 PM

Tried the example app using npm i && npm run dev, but it doesn't pick up the types. I'll see if I can find why. My guess is it's not updating the TS env accordingly, because the network tab does indeed show the files fetching.

nandorojo commented 2 months ago

I can't seem to get this working. However, I'll share the solution I made myself which has been working for me. Definitely open to feedback there though.

Usage

extensions: [..., tsAta({ env })]

Code

import { EditorView } from '@codemirror/view'
import { tsFacet } from '@valtown/codemirror-ts'
import {
  setupTypeAcquisition,
  // @ts-ignore
  getReferencesForModule,
} from '@typescript/ata'
import typescript from 'typescript'
import type { VirtualTypeScriptEnvironment } from '@typescript/vfs'
import { createOrUpdateFile } from 'app/features/code-editor/typescript/vendor/sync/update'
import { type VirtualTypeScriptEnvironment } from '@typescript/vfs'

const getReferences =
  getReferencesForModule as typeof import('@typescript/ata/src/index').getReferencesForModule

/**
 * Get dependencies for the TypeScript environment
 */
export function tsAta({ env }: { env: VirtualTypeScriptEnvironment }) {
  console.log('[ata][ts]')
  const ata = setupTypeAcquisition({
    typescript,
    projectName: 'typescript-ata',
    logger: console,
    delegate: {
      receivedFile: (code: string, path: string) => {
        // Add code to your runtime at the path...
        createOrUpdateFile(env, path, code)
      }
    },
  })
  let first = true
  return EditorView.updateListener.of((update) => {
    const config = update.view.state.facet(tsFacet)
    if (!config) return
    if (!update.docChanged && !first) return
    first = false

    let content = update.state.doc.toString() || ' '

    getReferences(typescript, content).forEach(({ module }) => {
      if (module.startsWith('npm:')) {
        // create a declaration file from npm:package → package
        const [name, version] = module.slice(4).split('@')

        content = content.replaceAll(`"${module}"`, `'${module}'`) // single quotes

        content = content.replaceAll(
          `'${module}'`,
          // the new line is important to not break require statements etc with the comment
          `'${name}' // types: ${version ?? 'latest'}\n`
        )

        createOrUpdateFile(
          env,
          `node_modules/${name}/__custom-deno-types.d.ts`,
          `
declare module '${module}' {
  export * from '${name}'
}            
`
        )
      }
    })

    ata(content)
  })
}

export function createOrUpdateFile(
  env: VirtualTypeScriptEnvironment,
  path: string,
  code: string
): void {
  if (!env.getSourceFile(path)) {
    env.createFile(path, code)
  } else {
    env.updateFile(path, code)
  }
}
tmcw commented 2 months ago

That seems brilliant! I'll tinker with something similar.

nandorojo commented 1 month ago

Just to add to the discussion here:

The solution I posted above has been working. However, using the TS ATA directly in the browser has some downsides I'm finding:

  1. It sends a ton of requests from the browser directly, which feels like it's slowing things down a bit...
  2. There's not much caching going on at all

I wonder if there could be a way to run it via a middleware server or something that could:

  1. Cache the results of files
  2. Run the actual TS server quickly to generate the final .d.ts file(s) that the web browser will use. If possible bundle it into one file. Maybe this could just use tsc or a faster equivalent?
  3. Return that bundled .d.ts file to the browser, and cache it for future requests

For example, you could call your server with something like request('/ts-ata', { code }) and it returns the .d.ts file(s) to be used by the language server on the browser.

Have you guys experimented with something like this @tmcw?

nandorojo commented 1 month ago

Some downsides to the above approach:

  1. Currently ata provides a listener for each file that comes in as it streams in, which is useful for getting the types of some packages while others are loading. So for the first time, this would have a trade-off.

Potential Solutions:

  1. Pre-compute + cache for users from the DB directly.
  2. Or, use HTTP streams to give the response LLM-style when it's not a cache hit. And if it is a cache hit, return it.

One clear issue with this approach is that, every time someone types, you need a new cache key. However, this can be easily solved by using a hash of the result of getReferencesForModule as the cache key. We only need to cache things based on which modules are in a file so that should do the trick, I think.

nandorojo commented 1 month ago

Lastly, one simple thing I did to make this much faster was to only call ata(content) if the result of getReferences was greater than 0.

(Apologies for the many Sunday morning messages)