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?
- 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?
- 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.
JavaScript Emit API
microsoft/typescript-go#4699
didSaveevent, clients can listen and trigger a custom emit commandprogram.emit(emitOnly?: EmitOnly)- file system (including VFS if supplied), whole program, respects emit-blocking options likenoEmit,noEmitOnError, ...emitDeclarationOnlyprogram.emitToString(emitOnly?: EmitOnly)- in-memory string results, whole program, respects emit-blocking optionsprogram.getJavaScriptEmit(files?: readonly DocumentIdentifier[])- in-memory string results, specific file selection, bypasses emit-blocking optionsnoEmit,emitDeclarationOnly, etc.program.getDeclarationEmit(files?: readonly DocumentIdentifier[])- (same)And EmitOnly is:
Content Mappers
.vuefiles outside of the program)--watchand LSP where the program state will change over time.--moduleResolution bundleror for--moduleResolution nodenextand others?contentMappersfield - how does this work with tsconfig inheritance?.tsfile can map to the same span in a.vuefile.countin the<template>block has a definition that doesn't exist in the original.vuesource, but also has a mapping to have a mapping back to a node that is not the "true" definition site.