-
Notifications
You must be signed in to change notification settings - Fork 22
feat: introduce Vue language #2316
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5c1541b
e2789c5
252527f
168c445
ed34905
fbffc8c
ebac147
9f0fe6e
83f95b4
5f992cf
a50693f
2284b13
cb599d6
b091327
9301732
fe7269f
3be92f3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "@flint.fyi/vue-language": minor | ||
| "@flint.fyi/vue": minor | ||
| --- | ||
|
|
||
| Introduce Vue language. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,6 +43,7 @@ | |
| "bday", | ||
| "BDFL", | ||
| "bradzacher", | ||
| "codegen", | ||
| "codemod", | ||
| "contentinfo", | ||
| "deoptimizations", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,9 @@ | |
| "sharp", | ||
| "zod" | ||
| ] | ||
| }, | ||
| "packages/vue": { | ||
| "ignoreDependencies": ["vue"] | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <h1 align="center"><code>@flint.fyi/vue-language</code></h1> | ||
|
|
||
| <p align="center"> | ||
| [Experimental] Vue.js language for Flint. | ||
| ❤️🔥 | ||
| </p> | ||
|
|
||
| This is an internal package for Flint. | ||
| Documentation will eventually be added. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,52 @@ | ||
| { | ||
| "name": "@flint.fyi/vue-language", | ||
| "version": "0.0.1", | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'd be cool if there was some tooling to warn us against starting numbering at random #s like we accidentally did for a few packages. |
||
| "description": "[Experimental] Vue.js language for Flint.", | ||
| "homepage": "https://flint.fyi", | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/flint-fyi/flint.git", | ||
| "directory": "packages/vue-language" | ||
| }, | ||
| "license": "MIT", | ||
| "author": { | ||
| "name": "JoshuaKGoldberg", | ||
| "email": "npm@joshuakgoldberg.com" | ||
| }, | ||
| "sideEffects": true, | ||
| "type": "module", | ||
| "exports": { | ||
| ".": "./src/index.ts" | ||
| }, | ||
| "files": [ | ||
| "lib/", | ||
| "!lib/**/*.map" | ||
| ], | ||
| "scripts": { | ||
| "test": "vitest --project vue-language" | ||
| }, | ||
| "dependencies": { | ||
| "@flint.fyi/ts-patch": "workspace:^", | ||
| "@flint.fyi/typescript-language": "workspace:^", | ||
| "@flint.fyi/utils": "workspace:^", | ||
| "@flint.fyi/volar-language": "workspace:^", | ||
| "@volar/language-core": "2.4.28", | ||
| "@vue/compiler-dom": "^3.5.0", | ||
| "@vue/language-core": "3.2.1", | ||
| "typescript": "^5.9.0 || ^6.0.0" | ||
| }, | ||
| "devDependencies": { | ||
| "@flint.fyi/core": "workspace:^", | ||
| "tsdown": "catalog:dev", | ||
| "vitest": "catalog:dev" | ||
| }, | ||
| "engines": { | ||
| "node": ">=24.0.0" | ||
| }, | ||
| "publishConfig": { | ||
| "access": "public", | ||
| "exports": { | ||
| ".": "./lib/index.js" | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| import type { ExtractedDirective } from "@flint.fyi/typescript-language"; | ||
| import { nullThrows } from "@flint.fyi/utils"; | ||
| import { | ||
| NodeTypes, | ||
| type RootNode, | ||
| type TemplateChildNode, | ||
| } from "@vue/compiler-dom"; | ||
|
|
||
| export function extractTemplateDirectives(ast: RootNode) { | ||
| const directives: ExtractedDirective[] = []; | ||
|
|
||
| function visitTemplate(elem: TemplateChildNode) { | ||
| if (elem.type === NodeTypes.ELEMENT) { | ||
| for (const child of elem.children) { | ||
| visitTemplate(child); | ||
| } | ||
| return; | ||
| } | ||
| if (elem.type !== NodeTypes.COMMENT) { | ||
| return; | ||
| } | ||
| const match = /\s*flint-(\S+)(?:\s+(.+))?/.exec(elem.content); | ||
| if (match == null) { | ||
| return; | ||
| } | ||
| const [, type, selection] = match; | ||
| directives.push({ | ||
| range: { | ||
| begin: { | ||
| column: elem.loc.start.column - 1, | ||
| line: elem.loc.start.line - 1, | ||
| raw: elem.loc.start.offset, | ||
| }, | ||
| end: { | ||
| column: elem.loc.end.column - 1, | ||
| line: elem.loc.end.line - 1, | ||
| raw: elem.loc.end.offset, | ||
| }, | ||
| }, | ||
| selection: nullThrows( | ||
| selection, | ||
| "Expected RegExp to provide second capturing group", | ||
| ), | ||
| type: nullThrows( | ||
| type, | ||
| "Expected RegExp to provide first capturing group", | ||
| ), | ||
| }); | ||
| } | ||
|
|
||
| for (const child of ast.children) { | ||
| visitTemplate(child); | ||
| } | ||
|
|
||
| return directives; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| export { vueLanguage } from "./language.ts"; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import { setTSExtraSupportedExtensions } from "@flint.fyi/ts-patch"; | ||
| import { assert, nullThrows } from "@flint.fyi/utils"; | ||
| import { createVolarBasedLanguage } from "@flint.fyi/volar-language"; | ||
| import type { Mapper as VolarMapper } from "@volar/language-core"; | ||
| import { NodeTypes, type RootNode, parse as vueParse } from "@vue/compiler-dom"; | ||
| import { | ||
| createVueLanguagePlugin, | ||
| createParsedCommandLine as createVueParsedCommandLine, | ||
| createParsedCommandLineByJson as createVueParsedCommandLineByJson, | ||
| tsCodegen, | ||
| VueVirtualCode, | ||
| } from "@vue/language-core"; | ||
|
|
||
| import { extractTemplateDirectives } from "./extractTemplateDirectives.ts"; | ||
| import { vueParsingErrorsToLanguageDiagnostics } from "./vueParsingErrorsToLanguageDiagnostics.ts"; | ||
|
|
||
| setTSExtraSupportedExtensions([".vue"]); | ||
|
|
||
| export interface VueServices { | ||
| vue: { | ||
| codegen: VueCodegen; | ||
| map: VolarMapper; | ||
| sfc: RootNode; | ||
| virtualCode: VueVirtualCode; | ||
| }; | ||
| } | ||
|
|
||
| type VueCodegen = | ||
| typeof tsCodegen extends WeakMap<WeakKey, infer V> ? V : never; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Aside] I cannot wait for the blog post explaining this architecture 😂 it's going to cause a lot of conversations around tooling folks. |
||
|
|
||
| export const vueLanguage = createVolarBasedLanguage<VueServices>( | ||
| (ts, options) => { | ||
| const { configFilePath } = options.options; | ||
| const host = options.host | ||
| ? { | ||
| ...options.host, | ||
| useCaseSensitiveFileNames: options.host.useCaseSensitiveFileNames(), | ||
| } | ||
| : ts.sys; | ||
| const vueCompilerOptions = ( | ||
| typeof configFilePath === "string" | ||
| ? createVueParsedCommandLine( | ||
| ts, | ||
| host, | ||
| configFilePath.replaceAll("\\", "/"), | ||
| ) | ||
| : createVueParsedCommandLineByJson( | ||
| ts, | ||
| host, | ||
| host.getCurrentDirectory(), | ||
| {}, | ||
| ) | ||
| ).vueOptions; | ||
|
|
||
| return { | ||
| createFile({ | ||
| data, | ||
| serviceScript, | ||
| sourceFile, | ||
| sourceScript, | ||
| volarLanguage, | ||
| }) { | ||
| const sourceText = sourceScript.snapshot.getText( | ||
| 0, | ||
| sourceScript.snapshot.getLength(), | ||
| ); | ||
| const virtualCode = sourceScript.generated.root; | ||
| assert( | ||
| virtualCode instanceof VueVirtualCode, | ||
| "Expected sourceScript.generated.root to be VueServiceCode", | ||
| ); | ||
|
|
||
| const codegen = nullThrows( | ||
| tsCodegen.get(virtualCode.sfc), | ||
| `tsCodegen for ${data.filePathAbsolute} is undefined`, | ||
| ); | ||
auvred marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| const map = volarLanguage.maps.get(serviceScript.code, sourceScript); | ||
|
|
||
| const sfcAst = vueParse(sourceText, { | ||
| comments: true, | ||
| expressionPlugins: ["typescript"], | ||
| onError: () => { | ||
| // We ignore errors because virtual code already provides them, | ||
| // and it also provides them with sourceText-based locations, | ||
| // so we don't have to remap them. Oh, and it also contains errors from | ||
| // other blocks rather than only <template> as well. | ||
| // If we don't provide this callback, @vue/compiler-core will throw. | ||
| }, | ||
| parseMode: "html", | ||
| }); | ||
|
|
||
| return { | ||
| // TODO: extract directives from other blocks too | ||
| directives: extractTemplateDirectives(sfcAst), | ||
| extraContext: { | ||
| vue: { | ||
| codegen, | ||
| map, | ||
| sfc: sfcAst, | ||
| virtualCode, | ||
| }, | ||
| }, | ||
| firstStatementPosition: | ||
| sfcAst.children.find((c) => c.type !== NodeTypes.COMMENT)?.loc.start | ||
| .offset ?? sourceText.length, | ||
| getDiagnostics() { | ||
| return vueParsingErrorsToLanguageDiagnostics( | ||
| sourceFile.fileName.startsWith("./") | ||
| ? sourceFile.fileName.slice(2) | ||
| : // TODO: use LinterHost.getCurrentDirectory() | ||
| sourceFile.fileName.slice(process.cwd().length + 1), | ||
| virtualCode.vueSfc?.errors ?? [], | ||
| ); | ||
| }, | ||
| }; | ||
| }, | ||
| languagePlugins: [ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is just so awesome 🚀 |
||
| createVueLanguagePlugin<string>( | ||
| ts, | ||
| options.options, | ||
| vueCompilerOptions, | ||
| (id) => id, | ||
| ), | ||
| ], | ||
| }; | ||
| }, | ||
| ); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| import type { LanguageDiagnostics } from "@flint.fyi/core"; | ||
| import type { CompilerError } from "@vue/compiler-dom"; | ||
|
|
||
| export function vueParsingErrorsToLanguageDiagnostics( | ||
| fileName: string, | ||
| errors: (CompilerError | SyntaxError)[], | ||
| ): LanguageDiagnostics { | ||
| return errors.map((error) => { | ||
| let code = "VUE"; | ||
| let loc = ""; | ||
| if ("code" in error) { | ||
| code += error.code.toString(); | ||
| loc = | ||
| error.loc != null | ||
| ? `:${error.loc.start.line}:${error.loc.start.column}` | ||
| : ""; | ||
| } | ||
| return { | ||
| code, | ||
| text: `${fileName}${loc} - ${code}: ${error.name} - ${error.message}`, | ||
| }; | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| "extends": "../../tsconfig.base.json", | ||
| "include": [], | ||
| "references": [ | ||
| { "path": "./tsconfig.src.json" }, | ||
| { "path": "./tsconfig.test.json" } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "tsBuildInfoFile": "node_modules/.cache/tsbuild/info.src.json", | ||
| "rootDir": "src/", | ||
| "outDir": "lib/", | ||
| "types": ["node"] | ||
| }, | ||
| "extends": "../../tsconfig.base.json", | ||
| "include": ["src"], | ||
| "exclude": ["src/**/*.test.ts"], | ||
| "references": [ | ||
| { "path": "../core" }, | ||
| { "path": "../ts-patch" }, | ||
| { "path": "../typescript-language" }, | ||
| { "path": "../utils" }, | ||
| { "path": "../volar-language" } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "compilerOptions": { | ||
| "tsBuildInfoFile": "node_modules/.cache/tsbuild/info.test.json", | ||
| "rootDir": "src/", | ||
| "outDir": "node_modules/.cache/tsbuild/test", | ||
| "types": ["node"], | ||
| "erasableSyntaxOnly": false | ||
| }, | ||
| "extends": "../../tsconfig.base.json", | ||
| "include": ["src/**/*.test.ts"], | ||
| "references": [{ "path": "./tsconfig.src.json" }] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,19 @@ | ||
| import { defineConfig } from "tsdown"; | ||
|
|
||
| export default defineConfig({ | ||
| attw: { | ||
| enabled: "ci-only", | ||
| profile: "esm-only", | ||
| }, | ||
| clean: ["./node_modules/.cache/tsbuild/"], | ||
| dts: { build: true, incremental: true }, | ||
| entry: ["src/index.ts"], | ||
| exports: { | ||
| devExports: true, | ||
| packageJson: false, | ||
| }, | ||
| failOnWarn: true, | ||
| fixedExtension: false, | ||
| outDir: "lib", | ||
| unbundle: true, | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <h1 align="center"><code>@flint.fyi/vue</code></h1> | ||
|
|
||
| <p align="center"> | ||
| [Experimental] Vue.js language plugin for Flint. | ||
| ❤️🔥 | ||
| </p> | ||
|
|
||
| This is an internal package for Flint. | ||
| Documentation will eventually be added. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's required for tests (virtual code for
.vuefiles containsvuepackage imports)