Skip to content

Commit 0f2e0b8

Browse files
authored
feat(language-core): resolve external stylesheets (#5136)
1 parent 4c6d2b2 commit 0f2e0b8

File tree

11 files changed

+116
-2
lines changed

11 files changed

+116
-2
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Code, Sfc, VueCodeInformation } from '../../types';
2+
import { combineLastMapping, newLine } from '../utils';
3+
import { wrapWith } from '../utils/wrapWith';
4+
5+
export function* generateExternalStylesheets(
6+
style: Sfc['styles'][number]
7+
): Generator<Code> {
8+
const features: VueCodeInformation = {
9+
navigation: true,
10+
verification: true
11+
};
12+
if (typeof style.src === 'object') {
13+
yield `${newLine} & typeof import(`;
14+
yield* wrapWith(
15+
style.src.offset,
16+
style.src.offset + style.src.text.length,
17+
'main',
18+
features,
19+
`'`,
20+
[style.src.text, 'main', style.src.offset, combineLastMapping],
21+
`'`
22+
);
23+
yield `).default`;
24+
}
25+
for (const { text, offset } of style.imports) {
26+
yield `${newLine} & typeof import('`;
27+
yield [
28+
text,
29+
style.name,
30+
offset,
31+
features
32+
];
33+
yield `').default`;
34+
}
35+
}

packages/language-core/lib/codegen/style/modules.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { codeFeatures } from '../codeFeatures';
33
import type { ScriptCodegenOptions } from '../script';
44
import { endOfLine, newLine } from '../utils';
55
import { generateClassProperty } from './classProperty';
6+
import { generateExternalStylesheets } from './externalStylesheets';
67

78
export function* generateStyleModules(
89
options: ScriptCodegenOptions
@@ -22,10 +23,13 @@ export function* generateStyleModules(
2223
text,
2324
'main',
2425
offset,
25-
codeFeatures.withoutHighlight
26+
codeFeatures.navigation,
2627
];
2728
}
2829
yield `: Record<string, string> & __VLS_PrettifyGlobal<{}`;
30+
if (options.vueCompilerOptions.resolveExternalStylesheets) {
31+
yield* generateExternalStylesheets(style);
32+
}
2933
for (const className of style.classNames) {
3034
yield* generateClassProperty(
3135
i,

packages/language-core/lib/codegen/style/scopedClasses.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { ScriptCodegenOptions } from '../script';
33
import type { TemplateCodegenContext } from '../template/context';
44
import { endOfLine } from '../utils';
55
import { generateClassProperty } from './classProperty';
6+
import { generateExternalStylesheets } from './externalStylesheets';
67

78
export function* generateStyleScopedClasses(
89
options: ScriptCodegenOptions,
@@ -19,6 +20,9 @@ export function* generateStyleScopedClasses(
1920
const firstClasses = new Set<string>();
2021
yield `type __VLS_StyleScopedClasses = {}`;
2122
for (const [style, i] of styles) {
23+
if (options.vueCompilerOptions.resolveExternalStylesheets) {
24+
yield* generateExternalStylesheets(style);
25+
}
2226
for (const className of style.classNames) {
2327
if (firstClasses.has(className.text)) {
2428
ctx.scopedClasses.push({

packages/language-core/lib/types.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export interface VueCompilerOptions {
4343
inferTemplateDollarSlots: boolean;
4444
skipTemplateCodegen: boolean;
4545
fallthroughAttributes: boolean;
46+
resolveExternalStylesheets: boolean;
4647
fallthroughComponentNames: string[];
4748
dataAttributes: string[];
4849
htmlAttributes: string[];
@@ -139,8 +140,13 @@ export interface Sfc {
139140
ast: ts.SourceFile;
140141
} | undefined;
141142
styles: readonly (SfcBlock & {
143+
src: SfcBlockAttr | undefined;
144+
module: SfcBlockAttr | undefined;
142145
scoped: boolean;
143-
module?: SfcBlockAttr | undefined;
146+
imports: {
147+
text: string;
148+
offset: number;
149+
}[],
144150
cssVars: {
145151
text: string;
146152
offset: number;
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const cssImportReg = /(?<=@import\s+url\()(["']?).*?\1(?=\))|(?<=@import\b\s*)(["']).*?\2/g;
2+
3+
export function* parseCssImports(css: string) {
4+
const matches = css.matchAll(cssImportReg);
5+
for (const match of matches) {
6+
let text = match[0];
7+
let offset = match.index;
8+
if (text.startsWith('\'') || text.startsWith('"')) {
9+
text = text.slice(1, -1);
10+
offset += 1;
11+
}
12+
if (text) {
13+
yield { text, offset };
14+
}
15+
}
16+
}

packages/language-core/lib/utils/ts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,7 @@ export function getDefaultCompilerOptions(target = 99, lib = 'vue', strictTempla
281281
inferTemplateDollarSlots: false,
282282
skipTemplateCodegen: false,
283283
fallthroughAttributes: false,
284+
resolveExternalStylesheets: false,
284285
fallthroughComponentNames: [
285286
'Transition',
286287
'KeepAlive',

packages/language-core/lib/virtualFile/computedSfc.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { computed, setCurrentSub } from 'alien-signals';
44
import type * as ts from 'typescript';
55
import type { Sfc, SfcBlock, SfcBlockAttr, VueLanguagePluginReturn } from '../types';
66
import { parseCssClassNames } from '../utils/parseCssClassNames';
7+
import { parseCssImports } from '../utils/parseCssImports';
78
import { parseCssVars } from '../utils/parseCssVars';
89
import { computedArray, computedItems } from '../utils/signals';
910

@@ -114,8 +115,13 @@ export function computedSfc(
114115
computed(() => getParseResult()?.descriptor.styles ?? []),
115116
(getBlock, i) => {
116117
const base = computedSfcBlock('style_' + i, 'css', getBlock);
118+
const getSrc = computedAttrValue('__src', base, getBlock);
117119
const getModule = computedAttrValue('__module', base, getBlock);
118120
const getScoped = computed(() => !!getBlock().scoped);
121+
const getImports = computedItems(
122+
() => [...parseCssImports(base.content)],
123+
(oldItem, newItem) => oldItem.text === newItem.text && oldItem.offset === newItem.offset
124+
);
119125
const getCssVars = computedItems(
120126
() => [...parseCssVars(base.content)],
121127
(oldItem, newItem) => oldItem.text === newItem.text && oldItem.offset === newItem.offset
@@ -125,8 +131,10 @@ export function computedSfc(
125131
(oldItem, newItem) => oldItem.text === newItem.text && oldItem.offset === newItem.offset
126132
);
127133
return () => mergeObject(base, {
134+
get src() { return getSrc(); },
128135
get module() { return getModule(); },
129136
get scoped() { return getScoped(); },
137+
get imports() { return getImports(); },
130138
get cssVars() { return getCssVars(); },
131139
get classNames() { return getClassNames(); },
132140
}) satisfies Sfc['styles'][number];

packages/language-core/schemas/vue-tsconfig.schema.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@
177177
],
178178
"markdownDescription": "https://github.com/vuejs/language-tools/issues/1038, https://github.com/vuejs/language-tools/issues/1121"
179179
},
180+
"experimentalResolveExternalStylesheets": {
181+
"type": "boolean",
182+
"default": false
183+
},
180184
"experimentalModelPropName": {
181185
"type": "object",
182186
"default": {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
declare module '*.css' {
2+
const classes: {
3+
foo: string;
4+
}
5+
export default classes;
6+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<script setup lang="ts">
2+
import { useCssModule } from 'vue';
3+
import { exactType } from '../shared';
4+
5+
const $src = useCssModule("$src");
6+
exactType($src, {} as Record<string, string> & {
7+
foo: string;
8+
});
9+
10+
const $import = useCssModule("$import");
11+
exactType($import, {} as Record<string, string> & {
12+
foo: string;
13+
bar: string;
14+
});
15+
</script>
16+
17+
<style module="$src" src="./main.css"></style>
18+
19+
<style module="$import">
20+
@import "./main.css";
21+
22+
.bar { }
23+
</style>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"extends": "../../../tsconfig.base.json",
3+
"vueCompilerOptions": {
4+
"experimentalResolveExternalStylesheets": true
5+
},
6+
"include": [ "**/*" ]
7+
}

0 commit comments

Comments
 (0)