Skip to content

Design Meeting Notes, 2026-07-23 #63676

Description

@DanielRosenwasser

JavaScript Emit API

microsoft/typescript-go#4699

  • How would something like "compile on save" be reimplemented?
    • Idea: LSP has didSave event, clients can listen and trigger a custom emit command
    • Does emit in LSP make sense?
    • We do expect it for the playground.
    • Sounds like yes.
  • Source maps?
    • Should add those too.
  • What's the (internal) API look like?
    • program.emit(emitOnly?: EmitOnly) - file system (including VFS if supplied), whole program, respects emit-blocking options like noEmit, noEmitOnError, ...

      • Some weirdness in interaction, e.g. emitDeclarationOnly
    • program.emitToString(emitOnly?: EmitOnly) - in-memory string results, whole program, respects emit-blocking options

      • Basically a string over IPC
    • program.getJavaScriptEmit(files?: readonly DocumentIdentifier[]) - in-memory string results, specific file selection, bypasses emit-blocking options

      • In-memory, does not respect noEmit, emitDeclarationOnly, etc.
    • program.getDeclarationEmit(files?: readonly DocumentIdentifier[]) - (same)

    • And EmitOnly is:

      export enum EmitOnly {
          All = 0,
          OnlyJs = 1,
          OnlyDts = 2,
      }
  • May have to consider how pre/post transformations work here, plus dynamically contributed content - recall that Angular might have been looking for this with ngc?

Content Mappers

// tsconfig.json
{
    "compilerOptions": {
        // ...
    },
    "contentMappers": [
        {
            "package": "vue-content-mapper",
            "extensions": [".vue"],
        }
    ],
    "include": ["src"]
}
// node_modules/vue-content-mapper/package.json
{
    "name": "vue-content-mapper",
    "version": "1.0.0",
    "tsContentMapper": {
        "exec": ["node", "dist/server.js"],
        "compilerOptions": ["module", "jsx", "jsxImportSource"]
    }
}
  • What kind of versioning contract can we provide here?
    • Stuff like LSP/Language Server Protocol doesn't provide a version per se - it's all capabilities based. Looks like same with AHP/Agent Host Protocol.
      • Can keep version, but stick close to that model.
  • Why does this have to be a protocol instead of something like a potentially long-running process? Feels heavy.
    • Don't want to have multiple invocations of the mapper.
    • Cascading resolution: we don't always know which files the program itself includes (you can end up with .vue files outside of the program)
    • Also, we have long-running things like --watch and LSP where the program state will change over time.
    • These transforms are all supposed to be idempotent.
  • One issue is: are these able to react to their configurations, and invalidate their resolution?
    • We can react to the package itself changing, but not currently to configuration.
    • Maybe we need something about config files that the plugin can watch.
  • Do we allow this only for --moduleResolution bundler or for --moduleResolution nodenext and others?
    • It works across everything.
  • Top-level contentMappers field - how does this work with tsconfig inheritance?
    • It currently doesn't.
  • This does have some invasiveness in our LSP implementation.
    • Two different spans in a .ts file can map to the same span in a .vue file.
      • Also, want these operations to coalesce occasionally.
    • So operations at Line X Column Y now potentially have multiple results!
      • So some operations default to the first result (hover), some concatenate results (go-to-definition).
  • This doesn't completely subsume Volar - Volar can mask/fix up errors.
    • But you can build an LSP over this to do this if desired.
  • What are examples of that funky LSP behavior?
    • Take for example:
      <script setup lang="ts">
      import { ref, computed } from "vue";
      
      const count = ref(0); const label = ref("clicks");
      const label = computed(() => count.value === 1 ? "click" : "clicks");
      const doubled = computed(() => count.value *2);
      
      function increment(): void {
        count.value++;
      }
      </script>
      
      <template>
        <div class="counter">
          <p>{{ count }} {{ label }}</p>
          <p>doubled = {{ doubled }}</p>
          <button @click="increment()">+1</button>
          <ul>
            <li v-for="(n, i) in [count, doubled]" :key="i">{{ n }}</li>
          </ul>
          <span v-if="count > 5">high</span>
        </div>
      </template>
    • Kinda remaps to...
      // SCRIPT SETUP START:
      
      export default createComponent({
        // ...
        setup(props) {
      
      // VERBATIM-ISH SCRIPT BLOCK:
        const count = ref(0);
        // ...
      
      // GENERATED AFTER SCRIPT:
        return {
          count,
        };
      });
      
      // GENERATED FROM TEMPLATE BLOCK:
      
      function render(props)
        const { count } = props;
      
        // usage of count
        count;
      }
    • A reference to count in the <template> block has a definition that doesn't exist in the original .vue source, but also has a mapping to have a mapping back to a node that is not the "true" definition site.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Design NotesNotes from our design meetings

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions