Skip to content

Commit c7fe96f

Browse files
authored
feat(language-core): type support for CSS Modules API (#4674)
1 parent d6bd797 commit c7fe96f

File tree

8 files changed

+188
-29
lines changed

8 files changed

+188
-29
lines changed

packages/language-core/lib/codegen/script/scriptSetup.ts

+70-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { endOfLine, generateSfcBlockSection, newLine } from '../common';
44
import { generateComponent, generateEmitsOption } from './component';
55
import type { ScriptCodegenContext } from './context';
66
import { ScriptCodegenOptions, codeFeatures } from './index';
7-
import { generateTemplate } from './template';
7+
import { generateCssClassProperty, generateTemplate } from './template';
88

99
export function* generateScriptSetupImports(
1010
scriptSetup: NonNullable<Sfc['scriptSetup']>,
@@ -211,6 +211,34 @@ function* generateSetupFunction(
211211
]);
212212
}
213213
}
214+
if (scriptSetupRanges.cssModules.length) {
215+
for (const { exp, arg } of scriptSetupRanges.cssModules) {
216+
if (arg) {
217+
setupCodeModifies.push([
218+
[
219+
` as Omit<__VLS_StyleModules, '$style'>[`,
220+
generateSfcBlockSection(scriptSetup, arg.start, arg.end, codeFeatures.all),
221+
`]`
222+
],
223+
exp.end,
224+
exp.end
225+
]);
226+
}
227+
else {
228+
setupCodeModifies.push([
229+
[
230+
` as __VLS_StyleModules[`,
231+
['', scriptSetup.name, exp.start, codeFeatures.verification],
232+
`'$style'`,
233+
['', scriptSetup.name, exp.end, codeFeatures.verification],
234+
`]`
235+
],
236+
exp.end,
237+
exp.end
238+
]);
239+
}
240+
}
241+
}
214242
for (const { define } of scriptSetupRanges.templateRefs) {
215243
if (define?.arg) {
216244
setupCodeModifies.push([[`<__VLS_Refs[${scriptSetup.content.slice(define.arg.start, define.arg.end)}], keyof __VLS_Refs>`], define.arg.start - 1, define.arg.start - 1]);
@@ -247,6 +275,7 @@ function* generateSetupFunction(
247275

248276
yield* generateComponentProps(options, ctx, scriptSetup, scriptSetupRanges, definePropMirrors);
249277
yield* generateModelEmits(options, scriptSetup, scriptSetupRanges);
278+
yield* generateStyleModules(options, ctx);
250279
yield* generateTemplate(options, ctx, false);
251280
yield `type __VLS_Refs = ReturnType<typeof __VLS_template>['refs']${endOfLine}`;
252281
yield `type __VLS_Slots = ReturnType<typeof __VLS_template>['slots']${endOfLine}`;
@@ -417,6 +446,45 @@ function* generateModelEmits(
417446
yield endOfLine;
418447
}
419448

449+
function* generateStyleModules(
450+
options: ScriptCodegenOptions,
451+
ctx: ScriptCodegenContext
452+
): Generator<Code> {
453+
const styles = options.sfc.styles.filter(style => style.module);
454+
if (!styles.length) {
455+
return;
456+
}
457+
yield `type __VLS_StyleModules = {${newLine}`;
458+
for (let i = 0; i < styles.length; i++) {
459+
const style = styles[i];
460+
const { name, offset } = style.module!;
461+
if (offset) {
462+
yield [
463+
name,
464+
'main',
465+
offset + 1,
466+
codeFeatures.all
467+
];
468+
}
469+
else {
470+
yield name;
471+
}
472+
yield `: Record<string, string> & ${ctx.helperTypes.Prettify.name}<{}`;
473+
for (const className of style.classNames) {
474+
yield* generateCssClassProperty(
475+
i,
476+
className.text,
477+
className.offset,
478+
'string',
479+
false
480+
);
481+
}
482+
yield `>${endOfLine}`;
483+
}
484+
yield `}`;
485+
yield endOfLine;
486+
}
487+
420488
function* generateDefinePropType(
421489
scriptSetup: NonNullable<Sfc['scriptSetup']>,
422490
propName: string | undefined,
@@ -453,7 +521,7 @@ function getPropAndLocalName(
453521
? 'modelValue'
454522
: localName;
455523
if (defineProp.name) {
456-
propName = propName!.replace(/['"]+/g, '')
524+
propName = propName!.replace(/['"]+/g, '');
457525
}
458526
return [propName, localName];
459527
}

packages/language-core/lib/codegen/script/template.ts

+3-21
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export function* generateTemplate(
2424
yield `function __VLS_template() {${newLine}`;
2525
}
2626
const templateCodegenCtx = createTemplateCodegenContext(new Set());
27-
yield* generateCtx(options, ctx, isClassComponent);
27+
yield* generateCtx(options, isClassComponent);
2828
yield* generateTemplateContext(options, templateCodegenCtx);
2929
yield* generateExportOptions(options);
3030
yield* generateConstNameOption(options);
@@ -76,7 +76,6 @@ function* generateConstNameOption(options: ScriptCodegenOptions): Generator<Code
7676

7777
function* generateCtx(
7878
options: ScriptCodegenOptions,
79-
ctx: ScriptCodegenContext,
8079
isClassComponent: boolean
8180
): Generator<Code> {
8281
yield `let __VLS_ctx!: `;
@@ -91,24 +90,7 @@ function* generateCtx(
9190
}
9291
/* CSS Module */
9392
if (options.sfc.styles.some(style => style.module)) {
94-
yield `& {${newLine}`;
95-
for (let i = 0; i < options.sfc.styles.length; i++) {
96-
const style = options.sfc.styles[i];
97-
if (style.module) {
98-
yield `${style.module}: Record<string, string> & ${ctx.helperTypes.Prettify.name}<{}`;
99-
for (const className of style.classNames) {
100-
yield* generateCssClassProperty(
101-
i,
102-
className.text,
103-
className.offset,
104-
'string',
105-
false
106-
);
107-
}
108-
yield `>${endOfLine}`;
109-
}
110-
}
111-
yield `}`;
93+
yield ` & __VLS_StyleModules`;
11294
}
11395
yield endOfLine;
11496
}
@@ -177,7 +159,7 @@ function* generateTemplateContext(
177159
yield `}${endOfLine}`;
178160
}
179161

180-
function* generateCssClassProperty(
162+
export function* generateCssClassProperty(
181163
styleIndex: number,
182164
classNameWithDot: string,
183165
offset: number,

packages/language-core/lib/parsers/scriptSetupRanges.ts

+14
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ export function parseScriptSetupRanges(
4242
name?: string;
4343
inheritAttrs?: string;
4444
} = {};
45+
const cssModules: {
46+
exp: TextRange;
47+
arg?: TextRange;
48+
}[] = [];
4549
const templateRefs: {
4650
name?: string;
4751
define?: ReturnType<typeof parseDefineFunction>;
@@ -108,6 +112,7 @@ export function parseScriptSetupRanges(
108112
emits,
109113
expose,
110114
options,
115+
cssModules,
111116
defineProp,
112117
templateRefs,
113118
};
@@ -371,6 +376,15 @@ export function parseScriptSetupRanges(
371376
define
372377
});
373378
}
379+
else if (vueCompilerOptions.composibles.useCssModule.includes(callText)) {
380+
const module: (typeof cssModules)[number] = {
381+
exp: _getStartEnd(node)
382+
};
383+
if (node.arguments.length) {
384+
module.arg = _getStartEnd(node.arguments[0]);
385+
}
386+
cssModules.push(module);
387+
}
374388
}
375389
ts.forEachChild(node, child => {
376390
parents.push(node);

packages/language-core/lib/types.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ export interface VueCompilerOptions {
4343
withDefaults: string[];
4444
templateRef: string[];
4545
};
46+
composibles: {
47+
useCssModule: string[];
48+
};
4649
plugins: VueLanguagePlugin[];
4750

4851
// experimental
@@ -92,6 +95,13 @@ export interface SfcBlock {
9295
attrs: Record<string, string | true>;
9396
}
9497

98+
export interface SFCStyleOverride {
99+
module?: {
100+
name: string;
101+
offset?: number;
102+
};
103+
}
104+
95105
export interface Sfc {
96106
content: string;
97107
template: SfcBlock & {
@@ -110,8 +120,7 @@ export interface Sfc {
110120
genericOffset: number;
111121
ast: ts.SourceFile;
112122
} | undefined;
113-
styles: readonly (SfcBlock & {
114-
module: string | undefined;
123+
styles: readonly (SfcBlock & SFCStyleOverride & {
115124
scoped: boolean;
116125
cssVars: {
117126
text: string;

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

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CompilerError, SFCDescriptor, SFCBlock, SFCStyleBlock, SFCScriptBlock, SFCTemplateBlock, SFCParseResult } from '@vue/compiler-sfc';
22
import type { ElementNode, SourceLocation } from '@vue/compiler-dom';
33
import * as compiler from '@vue/compiler-dom';
4+
import { SFCStyleOverride } from '../types';
45

56
export function parse(source: string): SFCParseResult {
67

@@ -90,7 +91,10 @@ function createBlock(node: ElementNode, source: string) {
9091
end
9192
};
9293
const attrs: Record<string, any> = {};
93-
const block: SFCBlock & Pick<SFCStyleBlock, 'scoped' | 'module'> & Pick<SFCScriptBlock, 'setup'> = {
94+
const block: SFCBlock
95+
& Pick<SFCStyleBlock, 'scoped'>
96+
& Pick<SFCStyleOverride, 'module'>
97+
& Pick<SFCScriptBlock, 'setup'> = {
9498
type,
9599
content,
96100
loc,
@@ -110,7 +114,10 @@ function createBlock(node: ElementNode, source: string) {
110114
block.scoped = true;
111115
}
112116
else if (p.name === 'module') {
113-
block.module = attrs[p.name];
117+
block.module = {
118+
name: p.value?.content ?? '$style',
119+
offset: p.value?.content ? p.value?.loc.start.offset - node.loc.start.offset : undefined
120+
};
114121
}
115122
}
116123
else if (type === 'script' && p.name === 'setup') {

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

+3
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ export function resolveVueCompilerOptions(vueOptions: Partial<VueCompilerOptions
233233
templateRef: ['templateRef', 'useTemplateRef'],
234234
...vueOptions.macros,
235235
},
236+
composibles: {
237+
useCssModule: ['useCssModule']
238+
},
236239
plugins: vueOptions.plugins ?? [],
237240

238241
// experimental

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type * as CompilerDOM from '@vue/compiler-dom';
22
import type { SFCBlock, SFCParseResult } from '@vue/compiler-sfc';
33
import { computed, computedArray, pauseTracking, resetTracking } from 'computeds';
44
import type * as ts from 'typescript';
5-
import type { Sfc, SfcBlock, VueLanguagePluginReturn } from '../types';
5+
import type { Sfc, SfcBlock, SFCStyleOverride, VueLanguagePluginReturn } from '../types';
66
import { parseCssClassNames } from '../utils/parseCssClassNames';
77
import { parseCssVars } from '../utils/parseCssVars';
88

@@ -117,7 +117,13 @@ export function computedSfc(
117117
computed(() => parsed()?.descriptor.styles ?? []),
118118
(block, i) => {
119119
const base = computedSfcBlock('style_' + i, 'css', block);
120-
const module = computed(() => typeof block().module === 'string' ? block().module as string : block().module ? '$style' : undefined);
120+
const module = computed(() => {
121+
const _module = block().module as SFCStyleOverride['module'];
122+
return _module ? {
123+
name: _module.name,
124+
offset: _module.offset ? base.start + _module.offset : undefined
125+
} : undefined;
126+
});
121127
const scoped = computed(() => !!block().scoped);
122128
const cssVars = computed(() => [...parseCssVars(base.content)]);
123129
const classNames = computed(() => [...parseCssClassNames(base.content)]);

packages/language-server/tests/renaming.spec.ts

+70
Original file line numberDiff line numberDiff line change
@@ -747,6 +747,76 @@ describe('Renaming', async () => {
747747
`);
748748
});
749749

750+
it('#4673', async () => {
751+
expect(
752+
await requestRename('fixture.vue', 'vue', `
753+
<script setup lang="ts">
754+
import { useCssModule } from 'vue';
755+
const $style = useCssModule();
756+
const stylAlias = useCssModule('styl');
757+
</script>
758+
759+
<template>
760+
<div :class="styl|.foo">{{ }}</div>
761+
</template>
762+
763+
<style module>
764+
.foo { }
765+
</style>
766+
767+
<style module="styl">
768+
.foo { }
769+
</style>
770+
`, 'stylus')
771+
).toMatchInlineSnapshot(`
772+
{
773+
"changes": {
774+
"file://\${testWorkspacePath}/fixture.vue": [
775+
{
776+
"newText": "stylus",
777+
"range": {
778+
"end": {
779+
"character": 22,
780+
"line": 8,
781+
},
782+
"start": {
783+
"character": 18,
784+
"line": 8,
785+
},
786+
},
787+
},
788+
{
789+
"newText": "stylus",
790+
"range": {
791+
"end": {
792+
"character": 23,
793+
"line": 15,
794+
},
795+
"start": {
796+
"character": 19,
797+
"line": 15,
798+
},
799+
},
800+
},
801+
{
802+
"newText": "stylus",
803+
"range": {
804+
"end": {
805+
"character": 40,
806+
"line": 4,
807+
},
808+
"start": {
809+
"character": 36,
810+
"line": 4,
811+
},
812+
},
813+
},
814+
],
815+
},
816+
}
817+
`);
818+
});
819+
750820
const openedDocuments: TextDocument[] = [];
751821

752822
afterEach(async () => {

0 commit comments

Comments
 (0)