diff --git a/packages/language-core/lib/codegen/script/template.ts b/packages/language-core/lib/codegen/script/template.ts index f273a6ddac..9ca16a8791 100644 --- a/packages/language-core/lib/codegen/script/template.ts +++ b/packages/language-core/lib/codegen/script/template.ts @@ -7,6 +7,7 @@ import { forEachInterpolationSegment } from '../template/interpolation'; import type { ScriptCodegenContext } from './context'; import { codeFeatures, type ScriptCodegenOptions } from './index'; import { generateInternalComponent } from './internalComponent'; +import { generateStyleScopedClasses } from '../template/styleScopedClasses'; export function* generateTemplate( options: ScriptCodegenOptions, @@ -124,13 +125,23 @@ function* generateTemplateContext( yield `let __VLS_components!: typeof __VLS_localComponents & __VLS_GlobalComponents & typeof __VLS_ctx${endOfLine}`; // for html completion, TS references... /* Style Scoped */ + const firstClasses = new Set(); yield `/* Style Scoped */${newLine}`; - yield `type __VLS_StyleScopedClasses = {}`; + yield `let __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)) { + templateCodegenCtx.scopedClasses.push({ + source: 'style_' + i, + className: className.text.slice(1), + offset: className.offset + 1 + }); + continue; + } + firstClasses.add(className.text); yield* generateCssClassProperty( i, className.text, @@ -142,7 +153,7 @@ function* generateTemplateContext( } } yield endOfLine; - yield `let __VLS_styleScopedClasses!: __VLS_StyleScopedClasses | keyof __VLS_StyleScopedClasses | (keyof __VLS_StyleScopedClasses)[]${endOfLine}`; + yield* generateStyleScopedClasses(templateCodegenCtx, true); yield* generateCssVars(options, templateCodegenCtx); if (options.templateCodegen) { @@ -173,7 +184,7 @@ function* generateCssClassProperty( '', 'style_' + styleIndex, offset, - codeFeatures.navigationWithoutRename, + codeFeatures.navigation, ]; yield `'`; yield [ diff --git a/packages/language-core/lib/codegen/template/context.ts b/packages/language-core/lib/codegen/template/context.ts index 10d1a7addd..480643d1a8 100644 --- a/packages/language-core/lib/codegen/template/context.ts +++ b/packages/language-core/lib/codegen/template/context.ts @@ -109,7 +109,11 @@ export function createTemplateCodegenContext(scriptSetupBindingNames: TemplateCo const hasSlotElements = new Set();; const blockConditions: string[] = []; const usedComponentCtxVars = new Set(); - const scopedClasses: { className: string, offset: number; }[] = []; + const scopedClasses: { + source: string; + className: string; + offset: number; + }[] = []; const emptyClassOffsets: number[] = []; const inlayHints: InlayHintInfo[] = []; diff --git a/packages/language-core/lib/codegen/template/element.ts b/packages/language-core/lib/codegen/template/element.ts index 91af8b4bdb..4bb0f87c87 100644 --- a/packages/language-core/lib/codegen/template/element.ts +++ b/packages/language-core/lib/codegen/template/element.ts @@ -1,4 +1,5 @@ import * as CompilerDOM from '@vue/compiler-dom'; +import type * as ts from 'typescript'; import { camelize, capitalize } from '@vue/shared'; import type { Code, VueCodeInformation } from '../../types'; import { hyphenateTag } from '../../utils/shared'; @@ -397,7 +398,7 @@ function* generateVScope( yield* generateElementDirectives(options, ctx, node); yield* generateReferencesForElements(options, ctx, node); // - yield* generateReferencesForScopedCssClasses(ctx, node); + yield* generateReferencesForScopedCssClasses(options, ctx, node); if (inScope) { yield `}${newLine}`; @@ -575,6 +576,7 @@ function* generateReferencesForElements( } function* generateReferencesForScopedCssClasses( + options: TemplateCodegenOptions, ctx: TemplateCodegenContext, node: CompilerDOM.ElementNode ): Generator { @@ -586,28 +588,17 @@ function* generateReferencesForScopedCssClasses( ) { let startOffset = prop.value.loc.start.offset; let content = prop.value.loc.source; + let isWrapped = false; if ( (content.startsWith(`'`) && content.endsWith(`'`)) || (content.startsWith(`"`) && content.endsWith(`"`)) ) { - startOffset++; content = content.slice(1, -1); + isWrapped = true; } if (content) { - let currentClassName = ''; - for (const char of (content + ' ')) { - if (char.trim() === '') { - if (currentClassName !== '') { - ctx.scopedClasses.push({ className: currentClassName, offset: startOffset }); - startOffset += currentClassName.length; - currentClassName = ''; - } - startOffset += char.length; - } - else { - currentClassName += char; - } - } + const classes = collectClasses(content, startOffset + (isWrapped ? 1 : 0)); + ctx.scopedClasses.push(...classes); } else { ctx.emptyClassOffsets.push(startOffset); @@ -619,14 +610,84 @@ function* generateReferencesForScopedCssClasses( && prop.exp?.type === CompilerDOM.NodeTypes.SIMPLE_EXPRESSION && prop.arg.content === 'class' ) { - yield `__VLS_styleScopedClasses = (`; - yield [ - prop.exp.content, - 'template', - prop.exp.loc.start.offset, - ctx.codeFeatures.navigationAndCompletion, - ]; - yield `)${endOfLine}`; + const content = '`${' + prop.exp.content + '}`'; + const startOffset = prop.exp.loc.start.offset - 3; + + const { ts } = options; + const ast = ts.createSourceFile('', content, 99 satisfies typeof ts.ScriptTarget.Latest); + const literals: ts.StringLiteralLike[] = []; + + ts.forEachChild(ast, node => { + if ( + !ts.isExpressionStatement(node) || + !isTemplateExpression(node.expression) + ) { + return; + } + + const expression = node.expression.templateSpans[0].expression; + + if (ts.isStringLiteralLike(expression)) { + literals.push(expression); + } + + if (ts.isArrayLiteralExpression(expression)) { + walkArrayLiteral(expression); + } + + if (ts.isObjectLiteralExpression(expression)) { + walkObjectLiteral(expression); + } + }); + + for (const literal of literals) { + const classes = collectClasses( + literal.text, + literal.end - literal.text.length - 1 + startOffset + ); + ctx.scopedClasses.push(...classes); + } + + function walkArrayLiteral(node: ts.ArrayLiteralExpression) { + const { elements } = node; + for (const element of elements) { + if (ts.isStringLiteralLike(element)) { + literals.push(element); + } + else if (ts.isObjectLiteralExpression(element)) { + walkObjectLiteral(element); + } + } + } + + function walkObjectLiteral(node: ts.ObjectLiteralExpression) { + const { properties } = node; + for (const property of properties) { + if (ts.isPropertyAssignment(property)) { + const { name } = property; + if (ts.isIdentifier(name)) { + walkIdentifier(name); + } + else if (ts.isComputedPropertyName(name)) { + const { expression } = name; + if (ts.isStringLiteralLike(expression)) { + literals.push(expression); + } + } + } + else if (ts.isShorthandPropertyAssignment(property)) { + walkIdentifier(property.name); + } + } + } + + function walkIdentifier(node: ts.Identifier) { + ctx.scopedClasses.push({ + source: 'template', + className: node.text, + offset: node.end - node.text.length + startOffset + }); + } } } } @@ -638,3 +699,37 @@ function camelizeComponentName(newName: string) { function getTagRenameApply(oldName: string) { return oldName === hyphenateTag(oldName) ? hyphenateTag : undefined; } + +function collectClasses(content: string, startOffset = 0) { + const classes: { + source: string; + className: string; + offset: number; + }[] = []; + + let currentClassName = ''; + let offset = 0; + for (const char of (content + ' ')) { + if (char.trim() === '') { + if (currentClassName !== '') { + classes.push({ + source: 'template', + className: currentClassName, + offset: offset + startOffset + }); + offset += currentClassName.length; + currentClassName = ''; + } + offset += char.length; + } + else { + currentClassName += char; + } + } + return classes; +} + +// isTemplateExpression is missing in tsc +function isTemplateExpression(node: ts.Node): node is ts.TemplateExpression { + return node.kind === 228 satisfies ts.SyntaxKind.TemplateExpression; +} \ No newline at end of file diff --git a/packages/language-core/lib/codegen/template/index.ts b/packages/language-core/lib/codegen/template/index.ts index 5c5665e168..6f70173f97 100644 --- a/packages/language-core/lib/codegen/template/index.ts +++ b/packages/language-core/lib/codegen/template/index.ts @@ -6,6 +6,7 @@ import { TemplateCodegenContext, createTemplateCodegenContext } from './context' import { getCanonicalComponentName, getPossibleOriginalComponentNames } from './element'; import { generateObjectProperty } from './objectProperty'; import { generateTemplateChild, getVForNode } from './templateChild'; +import { generateStyleScopedClasses } from './styleScopedClasses'; export interface TemplateCodegenOptions { ts: typeof ts; @@ -36,7 +37,7 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator { - yield `if (typeof __VLS_styleScopedClasses === 'object' && !Array.isArray(__VLS_styleScopedClasses)) {${newLine}`; - for (const offset of ctx.emptyClassOffsets) { - yield `__VLS_styleScopedClasses['`; - yield [ - '', - 'template', - offset, - ctx.codeFeatures.additionalCompletion, - ]; - yield `']${endOfLine}`; - } - for (const { className, offset } of ctx.scopedClasses) { - yield `__VLS_styleScopedClasses[`; - yield [ - '', - 'template', - offset, - ctx.codeFeatures.navigationWithoutRename, - ]; - yield `'`; - - // fix https://github.com/vuejs/language-tools/issues/4537 - yield* escapeString(className, offset, ['\\', '\'']); - yield `'`; - yield [ - '', - 'template', - offset + className.length, - ctx.codeFeatures.navigationWithoutRename, - ]; - yield `]${endOfLine}`; - } - yield `}${newLine}`; - } - function* generatePreResolveComponents(): Generator { yield `let __VLS_resolvedLocalAndGlobalComponents!: {}`; if (options.template.ast) { @@ -144,45 +109,6 @@ export function* generateTemplate(options: TemplateCodegenOptions): Generator { - let count = 0; - - const currentEscapeTargets = [...escapeTargets]; - const firstEscapeTarget = currentEscapeTargets.shift()!; - const splitted = className.split(firstEscapeTarget); - - for (let i = 0; i < splitted.length; i++) { - const part = splitted[i]; - const partLength = part.length; - - if (escapeTargets.length > 0) { - yield* escapeString(part, offset + count, [...currentEscapeTargets]); - } else { - yield [ - part, - 'template', - offset + count, - ctx.codeFeatures.navigationAndAdditionalCompletion, - ]; - } - - if (i !== splitted.length - 1) { - yield '\\'; - - yield [ - firstEscapeTarget, - 'template', - offset + count + partLength, - ctx.codeFeatures.navigationAndAdditionalCompletion, - ]; - - count += partLength + 1; - } else { - count += partLength; - } - } - } } export function* forEachElementNode(node: CompilerDOM.RootNode | CompilerDOM.TemplateChildNode): Generator { diff --git a/packages/language-core/lib/codegen/template/styleScopedClasses.ts b/packages/language-core/lib/codegen/template/styleScopedClasses.ts new file mode 100644 index 0000000000..fe1777866b --- /dev/null +++ b/packages/language-core/lib/codegen/template/styleScopedClasses.ts @@ -0,0 +1,80 @@ +import type { Code } from '../../types'; +import type { TemplateCodegenContext } from './context'; +import { endOfLine, newLine } from '../common'; + +export function* generateStyleScopedClasses( + ctx: TemplateCodegenContext, + withDot = false +): Generator { + for (const offset of ctx.emptyClassOffsets) { + yield `__VLS_styleScopedClasses['`; + yield [ + '', + 'template', + offset, + ctx.codeFeatures.additionalCompletion, + ]; + yield `']${endOfLine}`; + } + for (const { source, className, offset } of ctx.scopedClasses) { + yield `__VLS_styleScopedClasses[`; + yield [ + '', + source, + offset - (withDot ? 1 : 0), + ctx.codeFeatures.navigation, + ]; + yield `'`; + + // fix https://github.com/vuejs/language-tools/issues/4537 + yield* escapeString(source, className, offset, ['\\', '\'']); + yield `'`; + yield [ + '', + source, + offset + className.length, + ctx.codeFeatures.navigationWithoutRename, + ]; + yield `]${endOfLine}`; + } + yield newLine; + + function* escapeString(source: string, className: string, offset: number, escapeTargets: string[]): Generator { + let count = 0; + + const currentEscapeTargets = [...escapeTargets]; + const firstEscapeTarget = currentEscapeTargets.shift()!; + const splitted = className.split(firstEscapeTarget); + + for (let i = 0; i < splitted.length; i++) { + const part = splitted[i]; + const partLength = part.length; + + if (escapeTargets.length > 0) { + yield* escapeString(source, part, offset + count, [...currentEscapeTargets]); + } else { + yield [ + part, + source, + offset + count, + ctx.codeFeatures.navigationAndAdditionalCompletion, + ]; + } + + if (i !== splitted.length - 1) { + yield '\\'; + + yield [ + firstEscapeTarget, + source, + offset + count + partLength, + ctx.codeFeatures.navigationAndAdditionalCompletion, + ]; + + count += partLength + 1; + } else { + count += partLength; + } + } + } +} \ No newline at end of file diff --git a/test-workspace/tsc/passedFixtures/#3688/main.vue b/test-workspace/tsc/passedFixtures/#3688/main.vue index d894172b00..7fbf85ca0a 100644 --- a/test-workspace/tsc/passedFixtures/#3688/main.vue +++ b/test-workspace/tsc/passedFixtures/#3688/main.vue @@ -1,8 +1,6 @@