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

refactor: direct access to script setup variables #4344

Closed
wants to merge 20 commits into from
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"private": true,
"packageManager": "pnpm@9.1.0",
"scripts": {
"build": "tsc -b",
"watch": "npm run build && (npm run watch:base & npm run watch:vue)",
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/lib/codegen/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type * as ts from 'typescript';
import { getNodeText } from '../parsers/scriptSetupRanges';
import { getNodeText } from '../utils/parseBindings';
import type { Code, SfcBlock, VueCodeInformation } from '../types';

export const newLine = '\n';
Expand Down
5 changes: 3 additions & 2 deletions packages/language-core/lib/codegen/script/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,9 +108,10 @@ export function createScriptCodegenContext(options: ScriptCodegenOptions) {
generatedPropsType: false,
scriptSetupGeneratedOffset: undefined as number | undefined,
bypassDefineComponent: options.lang === 'js' || options.lang === 'jsx',
// TODO: maybe not using substring to get names?
bindingNames: new Set([
...options.scriptRanges?.bindings.map(range => options.sfc.script!.content.substring(range.start, range.end)) ?? [],
...options.scriptSetupRanges?.bindings.map(range => options.sfc.scriptSetup!.content.substring(range.start, range.end)) ?? [],
...options.scriptRanges?.bindings.bindingRanges.map(range => options.sfc.script!.content.substring(range.start, range.end)) ?? [],
...options.scriptSetupRanges?.bindings.bindingRanges.map(range => options.sfc.scriptSetup!.content.substring(range.start, range.end)) ?? [],
]),
helperTypes,
generateHelperTypes,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ export function* generateInternalComponent(
// bindings
const templateUsageVars = getTemplateUsageVars(options, ctx);
for (const [content, bindings] of [
[options.sfc.scriptSetup.content, options.scriptSetupRanges.bindings] as const,
[options.sfc.scriptSetup.content, options.scriptSetupRanges.bindings.bindingRanges] as const,
options.sfc.script && options.scriptRanges
? [options.sfc.script.content, options.scriptRanges.bindings] as const
? [options.sfc.script.content, options.scriptRanges.bindings.bindingRanges] as const
: ['', []] as const,
]) {
for (const expose of bindings) {
Expand Down
8 changes: 8 additions & 0 deletions packages/language-core/lib/codegen/template/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type * as CompilerDOM from '@vue/compiler-dom';
import type { Code, VueCodeInformation } from '../../types';
import type { BindingTypes } from '../../utils/parseBindings';
import { endOfLine, newLine, wrapWith } from '../common';

const _codeFeatures = {
Expand Down Expand Up @@ -96,6 +97,7 @@ export function createTemplateCodegenContext() {
const blockConditions: string[] = [];
const usedComponentCtxVars = new Set<string>();
const scopedClasses: { className: string, offset: number; }[] = [];
let bindingTypes: Map<string, BindingTypes> | undefined;

return {
slots,
Expand All @@ -106,6 +108,9 @@ export function createTemplateCodegenContext() {
blockConditions,
usedComponentCtxVars,
scopedClasses,
get bindingTypes() {
return bindingTypes;
},
accessGlobalVariable(name: string, offset?: number) {
let arr = accessGlobalVariables.get(name);
if (!arr) {
Expand All @@ -127,6 +132,9 @@ export function createTemplateCodegenContext() {
getInternalVariable: () => {
return `__VLS_${variableId++}`;
},
setBindingTypes: (types: Map<string, BindingTypes>) => {
bindingTypes = types;
},
ignoreError: function* (): Generator<Code> {
if (!ignoredError) {
ignoredError = true;
Expand Down
8 changes: 7 additions & 1 deletion packages/language-core/lib/codegen/template/element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import * as CompilerDOM from '@vue/compiler-dom';
import { camelize, capitalize } from '@vue/shared';
import type { Code, VueCodeInformation } from '../../types';
import { hyphenateTag } from '../../utils/shared';
import { BindingTypes } from '../../utils/parseBindings';
import { collectVars, createTsAst, endOfLine, newLine, variableNameRegex, wrapWith } from '../common';
import { generateCamelized } from './camelized';
import type { TemplateCodegenContext } from './context';
Expand Down Expand Up @@ -30,7 +31,12 @@ export function* generateComponent(
: [startTagOffset];
const propsFailedExps: CompilerDOM.SimpleExpressionNode[] = [];
const possibleOriginalNames = getPossibleOriginalComponentNames(node.tag, true);
const matchImportName = possibleOriginalNames.find(name => options.scriptSetupImportComponentNames.has(name));
const matchImportName = possibleOriginalNames.find(name => {
const bindingType = ctx.bindingTypes?.get(name);
if (bindingType) {
return bindingType & BindingTypes.Component;
}
});
const var_originalComponent = matchImportName ?? ctx.getInternalVariable();
const var_functionalComponent = ctx.getInternalVariable();
const var_componentInstance = ctx.getInternalVariable();
Expand Down
6 changes: 5 additions & 1 deletion packages/language-core/lib/codegen/template/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as CompilerDOM from '@vue/compiler-dom';
import type * as ts from 'typescript';
import type { Code, Sfc, VueCompilerOptions } from '../../types';
import type { BindingTypes } from '../../utils/parseBindings';
import { endOfLine, newLine, wrapWith } from '../common';
import { createTemplateCodegenContext } from './context';
import { getCanonicalComponentName, getPossibleOriginalComponentNames } from './element';
Expand All @@ -15,14 +16,17 @@ export interface TemplateCodegenOptions {
template: NonNullable<Sfc['template']>;
shouldGenerateScopedClasses?: boolean;
stylesScopedClasses: Set<string>;
scriptSetupImportComponentNames: Set<string>;
hasDefineSlots?: boolean;
slotsAssignName?: string;
propsAssignName?: string;
bindingTypes?: Map<string, BindingTypes>;
}

export function* generateTemplate(options: TemplateCodegenOptions) {
const ctx = createTemplateCodegenContext();
if (options.bindingTypes) {
ctx.setBindingTypes(options.bindingTypes);
};

let hasSlot = false;

Expand Down
9 changes: 5 additions & 4 deletions packages/language-core/lib/codegen/template/interpolation.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { isGloballyWhitelisted } from '@vue/shared';
import { isGloballyAllowed } from '@vue/shared';
import type * as ts from 'typescript';
import { getNodeText, getStartEnd } from '../../parsers/scriptSetupRanges';
import { BindingTypes, getNodeText, getStartEnd } from '../../utils/parseBindings';
import type { Code, VueCodeInformation, VueCompilerOptions } from '../../types';
import { collectVars, createTsAst } from '../common';
import type { TemplateCodegenContext } from './context';
Expand Down Expand Up @@ -89,9 +89,10 @@ export function* forEachInterpolationSegment(
if (
ctx.hasLocalVariable(text) ||
// https://github.com/vuejs/core/blob/245230e135152900189f13a4281302de45fdcfaa/packages/compiler-core/src/transforms/transformExpression.ts#L342-L352
isGloballyWhitelisted(text) ||
isGloballyAllowed(text) ||
text === 'require' ||
text.startsWith('__VLS_')
text.startsWith('__VLS_') ||
((ctx.bindingTypes?.get(text) ?? 0) & BindingTypes.NoUnref)
) {
// localVarOffsets.push(localVar.getStart(ast));
}
Expand Down
16 changes: 8 additions & 8 deletions packages/language-core/lib/parsers/scriptRanges.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import type { TextRange } from '../types';
import type * as ts from 'typescript';
import { getNodeText, getStartEnd, parseBindingRanges } from './scriptSetupRanges';
import { BindingTypes, getNodeText, getStartEnd, parseBindings } from '../utils/parseBindings';
import { injectTscApiShim } from '../utils/tscApiShim';

export interface ScriptRanges extends ReturnType<typeof parseScriptRanges> { }

export function parseScriptRanges(ts: typeof import('typescript'), ast: ts.SourceFile, hasScriptSetup: boolean, withNode: boolean) {

ts = injectTscApiShim(ts);

let exportDefault: (TextRange & {
expression: TextRange,
args: TextRange,
Expand All @@ -15,14 +18,16 @@ export function parseScriptRanges(ts: typeof import('typescript'), ast: ts.Sourc
nameOption: TextRange | undefined,
}) | undefined;

const bindings = hasScriptSetup ? parseBindingRanges(ts, ast) : [];
const bindings = hasScriptSetup
? parseBindings(ts, ast)
: { bindingRanges: [], bindingTypes: new Map<string, BindingTypes>() };

ts.forEachChild(ast, raw => {

if (ts.isExportAssignment(raw)) {

let node: ts.AsExpression | ts.ExportAssignment | ts.ParenthesizedExpression = raw;
while (isAsExpression(node.expression) || ts.isParenthesizedExpression(node.expression)) { // fix https://github.com/vuejs/language-tools/issues/1882
while (ts.isAsExpression(node.expression) || ts.isParenthesizedExpression(node.expression)) { // fix https://github.com/vuejs/language-tools/issues/1882
node = node.expression;
}

Expand Down Expand Up @@ -71,9 +76,4 @@ export function parseScriptRanges(ts: typeof import('typescript'), ast: ts.Sourc
function _getStartEnd(node: ts.Node) {
return getStartEnd(ts, node, ast);
}

// isAsExpression is missing in tsc
function isAsExpression(node: ts.Node): node is ts.AsExpression {
return node.kind === ts.SyntaxKind.AsExpression;
}
}
121 changes: 2 additions & 119 deletions packages/language-core/lib/parsers/scriptSetupRanges.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type * as ts from 'typescript';
import type { VueCompilerOptions, TextRange } from '../types';
import { getNodeText, getStartEnd, parseBindings } from '../utils/parseBindings';

export interface ScriptSetupRanges extends ReturnType<typeof parseScriptSetupRanges> { }

Expand Down Expand Up @@ -46,10 +47,9 @@ export function parseScriptSetupRanges(
required: boolean;
isModel?: boolean;
}[] = [];
const bindings = parseBindingRanges(ts, ast);
const bindings = parseBindings(ts, ast, vueCompilerOptions);
const text = ast.text;
const leadingCommentEndOffset = ts.getLeadingCommentRanges(text, 0)?.reverse()[0].end ?? 0;
const importComponentNames = new Set<string>();

ts.forEachChild(ast, node => {
const isTypeExport = (ts.isTypeAliasDeclaration(node) || ts.isInterfaceDeclaration(node)) && node.modifiers?.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword);
Expand All @@ -71,25 +71,13 @@ export function parseScriptSetupRanges(
}
foundNonImportExportNode = true;
}

if (
ts.isImportDeclaration(node)
&& node.importClause?.name
&& !node.importClause.isTypeOnly
) {
const moduleName = getNodeText(ts, node.moduleSpecifier, ast).slice(1, -1);
if (vueCompilerOptions.extensions.some(ext => moduleName.endsWith(ext))) {
importComponentNames.add(getNodeText(ts, node.importClause.name, ast));
}
}
});
ts.forEachChild(ast, child => visitNode(child, [ast]));

return {
leadingCommentEndOffset,
importSectionEndOffset,
bindings,
importComponentNames,
props,
slots,
emits,
Expand Down Expand Up @@ -267,108 +255,3 @@ export function parseScriptSetupRanges(
});
}
}

export function parseBindingRanges(ts: typeof import('typescript'), sourceFile: ts.SourceFile) {
const bindings: TextRange[] = [];
ts.forEachChild(sourceFile, node => {
if (ts.isVariableStatement(node)) {
for (const node_2 of node.declarationList.declarations) {
const vars = _findBindingVars(node_2.name);
for (const _var of vars) {
bindings.push(_var);
}
}
}
else if (ts.isFunctionDeclaration(node)) {
if (node.name && ts.isIdentifier(node.name)) {
bindings.push(_getStartEnd(node.name));
}
}
else if (ts.isClassDeclaration(node)) {
if (node.name) {
bindings.push(_getStartEnd(node.name));
}
}
else if (ts.isEnumDeclaration(node)) {
bindings.push(_getStartEnd(node.name));
}

if (ts.isImportDeclaration(node)) {
if (node.importClause && !node.importClause.isTypeOnly) {
if (node.importClause.name) {
bindings.push(_getStartEnd(node.importClause.name));
}
if (node.importClause.namedBindings) {
if (ts.isNamedImports(node.importClause.namedBindings)) {
for (const element of node.importClause.namedBindings.elements) {
bindings.push(_getStartEnd(element.name));
}
}
else if (ts.isNamespaceImport(node.importClause.namedBindings)) {
bindings.push(_getStartEnd(node.importClause.namedBindings.name));
}
}
}
}
});
return bindings;
function _getStartEnd(node: ts.Node) {
return getStartEnd(ts, node, sourceFile);
}
function _findBindingVars(left: ts.BindingName) {
return findBindingVars(ts, left, sourceFile);
}
}

export function findBindingVars(ts: typeof import('typescript'), left: ts.BindingName, sourceFile: ts.SourceFile) {
const vars: TextRange[] = [];
worker(left);
return vars;
function worker(_node: ts.Node) {
if (ts.isIdentifier(_node)) {
vars.push(getStartEnd(ts, _node, sourceFile));
}
// { ? } = ...
// [ ? ] = ...
else if (ts.isObjectBindingPattern(_node) || ts.isArrayBindingPattern(_node)) {
for (const property of _node.elements) {
if (ts.isBindingElement(property)) {
worker(property.name);
}
}
}
// { foo: ? } = ...
else if (ts.isPropertyAssignment(_node)) {
worker(_node.initializer);
}
// { foo } = ...
else if (ts.isShorthandPropertyAssignment(_node)) {
vars.push(getStartEnd(ts, _node.name, sourceFile));
}
// { ...? } = ...
// [ ...? ] = ...
else if (ts.isSpreadAssignment(_node) || ts.isSpreadElement(_node)) {
worker(_node.expression);
}
}
}

export function getStartEnd(
ts: typeof import('typescript'),
node: ts.Node,
sourceFile: ts.SourceFile
) {
return {
start: (ts as any).getTokenPosOfNode(node, sourceFile) as number,
end: node.end,
};
}

export function getNodeText(
ts: typeof import('typescript'),
node: ts.Node,
sourceFile: ts.SourceFile
) {
const { start, end } = getStartEnd(ts, node, sourceFile);
return sourceFile.text.substring(start, end);
}
22 changes: 2 additions & 20 deletions packages/language-core/lib/plugins/vue-tsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,10 +119,10 @@ function createTsx(
template: _sfc.template,
shouldGenerateScopedClasses: shouldGenerateScopedClasses(),
stylesScopedClasses: stylesScopedClasses(),
scriptSetupImportComponentNames: scriptSetupImportComponentNames(),
hasDefineSlots: hasDefineSlots(),
slotsAssignName: slotsAssignName(),
propsAssignName: propsAssignName(),
bindingTypes: bindingTypes(),
});

let current = codegen.next();
Expand All @@ -139,15 +139,9 @@ function createTsx(
};
});
const hasDefineSlots = computed(() => !!scriptSetupRanges()?.slots.define);
const scriptSetupImportComponentNames = computed<Set<string>>(oldNames => {
const newNames = scriptSetupRanges()?.importComponentNames ?? new Set();
if (newNames && oldNames && twoSetsEqual(newNames, oldNames)) {
return oldNames;
}
return newNames;
});
const slotsAssignName = computed(() => scriptSetupRanges()?.slots.name);
const propsAssignName = computed(() => scriptSetupRanges()?.props.name);
const bindingTypes = computed(() => scriptSetupRanges()?.bindings.bindingTypes ?? scriptRanges()?.bindings.bindingTypes);
const generatedScript = computed(() => {
const codes: Code[] = [];
const linkedCodeMappings: Mapping[] = [];
Expand Down Expand Up @@ -190,15 +184,3 @@ function createTsx(
generatedTemplate,
};
}

function twoSetsEqual(a: Set<string>, b: Set<string>) {
if (a.size !== b.size) {
return false;
}
for (const file of a) {
if (!b.has(file)) {
return false;
}
}
return true;
}