-
-
Notifications
You must be signed in to change notification settings - Fork 418
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor(language-core): improve maintainability of codegen (#4276)
- Loading branch information
1 parent
490268a
commit f46634c
Showing
74 changed files
with
3,929 additions
and
3,662 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
]; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.