Skip to content
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

feat(language-core): enhance single root nodes collection #4819

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
12 changes: 7 additions & 5 deletions packages/language-core/lib/codegen/script/component.ts
Original file line number Diff line number Diff line change
@@ -40,11 +40,13 @@ export function* generateComponent(
const { args } = options.scriptRanges.exportDefault;
yield generateSfcBlockSection(options.sfc.script, args.start + 1, args.end - 1, codeFeatures.all);
}
if (options.vueCompilerOptions.target >= 3.5 && scriptSetupRanges.templateRefs.length) {
yield `__typeRefs: {} as __VLS_TemplateResult['refs'],${newLine}`;
}
if (options.vueCompilerOptions.target >= 3.5 && options.templateCodegen?.singleRootElType) {
yield `__typeEl: {} as __VLS_TemplateResult['rootEl'],${newLine}`;
if (options.vueCompilerOptions.target >= 3.5) {
if (scriptSetupRanges.templateRefs.length) {
yield `__typeRefs: {} as __VLS_TemplateResult['refs'],${newLine}`;
}
if (options.templateCodegen?.singleRootElTypes.length) {
yield `__typeEl: {} as __VLS_TemplateResult['rootEl'],${newLine}`;
}
}
yield `})`;
}
6 changes: 3 additions & 3 deletions packages/language-core/lib/codegen/template/context.ts
Original file line number Diff line number Diff line change
@@ -130,10 +130,10 @@ export function createTemplateCodegenContext(options: Pick<TemplateCodegenOption
emptyClassOffsets,
inlayHints,
hasSlot: false,
inheritedAttrVars: new Set(),
inheritedAttrVars: new Set<string>(),
templateRefs,
singleRootElType: undefined as string | undefined,
singleRootNode: undefined as CompilerDOM.ElementNode | undefined,
singleRootElTypes: [] as string[],
singleRootNodes: new Set<CompilerDOM.ElementNode | null>(),
accessExternalVariable(name: string, offset?: number) {
let arr = accessExternalVariables.get(name);
if (!arr) {
13 changes: 7 additions & 6 deletions packages/language-core/lib/codegen/template/element.ts
Original file line number Diff line number Diff line change
@@ -233,7 +233,8 @@ export function* generateComponent(
}

const [refName, offset] = yield* generateVScope(options, ctx, node, props);
const isRootNode = node === ctx.singleRootNode;
const tag = hyphenateTag(node.tag);
const isRootNode = ctx.singleRootNodes.has(node) && !options.vueCompilerOptions.fallthroughComponentTags.includes(tag);

if (refName || isRootNode) {
const varName = ctx.getInternalVariable();
@@ -252,7 +253,7 @@ export function* generateComponent(
ctx.templateRefs.set(refName, [varName, offset!]);
}
if (isRootNode) {
ctx.singleRootElType = `NonNullable<typeof ${varName}>['$el']`;
ctx.singleRootElTypes.push(`NonNullable<typeof ${varName}>['$el']`);
}
}

@@ -267,7 +268,7 @@ export function* generateComponent(
options.vueCompilerOptions.fallthroughAttributes
&& (
node.props.some(prop => prop.type === CompilerDOM.NodeTypes.DIRECTIVE && prop.name === 'bind' && prop.exp?.loc.source === '$attrs')
|| node === ctx.singleRootNode
|| ctx.singleRootNodes.has(node)
)
) {
const varAttrs = ctx.getInternalVariable();
@@ -352,8 +353,8 @@ export function* generateElement(
if (refName) {
ctx.templateRefs.set(refName, [`__VLS_nativeElements['${node.tag}']`, offset!]);
}
if (ctx.singleRootNode === node) {
ctx.singleRootElType = `typeof __VLS_nativeElements['${node.tag}']`;
if (ctx.singleRootNodes.has(node)) {
ctx.singleRootElTypes.push(`typeof __VLS_nativeElements['${node.tag}']`);
}

const slotDir = node.props.find(p => p.type === CompilerDOM.NodeTypes.DIRECTIVE && p.name === 'slot') as CompilerDOM.DirectiveNode;
@@ -368,7 +369,7 @@ export function* generateElement(
options.vueCompilerOptions.fallthroughAttributes
&& (
node.props.some(prop => prop.type === CompilerDOM.NodeTypes.DIRECTIVE && prop.name === 'bind' && prop.exp?.loc.source === '$attrs')
|| node === ctx.singleRootNode
|| ctx.singleRootNodes.has(node)
)
) {
ctx.inheritedAttrVars.add(`__VLS_intrinsicElements.${node.tag}`);
11 changes: 8 additions & 3 deletions packages/language-core/lib/codegen/template/index.ts
Original file line number Diff line number Diff line change
@@ -113,12 +113,17 @@ function* generateRefs(ctx: TemplateCodegenContext): Generator<Code> {
}

function* generateRootEl(ctx: TemplateCodegenContext): Generator<Code> {
if (ctx.singleRootElType) {
yield `var $el!: ${ctx.singleRootElType}${endOfLine}`;
yield `var $el!: `;
if (ctx.singleRootElTypes.length && !ctx.singleRootNodes.has(null)) {
yield newLine;
for (const type of ctx.singleRootElTypes) {
yield `| ${type}${newLine}`;
}
}
else {
yield `var $el!: any${endOfLine}`;
yield `any`;
}
yield endOfLine;
}

function* generatePreResolveComponents(options: TemplateCodegenOptions): Generator<Code> {
39 changes: 34 additions & 5 deletions packages/language-core/lib/codegen/template/templateChild.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as CompilerDOM from '@vue/compiler-dom';
import type { Code } from '../../types';
import { endOfLine, newLine } from '../common';
import { hyphenateTag } from '../../utils/shared';
import type { TemplateCodegenContext } from './context';
import { generateComponent, generateElement } from './element';
import type { TemplateCodegenOptions } from './index';
@@ -47,18 +48,16 @@ export function* generateTemplateChild(
}
}

const shouldInheritRootNodeAttrs = options.inheritAttrs;

const cur = node as CompilerDOM.ElementNode | CompilerDOM.IfNode | CompilerDOM.ForNode;
if (cur.codegenNode?.type === CompilerDOM.NodeTypes.JS_CACHE_EXPRESSION) {
cur.codegenNode = cur.codegenNode.value as any;
}

if (node.type === CompilerDOM.NodeTypes.ROOT) {
let prev: CompilerDOM.TemplateChildNode | undefined;
if (shouldInheritRootNodeAttrs && node.children.length === 1 && node.children[0].type === CompilerDOM.NodeTypes.ELEMENT) {
ctx.singleRootNode = node.children[0];
if (options.inheritAttrs) {
ctx.singleRootNodes = new Set(collectSingleRootNodes(options, node.children));
}
let prev: CompilerDOM.TemplateChildNode | undefined;
for (const childNode of node.children) {
yield* generateTemplateChild(options, ctx, childNode, currentComponent, prev, componentCtxVar);
prev = childNode;
@@ -129,6 +128,36 @@ export function* generateTemplateChild(
}
}

function* collectSingleRootNodes(
options: TemplateCodegenOptions,
children: CompilerDOM.TemplateChildNode[]
): Generator<CompilerDOM.ElementNode | null> {
if (children.length !== 1) {
// used to determine whether the component is always has a single root
if (children.length > 1) {
yield null;
}
return;
}

const child = children[0];
if (child.type === CompilerDOM.NodeTypes.IF) {
for (const branch of child.branches) {
yield* collectSingleRootNodes(options, branch.children);
}
return;
}
else if (child.type !== CompilerDOM.NodeTypes.ELEMENT) {
return;
}
yield child;

const tag = hyphenateTag(child.tag);
if (options.vueCompilerOptions.fallthroughComponentTags.includes(tag)) {
yield* collectSingleRootNodes(options, child.children);
}
}

// TODO: track https://github.com/vuejs/vue-next/issues/3498
export function getVForNode(node: CompilerDOM.ElementNode) {
const forDirective = node.props.find(
1 change: 1 addition & 0 deletions packages/language-core/lib/types.ts
Original file line number Diff line number Diff line change
@@ -31,6 +31,7 @@ export interface VueCompilerOptions {
strictTemplates: boolean;
skipTemplateCodegen: boolean;
fallthroughAttributes: boolean;
fallthroughComponentTags: string[];
dataAttributes: string[];
htmlAttributes: string[];
optionsWrapper: [string, string] | [];
7 changes: 7 additions & 0 deletions packages/language-core/lib/utils/ts.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import { posix as path } from 'path-browserify';
import type { RawVueCompilerOptions, VueCompilerOptions, VueLanguagePlugin } from '../types';
import { getAllExtensions } from '../languagePlugin';
import { generateGlobalTypes } from '../codegen/globalTypes';
import { hyphenateTag } from './shared';

export type ParsedCommandLine = ts.ParsedCommandLine & {
vueOptions: VueCompilerOptions;
@@ -234,6 +235,12 @@ export function resolveVueCompilerOptions(vueOptions: Partial<VueCompilerOptions
strictTemplates: vueOptions.strictTemplates ?? false,
skipTemplateCodegen: vueOptions.skipTemplateCodegen ?? false,
fallthroughAttributes: vueOptions.fallthroughAttributes ?? false,
fallthroughComponentTags: (vueOptions.fallthroughComponentTags ?? [
'Transition',
'KeepAlive',
'Teleport',
'Suspense'
]).map(tag => hyphenateTag(tag)),
dataAttributes: vueOptions.dataAttributes ?? [],
htmlAttributes: vueOptions.htmlAttributes ?? ['aria-*'],
optionsWrapper: vueOptions.optionsWrapper ?? (
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<script setup lang="ts">
import Child from './child.vue';
</script>

<template>
<template v-if="true">
<Child />
</template>
<template v-else-if="true">
<transition>
<a></a>
</transition>
</template>
<template v-else>
<img />
</template>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script setup lang="ts">
defineProps<{
foo?: string;
}>();
</script>

<template>
<div></div>
</template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script setup lang="ts">
import basic from './basic.vue';
</script>

<template>
<basic foo="1" mode="default" href="href" src="src" />
</template>

Unchanged files with check annotations Beta

test(`TypeScript - Stable`, () => {
expect(
getTscOutput('stable')
).toMatchInlineSnapshot(`

Check failure on line 10 in packages/tsc/tests/typecheck.spec.ts

GitHub Actions / build (18, macos-latest)

packages/tsc/tests/typecheck.spec.ts > vue-tsc > TypeScript - Stable

Error: Snapshot `vue-tsc > TypeScript - Stable 1` mismatched - Expected + Received @@ -4,6 +4,7 @@ "test-workspace/tsc/failureFixtures/#3632/script.vue(3,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/scriptSetup.vue(3,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/directives/main.vue(4,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins<ToResolvedProps<{}, {}>, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", "test-workspace/tsc/failureFixtures/directives/main.vue(9,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins<ToResolvedProps<{}, {}>, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", "test-workspace/tsc/failureFixtures/directives/main.vue(12,2): error TS2578: Unused '@ts-expect-error' directive.", + "test-workspace/tsc/passedFixtures/fallthroughAttributes_strictTemplate/fallthroughComponentTag/main.vue(6,9): error TS2353: Object literal may only specify known properties, and 'foo' does not exist in type '{} & VNodeProps & AllowedComponentProps & ComponentCustomProps'.", ] ❯ packages/tsc/tests/typecheck.spec.ts:10:5

Check failure on line 10 in packages/tsc/tests/typecheck.spec.ts

GitHub Actions / build (18, ubuntu-latest)

packages/tsc/tests/typecheck.spec.ts > vue-tsc > TypeScript - Stable

Error: Snapshot `vue-tsc > TypeScript - Stable 1` mismatched - Expected + Received @@ -4,6 +4,7 @@ "test-workspace/tsc/failureFixtures/#3632/script.vue(3,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/#3632/scriptSetup.vue(3,1): error TS1109: Expression expected.", "test-workspace/tsc/failureFixtures/directives/main.vue(4,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins<ToResolvedProps<{}, {}>, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", "test-workspace/tsc/failureFixtures/directives/main.vue(9,6): error TS2339: Property 'notExist' does not exist on type 'CreateComponentPublicInstanceWithMixins<ToResolvedProps<{}, {}>, { exist: typeof exist; }, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, ... 18 more ..., {}>'.", "test-workspace/tsc/failureFixtures/directives/main.vue(12,2): error TS2578: Unused '@ts-expect-error' directive.", + "test-workspace/tsc/passedFixtures/fallthroughAttributes_strictTemplate/fallthroughComponentTag/main.vue(6,9): error TS2353: Object literal may only specify known properties, and 'foo' does not exist in type '{} & VNodeProps & AllowedComponentProps & ComponentCustomProps'.", ] ❯ packages/tsc/tests/typecheck.spec.ts:10:5
[
"test-workspace/tsc/failureFixtures/#3632/both.vue(3,1): error TS1109: Expression expected.",
"test-workspace/tsc/failureFixtures/#3632/both.vue(7,1): error TS1109: Expression expected.",