Skip to content

feat: introduce Vue language#2316

Merged
auvred merged 17 commits intomainfrom
vue-language
Mar 18, 2026
Merged

feat: introduce Vue language#2316
auvred merged 17 commits intomainfrom
vue-language

Conversation

@auvred
Copy link
Copy Markdown
Member

@auvred auvred commented Feb 14, 2026

PR Checklist

Overview

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Feb 14, 2026

🦋 Changeset detected

Latest commit: 3be92f3

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
@flint.fyi/vue-language Minor
@flint.fyi/vue Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flint Ready Ready Preview, Comment Mar 18, 2026 11:03am

Request Review

Base automatically changed from volar-language to main March 10, 2026 18:34
]
},
"packages/vue": {
"ignoreDependencies": ["vue"]
Copy link
Copy Markdown
Member Author

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 .vue files contains vue package imports)

@auvred auvred marked this pull request as ready for review March 10, 2026 19:32
Copy link
Copy Markdown
Collaborator

@JoshuaKGoldberg JoshuaKGoldberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔥 great!!

I left a few small things but they are either too small to block or too big to block. This is really lovely to see.

Comment on lines +22 to +23
export interface VueServices {
vueServices: {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Naming] 🤔 this nesting of two things both called "Vue services" is odd. I think it'll be confusing to people trying to read & understand them. Especially folks coming into linting/Flint anew from the Vue side.

I also can't quickly think of a better property name than vueServices for the nested object containing Vue services. Maybe that's a sign that these all make sense as direct properties of VueServices, not nested underneath an object? That would simplify the overall structure of things.

Though, given the comment about Partial<FileServices>, maybe that's not doable? If so - how about calling this vue instead of vueServices? That would signal that it's the "Vue" area of "services".

WDYT?

Copy link
Copy Markdown
Member Author

@auvred auvred Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Naming] 🤔 this nesting of two things both called "Vue services" is odd. I think it'll be confusing to people trying to read & understand them. Especially folks coming into linting/Flint anew from the Vue side.

+1 on renaming. Just plain vue property would make more sense

Maybe that's a sign that these all make sense as direct properties of VueServices, not nested underneath an object? That would simplify the overall structure of things.

Though, given the comment about Partial<FileServices>, maybe that's not doable?

Yeah, answered in #2316 (comment)

If we make these properties top-level, then rule authors will have to do null checks for every individual property rather than just context.vue != null

}

type VueCodegen =
typeof tsCodegen extends WeakMap<WeakKey, infer V> ? V : never;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The 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.

typeof configFilePath === "string"
? createVueParsedCommandLine(
ts,
ts.sys,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Question] Shouldn't this also be using options.host in some way? (and the one too)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe (and @auvred pls correct me if I'm wrong) that the ts.sys that we have at this point is running in the LinterHost...

EDIT: though if that's the case not sure what's going on below with the options.host ?? ts.sys. Which also probably doesn't work anymore post-#2200 anyway??

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh, nice catch!!

Even though options.host is wrapped like 5 times in TS internals, its FS-related methods still end up in our LinterHost, I updated this place to prefer options.host when it's non-null (always)

Comment on lines +56 to +58
if (vueServices == null) {
return;
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Refactor] I now see the disadvantage of the Partial<FileServices> in createVolarBasedLanguage... is this really necessary? Can we just say:

  export function createVolarBasedLanguage<FileServices extends object>(
  	initializer: VolarLanguagePluginInitializer<FileServices>,
  ): Language<
  	TypeScriptNodesByName,
-  	Partial<FileServices> & TypeScriptFileServices
+  	FileServices & TypeScriptFileServices
  > {

?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, the problem is that we want Vue rules to lint both .vue and .ts files!

If we remove Partial from there, we will basically limit rules created from vueLanguage to lint only .vue files since Vue services are available only there.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So I'm hearing this is a #1253 (comment) problem?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Almost yes, but no. Currently, Vue is like a superset of TS. Both TS and Vue rules can lint the generated virtual TypeScript code, but only Vue rules can access Vue services (Vue AST) when they're linting .vue files (they can't access them when they're linting .ts files). 1253 is more about cross-language/cross-file linting

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, after a few read-thrus of your comment, do Vue rules also run on .ts files? If not I don't know how to understand that comment. (so #1253 (comment) but the future is sorta now)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, after a few read-thrus of your comment, do Vue rules also run on .ts files?

Yes, exactly! .ts files can also contain Vue-specific code, for example asyncComputed rule - https://github.com/auvred/flint/blob/5fee75d714f36bce5363a46c9c9d53cf8251da17/packages/vue/src/rules/asyncComputed.test.ts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooh, cool! Yeah I didn't get that. I'll go re-read and see where I missed that.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, it's still a little fuzzy around the edges but I think I get the general idea, maybe?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll write a bit more detailed explanation tomorrow 🤝

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, how this works:

First, we have TS rules (from @flint.fyi/ts). These rules can lint pure TypeScript code only. Thanks to Volar, .vue files, when imported into TS program, are converted to virtual generated TypeScript code.

So:

  • when TS rule lints .ts everything works as before
  • when TS rule lints .vue, it lints the virtual generated code, reports errors on it, and our linter engine maps these errors back to their original locations in Vue SFC

What happens when we create a new rule using language from createVolarBasedLanguage:

  • when Vue rule lints .ts, it lints it with the same context/services as TS rule, everything works exactly the same here
  • when Vue rule lints .vue file, similar to the TS rule, it can report errors on the virtual generated code (reports then mapped to the original locations), or it can access vue services from the context. In this case, Vue rule can visit the original AST of the Vue SFC, optionally map its location to the generated code (to get some type information), and report the errors with the original Vue SFC locations, in this case these locations are not mapped back since they're already valid.

Hope this helps!

Comment on lines +60 to +74
const toGeneratedLocation = (sourceLocation: number) => {
for (const [loc] of vueServices.map.toGeneratedLocation(
sourceLocation,
)) {
return loc;
}
return undefined;
};

const toGeneratedLocationOrThrow = (sourceLocation: number) => {
return nullThrows(
toGeneratedLocation(sourceLocation),
"Unable to map source location to generated location",
);
};
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Refactor] This strikes me as utilities that most-to-all Vue rules will need. I'm guessing they'll end up in a utils directory sooner or later.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For sure! But I think we need to write several more rules, so that we can better understand the appropriate shape for these utilities

@@ -0,0 +1,52 @@
{
"name": "@flint.fyi/vue",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, and: should this be split into the language & rules plugin the way others are too?

Suggested change
"name": "@flint.fyi/vue",
"name": "@flint.fyi/vue-language",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is! I missed that as well my first readthru 😂

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 But this PR already has both @flint.fyi/vue-language and @flint.fyi/vue? Or am I missing something?

Copy link
Copy Markdown
Member

@lishaduck lishaduck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 🙃

typeof configFilePath === "string"
? createVueParsedCommandLine(
ts,
ts.sys,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe (and @auvred pls correct me if I'm wrong) that the ts.sys that we have at this point is running in the LinterHost...

EDIT: though if that's the case not sure what's going on below with the options.host ?? ts.sys. Which also probably doesn't work anymore post-#2200 anyway??

}
};

// TODO: add vue: listeners to the language
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know what this means but cool. (aka what's vue:. Can I get like a 2-second vue crash-course? I think that might help)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oops, sorry bad phrasing... I meant this - #1163 (comment)

The unclear part is: is it OK to call all TS listeners before and then all vue: listeners after? Probably yes, because we don't have any other options. I'll file the followup issue

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ooooh, that makes sense. 👍🏻

@JoshuaKGoldberg JoshuaKGoldberg added the status: waiting for author Needs an action taken by the original poster label Mar 11, 2026
@github-actions github-actions bot removed the status: waiting for author Needs an action taken by the original poster label Mar 11, 2026
@auvred
Copy link
Copy Markdown
Member Author

auvred commented Mar 11, 2026

Okay I think I addressed almost everything!

I also want to say thank you both for diving into this and catching so many things here! I know how much brain hurts after trying to understand how all this Volar/Vue stuff works (I spent like several months digging in this, so that's no longer the case for me, but I remember that a few years ago I tried to implement all this as an ESLint plugin and I gave up after two days in a complete desperation because my brain melted 🫠)

@kovsu
Copy link
Copy Markdown
Member

kovsu commented Mar 11, 2026

Off topic: sensei, please teach me. 🫨

@auvred
Copy link
Copy Markdown
Member Author

auvred commented Mar 11, 2026

Even though I'm bad at writing, I'll try to explain everything in details in #2297

Copy link
Copy Markdown
Member

@lishaduck lishaduck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀

"test": "vitest --project vue-language"
},
"dependencies": {
"@flint.fyi/core": "workspace:^",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can just be a devDependency, it's type-only and not exposed (oh how I want a good public/private types detector...).

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow! What a catch... I can't even imagine how you noticed this

@auvred
Copy link
Copy Markdown
Member Author

auvred commented Mar 18, 2026

Okay, the time has come...

@auvred auvred merged commit d5c59ec into main Mar 18, 2026
8 checks passed
@auvred auvred deleted the vue-language branch March 18, 2026 11:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🚀 Feature: introduce Vue language

5 participants