Skip to content

feat(language-core): introduce compileSFCStyle to provide style related infomation #5548

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

Merged
merged 7 commits into from
Aug 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/language-core/lib/codegen/script/template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,14 @@ function* generateCssVars(options: ScriptCodegenOptions, ctx: TemplateCodegenCon
}
yield `// CSS variable injection ${newLine}`;
for (const style of options.sfc.styles) {
for (const cssBind of style.cssVars) {
for (const binding of style.bindings) {
yield* generateInterpolation(
options,
ctx,
style.name,
codeFeatures.all,
cssBind.text,
cssBind.offset,
binding.text,
binding.offset,
);
yield endOfLine;
}
Expand Down
2 changes: 2 additions & 0 deletions packages/language-core/lib/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import vueSfcCustomBlocks from './plugins/vue-sfc-customblocks';
import vueSfcScriptsFormat from './plugins/vue-sfc-scripts';
import vueSfcStyles from './plugins/vue-sfc-styles';
import vueSfcTemplate from './plugins/vue-sfc-template';
import vueStyleCss from './plugins/vue-style-css';
import vueTemplateHtmlPlugin from './plugins/vue-template-html';
import vueTemplateInlineCssPlugin from './plugins/vue-template-inline-css';
import vueTemplateInlineTsPlugin from './plugins/vue-template-inline-ts';
Expand All @@ -22,6 +23,7 @@ export function createPlugins(pluginContext: Parameters<VueLanguagePlugin>[0]) {
useHtmlFilePlugin,
vueRootTagsPlugin,
vueScriptJsPlugin,
vueStyleCss,
vueTemplateHtmlPlugin,
vueTemplateInlineCssPlugin,
vueTemplateInlineTsPlugin,
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/plugins/file-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const langReg = /\blang\s*=\s*(['"]?)(\S*)\b\1/;

const plugin: VueLanguagePlugin = ({ vueCompilerOptions }) => {
return {
version: 2.1,
version: 2.2,

getLanguageId(fileName) {
if (vueCompilerOptions.petiteVueExtensions.some(ext => fileName.endsWith(ext))) {
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/plugins/file-md.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const codeSnippetImportReg = /^\s*<<<\s*.+/gm;

const plugin: VueLanguagePlugin = ({ vueCompilerOptions }) => {
return {
version: 2.1,
version: 2.2,

getLanguageId(fileName) {
if (vueCompilerOptions.vitePressExtensions.some(ext => fileName.endsWith(ext))) {
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/plugins/file-vue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { parse } from '../utils/parseSfc';

const plugin: VueLanguagePlugin = ({ vueCompilerOptions }) => {
return {
version: 2.1,
version: 2.2,

getLanguageId(fileName) {
if (vueCompilerOptions.extensions.some(ext => fileName.endsWith(ext))) {
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/plugins/vue-root-tags.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { allCodeFeatures } from './shared';

const plugin: VueLanguagePlugin = () => {
return {
version: 2.1,
version: 2.2,

getEmbeddedCodes() {
return [{
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/plugins/vue-script-js.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { VueLanguagePlugin } from '../types';

const plugin: VueLanguagePlugin = ({ modules }) => {
return {
version: 2.1,
version: 2.2,

compileSFCScript(lang, script) {
if (lang === 'js' || lang === 'ts' || lang === 'jsx' || lang === 'tsx') {
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/plugins/vue-sfc-customblocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { allCodeFeatures } from './shared';

const plugin: VueLanguagePlugin = () => {
return {
version: 2.1,
version: 2.2,

getEmbeddedCodes(_fileName, sfc) {
return sfc.customBlocks.map((customBlock, i) => ({
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/plugins/vue-sfc-scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { VueLanguagePlugin } from '../types';

const plugin: VueLanguagePlugin = () => {
return {
version: 2.1,
version: 2.2,

getEmbeddedCodes(_fileName, sfc) {
const names: {
Expand Down
10 changes: 5 additions & 5 deletions packages/language-core/lib/plugins/vue-sfc-styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { allCodeFeatures } from './shared';

const plugin: VueLanguagePlugin = () => {
return {
version: 2.1,
version: 2.2,

getEmbeddedCodes(_fileName, sfc) {
const result: {
Expand All @@ -17,7 +17,7 @@ const plugin: VueLanguagePlugin = () => {
id: 'style_' + i,
lang: style.lang,
});
if (style.cssVars.length) {
if (style.bindings.length) {
result.push({
id: 'style_' + i + '_inline_ts',
lang: 'ts',
Expand All @@ -34,13 +34,13 @@ const plugin: VueLanguagePlugin = () => {
const style = sfc.styles[index];
if (embeddedFile.id.endsWith('_inline_ts')) {
embeddedFile.parentCodeId = 'style_' + index;
for (const cssVar of style.cssVars) {
for (const binding of style.bindings) {
embeddedFile.content.push(
'(',
[
cssVar.text,
binding.text,
style.name,
cssVar.offset,
binding.offset,
allCodeFeatures,
],
');\n',
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/plugins/vue-sfc-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { allCodeFeatures } from './shared';

const plugin: VueLanguagePlugin = () => {
return {
version: 2.1,
version: 2.2,

getEmbeddedCodes(_fileName, sfc) {
if (sfc.template?.lang === 'html') {
Expand Down
68 changes: 68 additions & 0 deletions packages/language-core/lib/plugins/vue-style-css.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { VueLanguagePlugin } from '../types';

const plugin: VueLanguagePlugin = () => {
return {
version: 2.2,

compileSFCStyle(_lang, style) {
return {
imports: [...parseCssImports(style)],
bindings: [...parseCssBindings(style)],
classNames: [...parseCssClassNames(style)],
};
},
};
};

export default plugin;

const cssImportReg = /(?<=@import\s+url\()(["']?).*?\1(?=\))|(?<=@import\b\s*)(["']).*?\2/g;
const cssBindingReg = /\bv-bind\(\s*(?:'([^']+)'|"([^"]+)"|([a-z_]\w*))\s*\)/gi;
const cssClassNameReg = /(?=(\.[a-z_][-\w]*)[\s.,+~>:#)[{])/gi;
const commentReg = /(?<=\/\*)[\s\S]*?(?=\*\/)|(?<=\/\/)[\s\S]*?(?=\n)/g;
const fragmentReg = /(?<={)[^{]*(?=(?<!\\);)/g;

function* parseCssImports(css: string) {
const matches = css.matchAll(cssImportReg);
for (const match of matches) {
let text = match[0];
let offset = match.index;
if (text.startsWith("'") || text.startsWith('"')) {
text = text.slice(1, -1);
offset += 1;
}
if (text) {
yield { text, offset };
}
}
}

function* parseCssBindings(css: string) {
css = fillBlank(css, commentReg);
const matchs = css.matchAll(cssBindingReg);
for (const match of matchs) {
const matchText = match.slice(1).find(t => t);
if (matchText) {
const offset = match.index + css.slice(match.index).indexOf(matchText);
yield { offset, text: matchText };
}
}
}

function* parseCssClassNames(css: string) {
css = fillBlank(css, commentReg, fragmentReg);
const matches = css.matchAll(cssClassNameReg);
for (const match of matches) {
const matchText = match[1];
if (matchText) {
yield { offset: match.index, text: matchText };
}
}
}

function fillBlank(css: string, ...regs: RegExp[]) {
for (const reg of regs) {
css = css.replace(reg, match => ' '.repeat(match.length));
}
return css;
}
2 changes: 1 addition & 1 deletion packages/language-core/lib/plugins/vue-template-html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const shouldAddSuffix = /(?<=<[^>/]+)$/;

const plugin: VueLanguagePlugin = ({ modules }) => {
return {
version: 2.1,
version: 2.2,

compileSFCTemplate(lang, template, options) {
if (lang === 'html' || lang === 'md') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const codeFeatures = {

const plugin: VueLanguagePlugin = () => {
return {
version: 2.1,
version: 2.2,

getEmbeddedCodes(_fileName, sfc) {
if (!sfc.template?.ast) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const plugin: VueLanguagePlugin = ctx => {
const parseds = new WeakMap<Sfc, ReturnType<typeof parse>>();

return {
version: 2.1,
version: 2.2,

getEmbeddedCodes(_fileName, sfc) {
if (!sfc.template?.ast) {
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/plugins/vue-tsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const validLangs = new Set(['js', 'jsx', 'ts', 'tsx']);

const plugin: VueLanguagePlugin = ctx => {
return {
version: 2.1,
version: 2.2,

requiredCompilerOptions: [
'noPropertyAccessFromIndexSignature',
Expand Down
7 changes: 5 additions & 2 deletions packages/language-core/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export interface VueCompilerOptions {
>;
}

export const validVersions = [2, 2.1] as const;
export const validVersions = [2, 2.1, 2.2] as const;

export type VueLanguagePluginReturn = {
version: typeof validVersions[number];
Expand All @@ -97,6 +97,9 @@ export type VueLanguagePluginReturn = {
template: string,
options: CompilerDOM.CompilerOptions,
): CompilerDOM.CodegenResult | undefined;
compileSFCStyle?(lang: string, style: string):
| Pick<Sfc['styles'][number], 'imports' | 'bindings' | 'classNames'>
| undefined;
updateSFCTemplate?(
oldResult: CompilerDOM.CodegenResult,
textChange: { start: number; end: number; newText: string },
Expand Down Expand Up @@ -162,7 +165,7 @@ export interface Sfc {
text: string;
offset: number;
}[];
cssVars: {
bindings: {
text: string;
offset: number;
}[];
Expand Down
15 changes: 0 additions & 15 deletions packages/language-core/lib/utils/parseCssClassNames.ts

This file was deleted.

16 changes: 0 additions & 16 deletions packages/language-core/lib/utils/parseCssImports.ts

This file was deleted.

23 changes: 0 additions & 23 deletions packages/language-core/lib/utils/parseCssVars.ts

This file was deleted.

23 changes: 14 additions & 9 deletions packages/language-core/lib/virtualFile/computedSfc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@ import type { SFCBlock, SFCParseResult } from '@vue/compiler-sfc';
import { computed, setCurrentSub } from 'alien-signals';
import type * as ts from 'typescript';
import type { Sfc, SfcBlock, SfcBlockAttr, VueLanguagePluginReturn } from '../types';
import { parseCssClassNames } from '../utils/parseCssClassNames';
import { parseCssImports } from '../utils/parseCssImports';
import { parseCssVars } from '../utils/parseCssVars';
import { computedArray, computedItems } from '../utils/signals';

export const templateInlineTsAsts = new WeakMap<CompilerDOM.RootNode, Map<string, ts.SourceFile>>();
Expand Down Expand Up @@ -133,16 +130,24 @@ export function computedSfc(
const getSrc = computedAttrValue('__src', base, getBlock);
const getModule = computedAttrValue('__module', base, getBlock);
const getScoped = computed(() => !!getBlock().scoped);
const getIr = computed(() => {
for (const plugin of plugins) {
const ast = plugin.compileSFCStyle?.(base.lang, base.content);
if (ast) {
return ast;
}
}
});
const getImports = computedItems(
() => [...parseCssImports(base.content)],
() => getIr()?.imports ?? [],
(oldItem, newItem) => oldItem.text === newItem.text && oldItem.offset === newItem.offset,
);
const getCssVars = computedItems(
() => [...parseCssVars(base.content)],
const getBindings = computedItems(
() => getIr()?.bindings ?? [],
(oldItem, newItem) => oldItem.text === newItem.text && oldItem.offset === newItem.offset,
);
const getClassNames = computedItems(
() => [...parseCssClassNames(base.content)],
() => getIr()?.classNames ?? [],
(oldItem, newItem) => oldItem.text === newItem.text && oldItem.offset === newItem.offset,
);
return () =>
Expand All @@ -159,8 +164,8 @@ export function computedSfc(
get imports() {
return getImports();
},
get cssVars() {
return getCssVars();
get bindings() {
return getBindings();
},
get classNames() {
return getClassNames();
Expand Down
2 changes: 1 addition & 1 deletion packages/language-plugin-pug/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ const plugin: VueLanguagePlugin = ({ modules }) => {
return {
name: require('./package.json').name,

version: 2.1,
version: 2.2,

getEmbeddedCodes(_fileName, sfc) {
if (sfc.template?.lang === 'pug') {
Expand Down
Loading