3 Lit Development Resources
Kevin Schaaf edited this page 2025-02-06 10:54:20 -08:00
This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

TypeScript Compiler API & Related Tools Notes

TypeScript Compiler API

  • Resources:
  • Symbols
    • AST Nodes can have an associated Symbol, which is something that ultimately refers to a Declaration (use checker.getSymbolAtLocation(node) to retrieve it)
    • Symbols can have multiple associated Declarations, because of the following:
      • Classes (the interface + the constructor)
      • Namespace merging (e.g. lots of declare global { interface HTMLElementTagNameMap {...} } contribute to the final value of HTMLElementTagNameMap
    • A Symbol's Declaration may be a ts.ImportSpecifier if it was imported (it will not necessarily be the original variable/class/function/… declaration that created the thing)
      • To get the original declaration of an imported symbol from its declaring source file, use checker.getAliasedSymbol(symbol)
      • If the symbol was not imported, but rather its type came from an ambient .d.ts file (for example, lib.dom.d.ts for DOM globals), the declaration for the symbol retrieved from current source file will be its declaration in the .d.ts file (since there is no ts.importSpecifier in the current source file)
  • Types
    • Types can have representation in the AST when things are explicitly typed: e.g. in let foo: number | Promise<number>, there will be syntax nodes for the type; those AST nodes extend ts.TypeNode (which extend the AST node base class ts.Node)
    • We can also get the inferred type of a syntax node using checker.getTypeAtLocation(node). This returns a ts.Type, which is not a tree of syntax nodes, but rather a tree of type nodes.
    • The compiler API does not have any built-in visitor support for Type trees, as far as I can tell. However, it is possible to convert a ts.Type to a ts.TypeNode using checker.typeToTypeNode(type)
      • This generates syntax nodes from type nodes (as if one had walked the type node and used the ts.factory API to create equivalent syntax nodes
      • The TypeNodes are then walkable using ts.ForEachChild
      • The API docs note that these syntax nodes "are not checkable", so their use is limited.
      • In particular, ts.getSymbolForLocation(node) fails to return a symbol for node created using typeToTypeNode(). In order to find symbols for TypeReferences, we need to use checker.getSymbolsInScope(node).filter(s => s.name === name)
  • Literal Types
    • The type inferred from a literal assignment/value will be a literal type (e.g. const foo = 'hi' will have a type of "hi")
    • To turn a literal type into its wider base type (e.g. string), use checker.getBaseTypeOfLiteralType(type)

Typescript-ESLint Plugin

  • Resources:
  • The conventional naming scheme used by ESLint is maddening. In general, it expects plugin npm module names to be prefixed with eslint-plugin-, and then a shorthand used in e.g. rule names, plugin names, etc. is derived from the suffix, and gets wonky with npm orgs:
    • npm module eslint-plugin-my-thing → eslint plugin name my-thing
    • npm module @my-org/eslint-plugin → eslint plugin name @my-org
    • npm module @my-org/eslint-plugin-my-thing → unclear; @my-org/my-thing doesn't seem to work
  • To see loading errors or debug console.logs from the plugin in VSCode, go the "Output" tab in the terminals area and select "ESLint" in the dropdown
  • In order to get a ts program/checker in a custom rule, the user's .eslintrc has to point to a tsconfig.json via a parserOptions.project setting (in addition to the extends: [ 'plugin:@lit-labs/all' ]` to pull in a config
  • Each time the rule runs, you get handed a new ts.Program. ts.SourceFiles within the program that weren't changed are referentially equal to the last time the rule ran, which bodes well for caching keyed off the ts Node.
  • Since control is inverted from the current analyzer factoring (i.e., we don't control the serviceHost that creates the program), I don't see a good way to do the /** Event {Type} */ gambit, since we can't update files (to add the dummy declarations) and re-check them. So might need to re-think that, unfortunately.

Typescript Language Server Plugin

  • Resources:
  • You can put an npm package for a LSP in tsconfig.json compilerOptions.plugins.name, however vscode will try to resolve it from the bundled vscode TS installation location (and fail) unless you switch vscode to using the "workspace TS version" via cmd-P: "Typescript: Select Typescript Version"; in a monorepo where there might be lots of TS's, the plugin needs to be installed adjacent to the selected TS.
  • To see loading errors or debug messages (by using info.project.projectService.logger.info, not console.log!), use cmd-P: "TypeScript: Open TS Server Log" (will ask to enable it once, apparently it's a perf thing since it logs so much).
  • To debug a plugin in VSCode:
    • Start a separate VSCode using TSS_DEBUG env variable, e.g.:
      TSS_DEBUG=5667 code --user-data-dir ~/.vscode-debug/ ./lit-next.code-workspace
    • Make sure to cmd-P "Typescript: Select Typescript Version" in the new VSCode window
    • From VSCode used to develop plugin, cmd-P, "Debug: Attach to Node Process", and select instance with port 5667
    • From there, debuggers in plugin source should work

Ideal:

  • Analyzer
    • Generate CEM-like models from source
    • Library for template type-checking
    • Collect diagnostics, mappable by TS node
  • Lit language service - Provides jump to definition, etc.
  • Lit ESLit plugin - James's rules + Lit template type-checking

First steps

  • Add eslint-plugin-lit folder to labs
  • Add rule to detect @property type disagreement with TypeScript type