From ee0bbdb694610f17f57423f3488fc848acf55987 Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Mon, 20 Jan 2025 20:22:06 +0800 Subject: [PATCH 1/6] feat: experimental resolve external stylesheets --- .../language-core/lib/codegen/script/index.ts | 11 +-- .../lib/codegen/script/scriptSetup.ts | 24 +++--- .../language-core/lib/codegen/script/src.ts | 81 +++++++++---------- .../lib/codegen/script/styleModulesType.ts | 42 ---------- .../lib/codegen/script/template.ts | 66 +-------------- .../lib/codegen/style/classProperty.ts | 35 ++++++++ .../lib/codegen/style/externalStylesheet.ts | 23 ++++++ .../lib/codegen/style/modules.ts | 46 +++++++++++ .../lib/codegen/style/scopedClasses.ts | 42 ++++++++++ .../language-core/lib/codegen/utils/index.ts | 22 ++++- packages/language-core/lib/types.ts | 36 ++++++--- .../lib/utils/parseCssImports.ts | 16 ++++ packages/language-core/lib/utils/parseSfc.ts | 62 +++++++++----- packages/language-core/lib/utils/ts.ts | 1 + .../lib/virtualFile/computedSfc.ts | 46 +++++------ .../schemas/vue-tsconfig.schema.json | 4 + 16 files changed, 339 insertions(+), 218 deletions(-) delete mode 100644 packages/language-core/lib/codegen/script/styleModulesType.ts create mode 100644 packages/language-core/lib/codegen/style/classProperty.ts create mode 100644 packages/language-core/lib/codegen/style/externalStylesheet.ts create mode 100644 packages/language-core/lib/codegen/style/modules.ts create mode 100644 packages/language-core/lib/codegen/style/scopedClasses.ts create mode 100644 packages/language-core/lib/utils/parseCssImports.ts diff --git a/packages/language-core/lib/codegen/script/index.ts b/packages/language-core/lib/codegen/script/index.ts index fd93e9c639..7fe62e1ff8 100644 --- a/packages/language-core/lib/codegen/script/index.ts +++ b/packages/language-core/lib/codegen/script/index.ts @@ -5,13 +5,13 @@ import type { ScriptRanges } from '../../parsers/scriptRanges'; import type { ScriptSetupRanges } from '../../parsers/scriptSetupRanges'; import type { Code, Sfc, VueCodeInformation, VueCompilerOptions } from '../../types'; import { generateGlobalTypes } from '../globalTypes'; +import { generateStyleModules } from '../style/modules'; import type { TemplateCodegenContext } from '../template/context'; import { endOfLine, generateSfcBlockSection, newLine } from '../utils'; import { generateComponentSelf } from './componentSelf'; import { createScriptCodegenContext, ScriptCodegenContext } from './context'; import { generateScriptSetup, generateScriptSetupImports } from './scriptSetup'; import { generateSrc } from './src'; -import { generateStyleModulesType } from './styleModulesType'; import { generateTemplate } from './template'; export const codeFeatures = { @@ -76,14 +76,16 @@ export function* generateScript(options: ScriptCodegenOptions): Generator`; } - yield `>(${newLine}` + yield `(${newLine}` + ` __VLS_props: NonNullable>['props'],${newLine}` + ` __VLS_ctx?: ${ctx.localTypes.PrettifyLocal}>, 'attrs' | 'emit' | 'slots'>>,${newLine}` // use __VLS_Prettify for less dts code + ` __VLS_expose?: NonNullable>['expose'],${newLine}` diff --git a/packages/language-core/lib/codegen/script/src.ts b/packages/language-core/lib/codegen/script/src.ts index 350a4b44bf..d52e1197d0 100644 --- a/packages/language-core/lib/codegen/script/src.ts +++ b/packages/language-core/lib/codegen/script/src.ts @@ -1,54 +1,51 @@ -import type { Code, Sfc } from '../../types'; -import { endOfLine } from '../utils'; +import type { Code, SfcBlockAttr } from '../../types'; +import { endOfLine, generateSfcBlockAttrValue } from '../utils'; import { codeFeatures } from './index'; -export function* generateSrc( - script: NonNullable, - src: string -): Generator { - if (src.endsWith('.d.ts')) { - src = src.slice(0, -'.d.ts'.length); +export function* generateSrc(src: SfcBlockAttr): Generator { + if (src === true) { + return; } - else if (src.endsWith('.ts')) { - src = src.slice(0, -'.ts'.length); + let { text } = src; + + if (text.endsWith('.d.ts')) { + text = text.slice(0, -'.d.ts'.length); + } + else if (text.endsWith('.ts')) { + text = text.slice(0, -'.ts'.length); } - else if (src.endsWith('.tsx')) { - src = src.slice(0, -'.tsx'.length) + '.jsx'; + else if (text.endsWith('.tsx')) { + text = text.slice(0, -'.tsx'.length) + '.jsx'; } - if (!src.endsWith('.js') && !src.endsWith('.jsx')) { - src = src + '.js'; + if (!text.endsWith('.js') && !text.endsWith('.jsx')) { + text = text + '.js'; } yield `export * from `; - yield [ - `'${src}'`, - 'script', - script.srcOffset - 1, - { - ...codeFeatures.all, - navigation: src === script.src - ? true - : { - shouldRename: () => false, - resolveRenameEditText(newName) { - if (newName.endsWith('.jsx') || newName.endsWith('.js')) { - newName = newName.split('.').slice(0, -1).join('.'); - } - if (script?.src?.endsWith('.d.ts')) { - newName = newName + '.d.ts'; - } - else if (script?.src?.endsWith('.ts')) { - newName = newName + '.ts'; - } - else if (script?.src?.endsWith('.tsx')) { - newName = newName + '.tsx'; - } - return newName; - }, + yield* generateSfcBlockAttrValue(src, text, { + ...codeFeatures.all, + navigation: text === src.text + ? true + : { + shouldRename: () => false, + resolveRenameEditText(newName) { + if (newName.endsWith('.jsx') || newName.endsWith('.js')) { + newName = newName.split('.').slice(0, -1).join('.'); + } + if (src?.text.endsWith('.d.ts')) { + newName = newName + '.d.ts'; + } + else if (src?.text.endsWith('.ts')) { + newName = newName + '.ts'; + } + else if (src?.text.endsWith('.tsx')) { + newName = newName + '.tsx'; + } + return newName; }, - }, - ]; + }, + }); yield endOfLine; - yield `export { default } from '${src}'${endOfLine}`; + yield `export { default } from '${text}'${endOfLine}`; } diff --git a/packages/language-core/lib/codegen/script/styleModulesType.ts b/packages/language-core/lib/codegen/script/styleModulesType.ts deleted file mode 100644 index 493ad36ccd..0000000000 --- a/packages/language-core/lib/codegen/script/styleModulesType.ts +++ /dev/null @@ -1,42 +0,0 @@ -import type { Code } from '../../types'; -import { endOfLine, newLine } from '../utils'; -import type { ScriptCodegenContext } from './context'; -import { ScriptCodegenOptions, codeFeatures } from './index'; -import { generateCssClassProperty } from './template'; - -export function* generateStyleModulesType( - options: ScriptCodegenOptions, - ctx: ScriptCodegenContext -): Generator { - const styles = options.sfc.styles.map((style, i) => [style, i] as const).filter(([style]) => style.module); - if (!styles.length && !options.scriptSetupRanges?.useCssModule.length) { - return; - } - yield `type __VLS_StyleModules = {${newLine}`; - for (const [style, i] of styles) { - const { name, offset } = style.module!; - if (offset) { - yield [ - name, - 'main', - offset + 1, - codeFeatures.all - ]; - } - else { - yield name; - } - yield `: Record & ${ctx.localTypes.PrettifyLocal}<{}`; - for (const className of style.classNames) { - yield* generateCssClassProperty( - i, - className.text, - className.offset, - 'string', - false - ); - } - yield `>${endOfLine}`; - } - yield `}${endOfLine}`; -} \ No newline at end of file diff --git a/packages/language-core/lib/codegen/script/template.ts b/packages/language-core/lib/codegen/script/template.ts index 7ae52d4789..e202445577 100644 --- a/packages/language-core/lib/codegen/script/template.ts +++ b/packages/language-core/lib/codegen/script/template.ts @@ -1,5 +1,6 @@ import type { Code } from '../../types'; import { hyphenateTag } from '../../utils/shared'; +import { generateStyleScopedClasses } from '../style/scopedClasses'; import { TemplateCodegenContext, createTemplateCodegenContext } from '../template/context'; import { generateInterpolation } from '../template/interpolation'; import { generateStyleScopedClassReferences } from '../template/styleScopedClasses'; @@ -137,71 +138,6 @@ function* generateTemplateBody( yield `type __VLS_TemplateEl = typeof __VLS_rootEl${endOfLine}`; } -function* generateStyleScopedClasses( - options: ScriptCodegenOptions, - ctx: TemplateCodegenContext -): Generator { - const firstClasses = new Set(); - yield `type __VLS_StyleScopedClasses = {}`; - for (let i = 0; i < options.sfc.styles.length; i++) { - const style = options.sfc.styles[i]; - const option = options.vueCompilerOptions.experimentalResolveStyleCssClasses; - if (option === 'always' || (option === 'scoped' && style.scoped)) { - for (const className of style.classNames) { - if (firstClasses.has(className.text)) { - ctx.scopedClasses.push({ - source: 'style_' + i, - className: className.text.slice(1), - offset: className.offset + 1 - }); - continue; - } - firstClasses.add(className.text); - yield* generateCssClassProperty( - i, - className.text, - className.offset, - 'boolean', - true - ); - } - } - } - yield endOfLine; -} - -export function* generateCssClassProperty( - styleIndex: number, - classNameWithDot: string, - offset: number, - propertyType: string, - optional: boolean -): Generator { - yield `${newLine} & { `; - yield [ - '', - 'style_' + styleIndex, - offset, - codeFeatures.navigation, - ]; - yield `'`; - yield [ - classNameWithDot.slice(1), - 'style_' + styleIndex, - offset + 1, - codeFeatures.navigation, - ]; - yield `'`; - yield [ - '', - 'style_' + styleIndex, - offset + classNameWithDot.length, - codeFeatures.navigationWithoutRename, - ]; - yield `${optional ? '?' : ''}: ${propertyType}`; - yield ` }`; -} - function* generateCssVars(options: ScriptCodegenOptions, ctx: TemplateCodegenContext): Generator { if (!options.sfc.styles.length) { return; diff --git a/packages/language-core/lib/codegen/style/classProperty.ts b/packages/language-core/lib/codegen/style/classProperty.ts new file mode 100644 index 0000000000..7a9e9e59e1 --- /dev/null +++ b/packages/language-core/lib/codegen/style/classProperty.ts @@ -0,0 +1,35 @@ +import type { Code } from '../../types'; +import { codeFeatures } from '../script'; +import { newLine } from '../utils'; + +export function* generateClassProperty( + styleIndex: number, + classNameWithDot: string, + offset: number, + propertyType: string, + optional: boolean +): Generator { + yield `${newLine} & { `; + yield [ + '', + 'style_' + styleIndex, + offset, + codeFeatures.navigation, + ]; + yield `'`; + yield [ + classNameWithDot.slice(1), + 'style_' + styleIndex, + offset + 1, + codeFeatures.navigation, + ]; + yield `'`; + yield [ + '', + 'style_' + styleIndex, + offset + classNameWithDot.length, + codeFeatures.navigationWithoutRename, + ]; + yield `${optional ? '?' : ''}: ${propertyType}`; + yield ` }`; +} diff --git a/packages/language-core/lib/codegen/style/externalStylesheet.ts b/packages/language-core/lib/codegen/style/externalStylesheet.ts new file mode 100644 index 0000000000..8ca1671e50 --- /dev/null +++ b/packages/language-core/lib/codegen/style/externalStylesheet.ts @@ -0,0 +1,23 @@ +import { Code, Sfc } from '../../types'; +import { codeFeatures } from '../script'; +import { generateSfcBlockAttrValue, newLine } from '../utils'; + +export function* generateExternalStylesheets( + style: Sfc['styles'][number], +): Generator { + if (typeof style.src === 'object') { + yield `${newLine} & typeof import(`; + yield* generateSfcBlockAttrValue(style.src, style.src.text, codeFeatures.navigation); + yield `).default`; + } + for (const { text, offset } of style.imports) { + yield `${newLine} & typeof import('`; + yield [ + text, + style.name, + offset, + codeFeatures.navigation + ]; + yield `').default`; + } +} diff --git a/packages/language-core/lib/codegen/style/modules.ts b/packages/language-core/lib/codegen/style/modules.ts new file mode 100644 index 0000000000..e12c2638c0 --- /dev/null +++ b/packages/language-core/lib/codegen/style/modules.ts @@ -0,0 +1,46 @@ +import type { Code } from '../../types'; +import type { ScriptCodegenContext } from '../script/context'; +import { ScriptCodegenOptions, codeFeatures } from '../script/index'; +import { endOfLine, newLine } from '../utils'; +import { generateClassProperty } from './classProperty'; +import { generateExternalStylesheets } from './externalStylesheet'; + +export function* generateStyleModules( + options: ScriptCodegenOptions, + ctx: ScriptCodegenContext +): Generator { + const styles = options.sfc.styles.map((style, i) => [style, i] as const).filter(([style]) => style.module); + if (!styles.length && !options.scriptSetupRanges?.useCssModule.length) { + return; + } + yield `type __VLS_StyleModules = {${newLine}`; + for (const [style, i] of styles) { + if (style.module === true) { + yield '$style'; + } + else { + const { text, offset } = style.module!; + yield [ + text, + 'main', + offset, + codeFeatures.navigation + ]; + } + yield `: Record & ${ctx.localTypes.PrettifyLocal}<{}`; + if (options.vueCompilerOptions.experimentalResolveExternalStylesheets) { + yield* generateExternalStylesheets(style); + } + for (const { text, offset } of style.classNames) { + yield* generateClassProperty( + i, + text, + offset, + 'string', + false + ); + } + yield `>${endOfLine}`; + } + yield `}${endOfLine}`; +} diff --git a/packages/language-core/lib/codegen/style/scopedClasses.ts b/packages/language-core/lib/codegen/style/scopedClasses.ts new file mode 100644 index 0000000000..704280d008 --- /dev/null +++ b/packages/language-core/lib/codegen/style/scopedClasses.ts @@ -0,0 +1,42 @@ +import type { Code } from '../../types'; +import type { ScriptCodegenOptions } from '../script'; +import type { TemplateCodegenContext } from '../template/context'; +import { endOfLine } from '../utils'; +import { generateClassProperty } from './classProperty'; +import { generateExternalStylesheets } from './externalStylesheet'; + +export function* generateStyleScopedClasses( + options: ScriptCodegenOptions, + ctx: TemplateCodegenContext +): Generator { + const firstClasses = new Set(); + yield `type __VLS_StyleScopedClasses = {}`; + for (let i = 0; i < options.sfc.styles.length; i++) { + const style = options.sfc.styles[i]; + const option = options.vueCompilerOptions.experimentalResolveStyleCssClasses; + if (option === 'always' || (option === 'scoped' && style.scoped)) { + if (options.vueCompilerOptions.experimentalResolveExternalStylesheets) { + yield* generateExternalStylesheets(style); + } + for (const className of style.classNames) { + if (firstClasses.has(className.text)) { + ctx.scopedClasses.push({ + source: 'style_' + i, + className: className.text.slice(1), + offset: className.offset + 1 + }); + continue; + } + firstClasses.add(className.text); + yield* generateClassProperty( + i, + className.text, + className.offset, + 'boolean', + true + ); + } + } + } + yield endOfLine; +} diff --git a/packages/language-core/lib/codegen/utils/index.ts b/packages/language-core/lib/codegen/utils/index.ts index a39915e108..7b8de9fe38 100644 --- a/packages/language-core/lib/codegen/utils/index.ts +++ b/packages/language-core/lib/codegen/utils/index.ts @@ -1,7 +1,7 @@ import * as CompilerDOM from '@vue/compiler-dom'; import type * as ts from 'typescript'; import { getNodeText } from '../../parsers/scriptSetupRanges'; -import type { Code, SfcBlock, VueCodeInformation } from '../../types'; +import type { Code, SfcBlock, SfcBlockAttr, VueCodeInformation } from '../../types'; export const newLine = `\n`; export const endOfLine = `;${newLine}`; @@ -94,3 +94,23 @@ export function generateSfcBlockSection(block: SfcBlock, start: number, end: num features, ]; } + +export function* generateSfcBlockAttrValue( + src: SfcBlockAttr & object, + text: string, + features: VueCodeInformation +): Generator { + const { offset, quotes } = src; + if (!quotes) { + yield [``, 'main', offset, { verification: true }]; + } + yield [ + `'${text}'`, + 'main', + quotes ? offset - 1 : offset, + features + ]; + if (!quotes) { + yield [``, 'main', offset + text.length, { __combineOffsetMapping: 2 }]; + } +} diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 6d1e05cac5..4fb038b4d9 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -54,6 +54,7 @@ export interface VueCompilerOptions { // experimental experimentalDefinePropProposal: 'kevinEdition' | 'johnsonEdition' | false; experimentalResolveStyleCssClasses: 'scoped' | 'always' | 'never'; + experimentalResolveExternalStylesheets: boolean; experimentalModelPropName: Record | Record[]>>; // internal @@ -103,6 +104,12 @@ export interface SfcBlock { attrs: Record; } +export type SfcBlockAttr = true | { + text: string; + offset: number; + quotes: boolean; +}; + export interface Sfc { content: string; comments: string[]; @@ -112,22 +119,22 @@ export interface Sfc { warnings: CompilerDOM.CompilerError[]; } | undefined; script: (SfcBlock & { - src: string | undefined; - srcOffset: number; + src: SfcBlockAttr | undefined; ast: ts.SourceFile; }) | undefined; scriptSetup: SfcBlock & { // https://github.com/vuejs/rfcs/discussions/436 - generic: string | undefined; - genericOffset: number; + generic: SfcBlockAttr | undefined; ast: ts.SourceFile; } | undefined; styles: readonly (SfcBlock & { + src: SfcBlockAttr | undefined; scoped: boolean; - module?: { - name: string; - offset?: number; - }; + module: SfcBlockAttr | undefined; + imports: { + text: string; + offset: number; + }[], cssVars: { text: string; offset: number; @@ -143,11 +150,16 @@ export interface Sfc { } declare module '@vue/compiler-sfc' { + interface SFCBlock { + __src?: SfcBlockAttr; + } + + interface SFCScriptBlock { + __generic?: SfcBlockAttr; + } + interface SFCStyleBlock { - __module?: { - name: string; - offset?: number; - }; + __module?: SfcBlockAttr; } } diff --git a/packages/language-core/lib/utils/parseCssImports.ts b/packages/language-core/lib/utils/parseCssImports.ts new file mode 100644 index 0000000000..820f0ccff0 --- /dev/null +++ b/packages/language-core/lib/utils/parseCssImports.ts @@ -0,0 +1,16 @@ +const cssImportReg = /(?<=@import\s+url\()(["']?).*?\1(?=\))|(?<=@import\b\s*)(["']).*?\2/g; + +export 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 }; + } + } +} diff --git a/packages/language-core/lib/utils/parseSfc.ts b/packages/language-core/lib/utils/parseSfc.ts index 750297cd23..85146bf7f1 100644 --- a/packages/language-core/lib/utils/parseSfc.ts +++ b/packages/language-core/lib/utils/parseSfc.ts @@ -1,5 +1,5 @@ import type { ElementNode, SourceLocation } from '@vue/compiler-dom'; -import * as compiler from '@vue/compiler-dom'; +import * as CompilerDOM from '@vue/compiler-dom'; import type { CompilerError, SFCBlock, SFCDescriptor, SFCParseResult, SFCScriptBlock, SFCStyleBlock, SFCTemplateBlock } from '@vue/compiler-sfc'; declare module '@vue/compiler-sfc' { @@ -11,7 +11,7 @@ declare module '@vue/compiler-sfc' { export function parse(source: string): SFCParseResult { const errors: CompilerError[] = []; - const ast = compiler.parse(source, { + const ast = CompilerDOM.parse(source, { // there are no components at SFC parsing level isNativeTag: () => true, // preserve all whitespaces @@ -36,11 +36,11 @@ export function parse(source: string): SFCParseResult { shouldForceReload: () => false, }; ast.children.forEach(node => { - if (node.type === compiler.NodeTypes.COMMENT) { + if (node.type === CompilerDOM.NodeTypes.COMMENT) { descriptor.comments.push(node.content); return; } - else if (node.type !== compiler.NodeTypes.ELEMENT) { + else if (node.type !== CompilerDOM.NodeTypes.ELEMENT) { return; } switch (node.tag) { @@ -101,38 +101,64 @@ function createBlock(node: ElementNode, source: string) { end }; const attrs: Record = {}; - const block: SFCBlock - & Pick - & Pick = { + const block: SFCBlock = { type, content, loc, attrs }; node.props.forEach(p => { - if (p.type === compiler.NodeTypes.ATTRIBUTE) { + if (p.type === CompilerDOM.NodeTypes.ATTRIBUTE) { attrs[p.name] = p.value ? p.value.content || true : true; if (p.name === 'lang') { - block.lang = p.value && p.value.content; + block.lang = p.value?.content; } else if (p.name === 'src') { - block.src = p.value && p.value.content; + block.__src = parseAttr(p, node); } - else if (type === 'style') { + else if (isScriptBlock(block)) { + if (p.name === 'setup') { + block.setup = attrs.setup; + } + else if (p.name === 'generic') { + block.__generic = parseAttr(p, node); + } + } + else if (isStyleBlock(block)) { if (p.name === 'scoped') { block.scoped = true; } else if (p.name === 'module') { - block.__module = { - name: p.value?.content ?? '$style', - offset: p.value?.content ? p.value?.loc.start.offset - node.loc.start.offset : undefined - }; + block.__module = parseAttr(p, node); } } - else if (type === 'script' && p.name === 'setup') { - block.setup = attrs.setup; - } } }); return block; } + +function isScriptBlock(block: SFCBlock): block is SFCScriptBlock { + return block.type === 'script'; +} + +function isStyleBlock(block: SFCBlock): block is SFCStyleBlock { + return block.type === 'style'; +} + +function parseAttr(p: CompilerDOM.AttributeNode, node: CompilerDOM.ElementNode) { + if (!p.value) { + return true; + } + const text = p.value.content; + const source = p.value.loc.source; + let offset = p.value.loc.start.offset - node.loc.start.offset; + const quotes = source.startsWith('"') || source.startsWith("'"); + if (quotes) { + offset++; + } + return { + text, + offset, + quotes + }; +} diff --git a/packages/language-core/lib/utils/ts.ts b/packages/language-core/lib/utils/ts.ts index ff9e3471a1..01ac85346c 100644 --- a/packages/language-core/lib/utils/ts.ts +++ b/packages/language-core/lib/utils/ts.ts @@ -256,6 +256,7 @@ function getDefaultOptions(options: Partial): VueCompilerOpt plugins: [], experimentalDefinePropProposal: false, experimentalResolveStyleCssClasses: 'scoped', + experimentalResolveExternalStylesheets: false, experimentalModelPropName: null! }; }; diff --git a/packages/language-core/lib/virtualFile/computedSfc.ts b/packages/language-core/lib/virtualFile/computedSfc.ts index 5471dd165e..77bd34d05b 100644 --- a/packages/language-core/lib/virtualFile/computedSfc.ts +++ b/packages/language-core/lib/virtualFile/computedSfc.ts @@ -2,8 +2,9 @@ import type * as CompilerDOM from '@vue/compiler-dom'; import type { SFCBlock, SFCParseResult } from '@vue/compiler-sfc'; import { activeSub, activeTrackId, computed, ISignal, setActiveSub, Signal, unstable } from 'alien-signals'; import type * as ts from 'typescript'; -import type { Sfc, SfcBlock, VueLanguagePluginReturn } from '../types'; +import type { Sfc, SfcBlock, SfcBlockAttr, VueLanguagePluginReturn } from '../types'; import { parseCssClassNames } from '../utils/parseCssClassNames'; +import { parseCssImports } from '../utils/parseCssImports'; import { parseCssVars } from '../utils/parseCssVars'; export function computedSfc( @@ -53,11 +54,7 @@ export function computedSfc( 'js', computed(() => parsed.get()?.descriptor.script ?? undefined), (block, base): NonNullable => { - const src = computed(() => block.get().src); - const srcOffset = computed(() => { - const _src = src.get(); - return _src ? untrackedSnapshot().getText(0, base.startTagEnd).lastIndexOf(_src) - base.startTagEnd : -1; - }); + const src = computedAttrValue('__src', base, block); const ast = computed(() => { for (const plugin of plugins) { const ast = plugin.compileSFCScript?.(base.lang, base.content); @@ -69,7 +66,6 @@ export function computedSfc( }); return mergeObject(base, { get src() { return src.get(); }, - get srcOffset() { return srcOffset.get(); }, get ast() { return ast.get(); }, }); } @@ -79,14 +75,7 @@ export function computedSfc( 'js', computed(() => parsed.get()?.descriptor.scriptSetup ?? undefined), (block, base): NonNullable => { - const generic = computed(() => { - const _block = block.get(); - return typeof _block.attrs.generic === 'string' ? _block.attrs.generic : undefined; - }); - const genericOffset = computed(() => { - const _generic = generic.get(); - return _generic !== undefined ? untrackedSnapshot().getText(0, base.startTagEnd).lastIndexOf(_generic) - base.startTagEnd : -1; - }); + const generic = computedAttrValue('__generic', base, block); const ast = computed(() => { for (const plugin of plugins) { const ast = plugin.compileSFCScript?.(base.lang, base.content); @@ -98,7 +87,6 @@ export function computedSfc( }); return mergeObject(base, { get generic() { return generic.get(); }, - get genericOffset() { return genericOffset.get(); }, get ast() { return ast.get(); }, }); } @@ -128,19 +116,17 @@ export function computedSfc( computed(() => parsed.get()?.descriptor.styles ?? []), (block, i) => { const base = computedSfcBlock('style_' + i, 'css', block); - const module = computed(() => { - const { __module } = block.get(); - return __module ? { - name: __module.name, - offset: __module.offset ? base.start + __module.offset : undefined - } : undefined; - }); + const src = computedAttrValue('__src', base, block); + const module = computedAttrValue('__module', base, block); const scoped = computed(() => !!block.get().scoped); + const imports = computed(() => [...parseCssImports(base.content)]); const cssVars = computed(() => [...parseCssVars(base.content)]); const classNames = computed(() => [...parseCssClassNames(base.content)]); return () => mergeObject(base, { + get src() { return src.get(); }, get module() { return module.get(); }, get scoped() { return scoped.get(); }, + get imports() { return imports.get(); }, get cssVars() { return cssVars.get(); }, get classNames() { return classNames.get(); }, }) satisfies Sfc['styles'][number]; @@ -312,6 +298,20 @@ export function computedSfc( get end() { return end.get(); }, }; } + + function computedAttrValue( + key: keyof T & string, + base: ReturnType, + block: ISignal + ) { + return computed(() => { + const val = block.get()[key] as SfcBlockAttr | undefined; + if (typeof val === 'object') { + val.offset = base.start + val.offset; + } + return val; + }); + } } function mergeObject(a: T, b: K): T & K { diff --git a/packages/language-core/schemas/vue-tsconfig.schema.json b/packages/language-core/schemas/vue-tsconfig.schema.json index 5c6f33426f..7989a6a195 100644 --- a/packages/language-core/schemas/vue-tsconfig.schema.json +++ b/packages/language-core/schemas/vue-tsconfig.schema.json @@ -107,6 +107,10 @@ ], "markdownDescription": "https://github.com/vuejs/language-tools/issues/1038, https://github.com/vuejs/language-tools/issues/1121" }, + "experimentalResolveExternalStylesheets": { + "type": "boolean", + "default": false + }, "experimentalModelPropName": { "type": "object", "default": { From e7697ebfebba9d99227ed4f8a1ad068a8ba71cdf Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Mon, 20 Jan 2025 20:28:21 +0800 Subject: [PATCH 2/6] test: add --- .../tsc/passedFixtures/#5136/env.d.ts | 6 +++++ .../tsc/passedFixtures/#5136/src.vue | 23 +++++++++++++++++++ .../tsc/passedFixtures/#5136/tsconfig.json | 7 ++++++ 3 files changed, 36 insertions(+) create mode 100644 test-workspace/tsc/passedFixtures/#5136/env.d.ts create mode 100644 test-workspace/tsc/passedFixtures/#5136/src.vue create mode 100644 test-workspace/tsc/passedFixtures/#5136/tsconfig.json diff --git a/test-workspace/tsc/passedFixtures/#5136/env.d.ts b/test-workspace/tsc/passedFixtures/#5136/env.d.ts new file mode 100644 index 0000000000..af15c91426 --- /dev/null +++ b/test-workspace/tsc/passedFixtures/#5136/env.d.ts @@ -0,0 +1,6 @@ +declare module '*.css' { + const classes: { + foo: string; + } + export default classes +} diff --git a/test-workspace/tsc/passedFixtures/#5136/src.vue b/test-workspace/tsc/passedFixtures/#5136/src.vue new file mode 100644 index 0000000000..8ad781c5e5 --- /dev/null +++ b/test-workspace/tsc/passedFixtures/#5136/src.vue @@ -0,0 +1,23 @@ + + + + + diff --git a/test-workspace/tsc/passedFixtures/#5136/tsconfig.json b/test-workspace/tsc/passedFixtures/#5136/tsconfig.json new file mode 100644 index 0000000000..5769f05070 --- /dev/null +++ b/test-workspace/tsc/passedFixtures/#5136/tsconfig.json @@ -0,0 +1,7 @@ +{ + "extends": "../../../tsconfig.base.json", + "vueCompilerOptions": { + "experimentalResolveExternalStylesheets": true + }, + "include": [ "**/*" ] +} \ No newline at end of file From 2a34e435eb2e45327a5b1ab4a3fe45d078641be3 Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Mon, 20 Jan 2025 20:34:39 +0800 Subject: [PATCH 3/6] feat: enable verification --- .../{externalStylesheet.ts => externalStylesheets.ts} | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) rename packages/language-core/lib/codegen/style/{externalStylesheet.ts => externalStylesheets.ts} (67%) diff --git a/packages/language-core/lib/codegen/style/externalStylesheet.ts b/packages/language-core/lib/codegen/style/externalStylesheets.ts similarity index 67% rename from packages/language-core/lib/codegen/style/externalStylesheet.ts rename to packages/language-core/lib/codegen/style/externalStylesheets.ts index 8ca1671e50..35d370177a 100644 --- a/packages/language-core/lib/codegen/style/externalStylesheet.ts +++ b/packages/language-core/lib/codegen/style/externalStylesheets.ts @@ -1,13 +1,17 @@ +import { CodeInformation } from '@volar/language-core'; import { Code, Sfc } from '../../types'; -import { codeFeatures } from '../script'; import { generateSfcBlockAttrValue, newLine } from '../utils'; export function* generateExternalStylesheets( style: Sfc['styles'][number], ): Generator { + const features: CodeInformation = { + navigation: true, + verification: true + }; if (typeof style.src === 'object') { yield `${newLine} & typeof import(`; - yield* generateSfcBlockAttrValue(style.src, style.src.text, codeFeatures.navigation); + yield* generateSfcBlockAttrValue(style.src, style.src.text, features); yield `).default`; } for (const { text, offset } of style.imports) { @@ -16,7 +20,7 @@ export function* generateExternalStylesheets( text, style.name, offset, - codeFeatures.navigation + features ]; yield `').default`; } From c5d0d07bab22eba2000ffaaf464aacd65d17ea78 Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Mon, 20 Jan 2025 20:49:54 +0800 Subject: [PATCH 4/6] fix: import url --- packages/language-core/lib/codegen/style/modules.ts | 2 +- packages/language-core/lib/codegen/style/scopedClasses.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/language-core/lib/codegen/style/modules.ts b/packages/language-core/lib/codegen/style/modules.ts index e12c2638c0..33ef0939ef 100644 --- a/packages/language-core/lib/codegen/style/modules.ts +++ b/packages/language-core/lib/codegen/style/modules.ts @@ -3,7 +3,7 @@ import type { ScriptCodegenContext } from '../script/context'; import { ScriptCodegenOptions, codeFeatures } from '../script/index'; import { endOfLine, newLine } from '../utils'; import { generateClassProperty } from './classProperty'; -import { generateExternalStylesheets } from './externalStylesheet'; +import { generateExternalStylesheets } from './externalStylesheets'; export function* generateStyleModules( options: ScriptCodegenOptions, diff --git a/packages/language-core/lib/codegen/style/scopedClasses.ts b/packages/language-core/lib/codegen/style/scopedClasses.ts index 704280d008..abaff4f619 100644 --- a/packages/language-core/lib/codegen/style/scopedClasses.ts +++ b/packages/language-core/lib/codegen/style/scopedClasses.ts @@ -3,7 +3,7 @@ import type { ScriptCodegenOptions } from '../script'; import type { TemplateCodegenContext } from '../template/context'; import { endOfLine } from '../utils'; import { generateClassProperty } from './classProperty'; -import { generateExternalStylesheets } from './externalStylesheet'; +import { generateExternalStylesheets } from './externalStylesheets'; export function* generateStyleScopedClasses( options: ScriptCodegenOptions, From a10f5a6a8ccfecd36777790e403bf00573afb8ce Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Mon, 20 Jan 2025 21:50:25 +0800 Subject: [PATCH 5/6] refactor: import type --- .../language-core/lib/codegen/style/externalStylesheets.ts | 5 ++--- packages/language-core/lib/codegen/style/modules.ts | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/language-core/lib/codegen/style/externalStylesheets.ts b/packages/language-core/lib/codegen/style/externalStylesheets.ts index 35d370177a..6eccfde656 100644 --- a/packages/language-core/lib/codegen/style/externalStylesheets.ts +++ b/packages/language-core/lib/codegen/style/externalStylesheets.ts @@ -1,11 +1,10 @@ -import { CodeInformation } from '@volar/language-core'; -import { Code, Sfc } from '../../types'; +import type { Code, Sfc, VueCodeInformation } from '../../types'; import { generateSfcBlockAttrValue, newLine } from '../utils'; export function* generateExternalStylesheets( style: Sfc['styles'][number], ): Generator { - const features: CodeInformation = { + const features: VueCodeInformation = { navigation: true, verification: true }; diff --git a/packages/language-core/lib/codegen/style/modules.ts b/packages/language-core/lib/codegen/style/modules.ts index 33ef0939ef..2dfa7dba08 100644 --- a/packages/language-core/lib/codegen/style/modules.ts +++ b/packages/language-core/lib/codegen/style/modules.ts @@ -1,6 +1,6 @@ import type { Code } from '../../types'; import type { ScriptCodegenContext } from '../script/context'; -import { ScriptCodegenOptions, codeFeatures } from '../script/index'; +import { type ScriptCodegenOptions, codeFeatures } from '../script/index'; import { endOfLine, newLine } from '../utils'; import { generateClassProperty } from './classProperty'; import { generateExternalStylesheets } from './externalStylesheets'; From 45ec9ed5e6361cf12c0add42659e43df8cc3a04e Mon Sep 17 00:00:00 2001 From: KazariEX <1364035137@qq.com> Date: Mon, 20 Jan 2025 21:51:44 +0800 Subject: [PATCH 6/6] chore: lint --- test-workspace/tsc/passedFixtures/#5136/env.d.ts | 8 ++++---- test-workspace/tsc/passedFixtures/#5136/src.vue | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test-workspace/tsc/passedFixtures/#5136/env.d.ts b/test-workspace/tsc/passedFixtures/#5136/env.d.ts index af15c91426..0259390453 100644 --- a/test-workspace/tsc/passedFixtures/#5136/env.d.ts +++ b/test-workspace/tsc/passedFixtures/#5136/env.d.ts @@ -1,6 +1,6 @@ declare module '*.css' { - const classes: { - foo: string; - } - export default classes + const classes: { + foo: string; + } + export default classes; } diff --git a/test-workspace/tsc/passedFixtures/#5136/src.vue b/test-workspace/tsc/passedFixtures/#5136/src.vue index 8ad781c5e5..835fed3f01 100644 --- a/test-workspace/tsc/passedFixtures/#5136/src.vue +++ b/test-workspace/tsc/passedFixtures/#5136/src.vue @@ -4,13 +4,13 @@ import { exactType } from '../shared'; const $src = useCssModule("$src"); exactType($src, {} as Record & { - foo: string; + foo: string; }); const $import = useCssModule("$import"); exactType($import, {} as Record & { - foo: string; - bar: string; + foo: string; + bar: string; });