Skip to content

Commit

Permalink
refactor(language-core): improve maintainability of codegen (#4276)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnsoncodehk committed Apr 21, 2024
1 parent 490268a commit f46634c
Show file tree
Hide file tree
Showing 74 changed files with 3,929 additions and 3,662 deletions.
1 change: 0 additions & 1 deletion extensions/vscode/src/nodeClientMain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { middleware } from './middleware';
export async function activate(context: vscode.ExtensionContext) {

const volarLabs = createLabsInfo(serverLib);
volarLabs.extensionExports.volarLabs.codegenStackSupport = true;

await commonActivate(context, (
id,
Expand Down
4 changes: 2 additions & 2 deletions packages/component-meta/lib/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,7 +314,7 @@ ${vueCompilerOptions.target < 3 ? vue2TypeHelpersCode : typeHelpersCode}

const vueFile = language.scripts.get(componentPath)?.generated?.root;
const vueDefaults = vueFile && exportName === 'default'
? (vueFile instanceof vue.VueGeneratedCode ? readVueComponentDefaultProps(vueFile, printer, ts, vueCompilerOptions) : {})
? (vueFile instanceof vue.VueVirtualCode ? readVueComponentDefaultProps(vueFile, printer, ts, vueCompilerOptions) : {})
: {};
const tsDefaults = !vueFile ? readTsComponentDefaultProps(
componentPath.substring(componentPath.lastIndexOf('.') + 1), // ts | js | tsx | jsx
Expand Down Expand Up @@ -684,7 +684,7 @@ function createSchemaResolvers(
}

function readVueComponentDefaultProps(
vueSourceFile: vue.VueGeneratedCode,
vueSourceFile: vue.VueVirtualCode,
printer: ts.Printer | undefined,
ts: typeof import('typescript'),
vueCompilerOptions: vue.VueCompilerOptions,
Expand Down
2 changes: 1 addition & 1 deletion packages/language-core/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './lib/generators/template';
export * from './lib/codegen/template';
export * from './lib/languageModule';
export * from './lib/parsers/scriptSetupRanges';
export * from './lib/plugins';
Expand Down
84 changes: 84 additions & 0 deletions packages/language-core/lib/codegen/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type * as ts from 'typescript';
import { getNodeText } from '../parsers/scriptSetupRanges';
import type { Code, SfcBlock, VueCodeInformation } from '../types';

export const newLine = '\n';
export const endOfLine = `;${newLine}`;
export const combineLastMapping: VueCodeInformation = { __combineLastMapping: true };
export const variableNameRegex = /^[a-zA-Z_$][0-9a-zA-Z_$]*$/;

export function* conditionWrapWith(
condition: boolean,
startOffset: number,
endOffset: number,
features: VueCodeInformation,
...wrapCodes: Code[]
): Generator<Code> {
if (condition) {
yield* wrapWith(startOffset, endOffset, features, ...wrapCodes);
}
else {
for (const wrapCode of wrapCodes) {
yield wrapCode;
}
}
}

export function* wrapWith(
startOffset: number,
endOffset: number,
features: VueCodeInformation,
...wrapCodes: Code[]
): Generator<Code> {
yield ['', 'template', startOffset, features];
let offset = 1;
for (const wrapCode of wrapCodes) {
if (typeof wrapCode !== 'string') {
offset++;
}
yield wrapCode;
}
yield ['', 'template', endOffset, { __combineOffsetMapping: offset }];
}

export function collectVars(
ts: typeof import('typescript'),
node: ts.Node,
ast: ts.SourceFile,
result: string[],
) {
if (ts.isIdentifier(node)) {
result.push(getNodeText(ts, node, ast));
}
else if (ts.isObjectBindingPattern(node)) {
for (const el of node.elements) {
collectVars(ts, el.name, ast, result);
}
}
else if (ts.isArrayBindingPattern(node)) {
for (const el of node.elements) {
if (ts.isBindingElement(el)) {
collectVars(ts, el.name, ast, result);
}
}
}
else {
ts.forEachChild(node, node => collectVars(ts, node, ast, result));
}
}
export function createTsAst(ts: typeof import('typescript'), astHolder: any, text: string) {
if (astHolder.__volar_ast_text !== text) {
astHolder.__volar_ast_text = text;
astHolder.__volar_ast = ts.createSourceFile('/a.ts', text, 99 satisfies ts.ScriptTarget.ESNext);
}
return astHolder.__volar_ast as ts.SourceFile;
}

export function generateSfcBlockSection(block: SfcBlock, start: number, end: number, features: VueCodeInformation): Code {
return [
block.content.substring(start, end),
block.name,
start,
features,
];
}
116 changes: 116 additions & 0 deletions packages/language-core/lib/codegen/script/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { ScriptRanges } from '../../parsers/scriptRanges';
import type { ScriptSetupRanges } from '../../parsers/scriptSetupRanges';
import type { Code, Sfc } from '../../types';
import { endOfLine, generateSfcBlockSection, newLine } from '../common';
import type { ScriptCodegenContext } from './context';
import { ScriptCodegenOptions, codeFeatures } from './index';

export function* generateComponent(
options: ScriptCodegenOptions,
ctx: ScriptCodegenContext,
scriptSetup: NonNullable<Sfc['scriptSetup']>,
scriptSetupRanges: ScriptSetupRanges,
): Generator<Code> {
if (options.sfc.script && options.scriptRanges?.exportDefault && options.scriptRanges.exportDefault.expression.start !== options.scriptRanges.exportDefault.args.start) {
// use defineComponent() from user space code if it exist
yield generateSfcBlockSection(options.sfc.script, options.scriptRanges.exportDefault.expression.start, options.scriptRanges.exportDefault.args.start, codeFeatures.all);
yield `{${newLine}`;
}
else {
yield `(await import('${options.vueCompilerOptions.lib}')).defineComponent({${newLine}`;
}

yield `setup() {${newLine}`;
yield `return {${newLine}`;
if (ctx.bypassDefineComponent) {
yield* generateComponentSetupReturns(scriptSetupRanges);
}
if (scriptSetupRanges.expose.define) {
yield `...__VLS_exposed,${newLine}`;
}
yield `}${endOfLine}`;
yield `},${newLine}`;
if (!ctx.bypassDefineComponent) {
yield* generateScriptSetupOptions(ctx, scriptSetup, scriptSetupRanges);
}
if (options.sfc.script && options.scriptRanges) {
yield* generateScriptOptions(options.sfc.script, options.scriptRanges);
}
yield `})`;
}

export function* generateComponentSetupReturns(scriptSetupRanges: ScriptSetupRanges): Generator<Code> {
// fill $props
if (scriptSetupRanges.props.define) {
// NOTE: defineProps is inaccurate for $props
yield `$props: __VLS_makeOptional(${scriptSetupRanges.props.name ?? `__VLS_props`}),${newLine}`;
yield `...${scriptSetupRanges.props.name ?? `__VLS_props`},${newLine}`;
}
// fill $emit
if (scriptSetupRanges.emits.define) {
yield `$emit: ${scriptSetupRanges.emits.name ?? '__VLS_emit'},${newLine}`;
}
}

export function* generateScriptOptions(
script: NonNullable<Sfc['script']>,
scriptRanges: ScriptRanges,
): Generator<Code> {
if (scriptRanges.exportDefault?.args) {
yield generateSfcBlockSection(script, scriptRanges.exportDefault.args.start + 1, scriptRanges.exportDefault.args.end - 1, codeFeatures.all);
}
}

export function* generateScriptSetupOptions(
ctx: ScriptCodegenContext,
scriptSetup: NonNullable<Sfc['scriptSetup']>,
scriptSetupRanges: ScriptSetupRanges,
): Generator<Code> {
const propsCodegens: (() => Generator<Code>)[] = [];

if (ctx.generatedPropsType) {
propsCodegens.push(function* () {
yield `{} as `;
if (scriptSetupRanges.props.withDefaults?.arg) {
yield `${ctx.helperTypes.WithDefaults.name}<`;
}
yield `${ctx.helperTypes.TypePropsToOption.name}<`;
yield `typeof __VLS_componentProps`;
yield `>`;
if (scriptSetupRanges.props.withDefaults?.arg) {
yield `, typeof __VLS_withDefaultsArg>`;
}
});
}
if (scriptSetupRanges.props.define?.arg) {
const { arg } = scriptSetupRanges.props.define;
propsCodegens.push(function* () {
yield generateSfcBlockSection(scriptSetup, arg.start, arg.end, codeFeatures.navigation);
});
}

if (propsCodegens.length === 1) {
yield `props: `;
for (const generate of propsCodegens) {
yield* generate();
}
yield `,${newLine}`;
}
else if (propsCodegens.length >= 2) {
yield `props: {${newLine}`;
for (const generate of propsCodegens) {
yield `...`;
yield* generate();
yield `,${newLine}`;
}
yield `},${newLine}`;
}
if (scriptSetupRanges.defineProp.filter(p => p.isModel).length || scriptSetupRanges.emits.define) {
yield `emits: ({} as __VLS_NormalizeEmits<typeof __VLS_modelEmitsType`;
if (scriptSetupRanges.emits.define) {
yield ` & typeof `;
yield scriptSetupRanges.emits.name ?? '__VLS_emit';
}
yield `>),${newLine}`;
}
}
132 changes: 132 additions & 0 deletions packages/language-core/lib/codegen/script/context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { getSlotsPropertyName } from '../../utils/shared';
import { newLine } from '../common';
import type { ScriptCodegenOptions } from './index';

interface HelperType {
name: string;
used?: boolean;
generated?: boolean;
code: string;
}

export type ScriptCodegenContext = ReturnType<typeof createScriptCodegenContext>;

export function createScriptCodegenContext(options: ScriptCodegenOptions) {
const helperTypes = {
OmitKeepDiscriminatedUnion: {
get name() {
this.used = true;
return `__VLS_OmitKeepDiscriminatedUnion`;
},
get code() {
return `type __VLS_OmitKeepDiscriminatedUnion<T, K extends keyof any> = T extends any
? Pick<T, Exclude<keyof T, K>>
: never;`;
},
} satisfies HelperType as HelperType,
WithDefaults: {
get name() {
this.used = true;
return `__VLS_WithDefaults`;
},
get code(): string {
return `type __VLS_WithDefaults<P, D> = {
[K in keyof Pick<P, keyof P>]: K extends keyof D
? ${helperTypes.Prettify.name}<P[K] & { default: D[K]}>
: P[K]
};`;
},
} satisfies HelperType as HelperType,
Prettify: {
get name() {
this.used = true;
return `__VLS_Prettify`;
},
get code() {
return `type __VLS_Prettify<T> = { [K in keyof T]: T[K]; } & {};`;
},
} satisfies HelperType as HelperType,
WithTemplateSlots: {
get name() {
this.used = true;
return `__VLS_WithTemplateSlots`;
},
get code(): string {
return `type __VLS_WithTemplateSlots<T, S> = T & {
new(): {
${getSlotsPropertyName(options.vueCompilerOptions.target)}: S;
${options.vueCompilerOptions.jsxSlots ? `$props: ${helperTypes.PropsChildren.name}<S>;` : ''}
}
};`;
},
} satisfies HelperType as HelperType,
PropsChildren: {
get name() {
this.used = true;
return `__VLS_PropsChildren`;
},
get code() {
return `type __VLS_PropsChildren<S> = {
[K in keyof (
boolean extends (
// @ts-ignore
JSX.ElementChildrenAttribute extends never
? true
: false
)
? never
// @ts-ignore
: JSX.ElementChildrenAttribute
)]?: S;
};`;
},
} satisfies HelperType as HelperType,
TypePropsToOption: {
get name() {
this.used = true;
return `__VLS_TypePropsToOption`;
},
get code() {
return options.compilerOptions.exactOptionalPropertyTypes ?
`type __VLS_TypePropsToOption<T> = {
[K in keyof T]-?: {} extends Pick<T, K>
? { type: import('${options.vueCompilerOptions.lib}').PropType<T[K]> }
: { type: import('${options.vueCompilerOptions.lib}').PropType<T[K]>, required: true }
};` :
`type __VLS_NonUndefinedable<T> = T extends undefined ? never : T;
type __VLS_TypePropsToOption<T> = {
[K in keyof T]-?: {} extends Pick<T, K>
? { type: import('${options.vueCompilerOptions.lib}').PropType<__VLS_NonUndefinedable<T[K]>> }
: { type: import('${options.vueCompilerOptions.lib}').PropType<T[K]>, required: true }
};`;
},
} satisfies HelperType as HelperType,
};

return {
generatedTemplate: false,
generatedPropsType: false,
scriptSetupGeneratedOffset: undefined as number | undefined,
bypassDefineComponent: options.lang === 'js' || options.lang === 'jsx',
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)) ?? [],
]),
helperTypes,
generateHelperTypes,
};

function* generateHelperTypes() {
let shouldCheck = true;
while (shouldCheck) {
shouldCheck = false;
for (const helperType of Object.values(helperTypes)) {
if (helperType.used && !helperType.generated) {
shouldCheck = true;
helperType.generated = true;
yield newLine + helperType.code + newLine;
}
}
}
}
}

0 comments on commit f46634c

Please sign in to comment.