diff --git a/.vscode/settings.json b/.vscode/settings.json index dbc7e4ae..6b4719f5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "onwarn", "textdocument", "treeshake", + "tsserverlibrary", "typecheck", "ucfirst", "Uncapitalize", diff --git a/extensions/vscode-vue-language-features/src/scheme/vue.ts b/extensions/vscode-vue-language-features/src/scheme/vue.ts index de07821a..55dc5064 100644 --- a/extensions/vscode-vue-language-features/src/scheme/vue.ts +++ b/extensions/vscode-vue-language-features/src/scheme/vue.ts @@ -1,6 +1,7 @@ -import { isVueFile, parseFileName } from '@vuedx/shared' +import { first, isNotNull, parseFileName } from '@vuedx/shared' +import { TextSpan } from '@vuedx/vue-virtual-textdocument' import { inject, injectable } from 'inversify' -import vscode from 'vscode' +import vscode, { TextEditor } from 'vscode' import { PluginCommunicationService } from '../services/PluginCommunicationService' import { Installable } from '../utils/installable' import { getVirtualFileNameFromUri } from '../utils/uri' @@ -18,17 +19,20 @@ export class VueVirtualDocumentProvider } private readonly openVueFiles = new Map>() + private readonly decoration = vscode.window.createTextEditorDecorationType({ + outline: 'solid red 1px', + }) + + private editors: readonly TextEditor[] = [] public install(): vscode.Disposable { super.install() - let selectionWatcher: vscode.Disposable | undefined - let cancellationToken: vscode.CancellationTokenSource | undefined - return vscode.Disposable.from( vscode.workspace.registerTextDocumentContentProvider('vue', this), - vscode.workspace.onDidChangeTextDocument(({ document }) => { + vscode.workspace.onDidChangeTextDocument(async ({ document }) => { if (document.languageId === 'vue') { + await delay(100) this.openVueFiles.get(document.fileName)?.forEach((uri) => { this.onDidChangeEmitter.fire(vscode.Uri.parse(uri)) }) @@ -56,72 +60,103 @@ export class VueVirtualDocumentProvider if (openFiles.size === 0) this.openVueFiles.delete(parsed.fileName) } }), - vscode.window.onDidChangeActiveTextEditor((editor) => { - selectionWatcher?.dispose() - if (editor == null || !isVueFile(editor.document.fileName)) return - const fileName = editor.document.fileName - - selectionWatcher = vscode.window.onDidChangeTextEditorSelection( - async ({ textEditor, selections }) => { - if (textEditor !== editor) return // ignore others - if (selections.length !== 1) return - if ( - !vscode.window.visibleTextEditors.some( - (editor) => - parseFileName(editor.document.fileName).fileName === fileName, - ) - ) { - return // No active virtual document - } - - cancellationToken?.cancel() - const current = new vscode.CancellationTokenSource() - cancellationToken = current - - const start = textEditor.document.offsetAt(editor.selection.start) - const end = textEditor.document.offsetAt(editor.selection.end) - const result = await this.plugin.first(async (_conneciton) => { - console.log(start, end) - return null as null | { - fileName: string - start: number - end: number - } // TODO: fix this - }) - - // not found or cancelled - if (result == null || current.token.isCancellationRequested) return - - const virtualEditor = vscode.window.visibleTextEditors.find( - (editor) => editor.document.fileName === result.fileName, + vscode.window.onDidChangeActiveTextEditor(() => { + this.resetAllDecorations() + }), + vscode.window.onDidChangeVisibleTextEditors((editors) => { + this.editors = editors + }), + vscode.window.onDidChangeTextEditorSelection( + async ({ textEditor, selections }) => { + if (textEditor !== vscode.window.activeTextEditor) return + if (textEditor.document.languageId === 'vue') { + const fileName = textEditor.document.fileName + const editor = this.editors.find( + (editor) => + editor.document.uri.scheme === 'vue' && + editor.document.fileName.startsWith(fileName), ) - if (virtualEditor == null) return // not active - - const range = new vscode.Range( - virtualEditor.document.positionAt(result.start), - virtualEditor.document.positionAt(result.end), + if (editor == null) return + const textSpans = await Promise.all( + selections.map( + async (selection) => + await this.plugin.first(async (connection) => { + const start = textEditor.document.offsetAt(selection.start) + const end = textEditor.document.offsetAt(selection.end) + + return await connection.findGeneratedTextSpan(fileName, { + start: Math.min(start, end), + length: Math.abs(end - start), + }) + }), + ), ) - - virtualEditor.options.cursorStyle = - vscode.TextEditorCursorStyle.Underline - virtualEditor.selection = new vscode.Selection( - range.start, - range.end, + this.setDecorations(textSpans, editor) + } else if (textEditor.document.uri.scheme === 'vue') { + const fileName = textEditor.document.fileName.replace( + /\.[tj]sx$/, + '', ) - virtualEditor.revealRange( - range, - vscode.TextEditorRevealType.Default, + const editor = this.editors.find((editor) => + editor.document.fileName.startsWith(fileName), ) - }, - ) - }), + + if (editor == null) return + const textSpans = await Promise.all( + selections.map( + async (selection) => + await this.plugin.first(async (connection) => { + const start = textEditor.document.offsetAt(selection.start) + const end = textEditor.document.offsetAt(selection.end) + + return await connection.findOriginalTextSpan(fileName, { + start: Math.min(start, end), + length: Math.abs(end - start), + }) + }), + ), + ) + this.setDecorations(textSpans, editor) + } + }, + ), ) } private readonly onDidChangeEmitter = new vscode.EventEmitter() public onDidChange = this.onDidChangeEmitter.event + private resetAllDecorations(): void { + this.editors.forEach((editor) => { + editor.setDecorations(this.decoration, []) + }) + } + + private setDecorations( + textSpans: Array, + editor: vscode.TextEditor, + ): void { + const ranges = textSpans + .filter(isNotNull) + .map( + (range) => + new vscode.Range( + editor.document.positionAt(range.start), + editor.document.positionAt(range.start + range.length), + ), + ) + + editor.setDecorations(this.decoration, []) + editor.setDecorations(this.decoration, ranges) + if (ranges.length > 0) { + editor.revealRange( + first(ranges), + vscode.TextEditorRevealType.InCenterIfOutsideViewport, + ) + } + } + async provideTextDocumentContent( request: vscode.Uri, ): Promise { diff --git a/extensions/vscode-vue-language-features/src/services/VirtualFileSwitcher.ts b/extensions/vscode-vue-language-features/src/services/VirtualFileSwitcher.ts index ff5723b2..b826efbd 100644 --- a/extensions/vscode-vue-language-features/src/services/VirtualFileSwitcher.ts +++ b/extensions/vscode-vue-language-features/src/services/VirtualFileSwitcher.ts @@ -2,9 +2,8 @@ import { isProjectRuntimeFile, isVueFile, parseFileName, - toFileName + toFileName, } from '@vuedx/shared' -import { } from '@vuedx/vue-virtual-textdocument' import { inject, injectable } from 'inversify' import { Disposable, @@ -12,7 +11,7 @@ import { StatusBarItem, TextEditor, Uri, - window + window, } from 'vscode' import { Installable } from '../utils/installable' import { getVirtualFileUri } from '../utils/uri' @@ -41,8 +40,8 @@ export class VirtualFileSwitcher extends Installable { window.onDidChangeActiveTextEditor(async (editor) => { await this.showStatusBar(editor) }), - this.plugin.onChange(() => { - void this.showStatusBar(window.activeTextEditor) + this.plugin.onChange(async () => { + await this.showStatusBar(window.activeTextEditor) }), ) } diff --git a/packages/compiler-tsx/compiler-tsx.api.md b/packages/compiler-tsx/compiler-tsx.api.md index 8c20b55c..b0b76bdc 100644 --- a/packages/compiler-tsx/compiler-tsx.api.md +++ b/packages/compiler-tsx/compiler-tsx.api.md @@ -56,8 +56,6 @@ export interface CompileOutput extends TransformedCode { errors: Array; // (undocumented) template?: RootNode; - // (undocumented) - unusedIdentifiers: string[]; } // @public (undocumented) diff --git a/packages/compiler-tsx/readme.md b/packages/compiler-tsx/readme.md index a6f3a25d..31b1bb51 100644 --- a/packages/compiler-tsx/readme.md +++ b/packages/compiler-tsx/readme.md @@ -13,27 +13,28 @@ npm add @vuedx/compiler-tsx ## API - ### [compile](#-vuedx-compiler-tsx-compile-function-1-) - -
More info **Signature:** + ```ts -export declare function compile(source: string, options: CompileOptions): Omit & { - map: RawSourceMap; -}; +export declare function compile( + source: string, + options: CompileOptions, +): Omit & { + map: RawSourceMap +} ``` -| Parameter | Type | Description | -| --- | --- | --- | -| source | string | - | -| options | CompileOptions | - | +| Parameter | Type | Description | +| --------- | --------------------------------------------------------------------------------------- | ----------- | +| source | string | - | +| options | CompileOptions | - |

@@ -42,105 +43,89 @@ export declare function compile(source: string, options: CompileOptions): Omit More info **Signature:** + ```ts -export declare function compileWithDecodedSourceMap(source: string, options: CompileOptions): CompileOutput; +export declare function compileWithDecodedSourceMap( + source: string, + options: CompileOptions, +): CompileOutput ``` -| Parameter | Type | Description | -| --- | --- | --- | -| source | string | - | -| options | CompileOptions | - | +| Parameter | Type | Description | +| --------- | --------------------------------------------------------------------------------------- | ----------- | +| source | string | - | +| options | CompileOptions | - |
## Types - ### [CompileOptions](#-vuedx-compiler-tsx-CompileOptions-interface) - - ```ts -export interface CompileOptions extends TransformOptions { -} +export interface CompileOptions extends TransformOptions {} ``` -
### [CompileOutput](#-vuedx-compiler-tsx-CompileOutput-interface) - - ```ts export interface CompileOutput extends TransformedCode { - descriptor: SFCDescriptor; - errors: Array; - template?: RootNode; - unusedIdentifiers: string[]; + descriptor: SFCDescriptor + errors: Array + template?: RootNode } ``` -
### [CustomAttributeNode](#-vuedx-compiler-tsx-CustomAttributeNode-interface) - - ```ts export interface CustomAttributeNode extends AttributeNode { - nameLoc: SourceLocation; + nameLoc: SourceLocation } ``` -
### [CustomBaseElementNode](#-vuedx-compiler-tsx-CustomBaseElementNode-interface) - - ```ts export interface CustomBaseElementNode extends BaseElementNode { - endTagLoc?: SourceLocation; - hoists?: CompoundExpressionNode[]; - startTagLoc: SourceLocation; - tagLoc: SourceLocation; + endTagLoc?: SourceLocation + hoists?: CompoundExpressionNode[] + startTagLoc: SourceLocation + tagLoc: SourceLocation } ``` -
### [CustomNode](#-vuedx-compiler-tsx-CustomNode-interface) - - ```ts export interface CustomNode extends Node { - scope: Scope; + scope: Scope } ``` -
diff --git a/packages/compiler-tsx/src/template/generate.ts b/packages/compiler-tsx/src/template/generate.ts index 5016d055..172eeb84 100644 --- a/packages/compiler-tsx/src/template/generate.ts +++ b/packages/compiler-tsx/src/template/generate.ts @@ -117,10 +117,6 @@ export function generate( ): TransformedCode { ctx = createGenerateContext(options) - writeLine( - `import type { GlobalComponents as ${ctx.internalIdentifierPrefix}GlobalComponents } from '${ctx.runtimeModuleName}';`, - ) - if (ctx.used.components.size > 0) { wrap( `${annotations.templateGlobals.start}\n`, @@ -191,6 +187,7 @@ function writeLine(code: string): void { } function genRootNode(node: RootNode): void { + genKnownIdentifierGetters(node.scope.globals) writeLine(`function ${ctx.internalIdentifierPrefix}render() {`) indent(() => { node.scope.getBinding('$slots') // forces to declare $slots @@ -208,6 +205,29 @@ function genRootNode(node: RootNode): void { writeLine(`${ctx.internalIdentifierPrefix}render();`) } +function genKnownIdentifierGetters(ids: string[]): void { + const known = ids.filter((id) => ctx.identifiers.has(id)) + if (known.length === 0) return + wrap( + annotations.templateGlobals.start, + annotations.templateGlobals.end, + () => { + ctx.newLine() + known.forEach((id) => { + writeLine( + `const ${ + ctx.internalIdentifierPrefix + }_get_identifier_${id} = () => ${getRuntimeFn( + ctx.typeIdentifier, + 'unref', + )}(${id});`, + ) + }) + }, + ) + ctx.newLine() +} + function genDirectiveChecks(el: BaseElementNode): void { const directives = el.props.filter(isDirectiveNode).filter((directive) => { return !['on', 'bind', 'text', 'html', 'model'].includes(directive.name) @@ -267,7 +287,13 @@ function genGlobalDeclarations(node: Node): void { if (node.scope.globals.length === 0) return writeLine(annotations.templateGlobals.start) node.scope.globals.forEach((id) => { - writeLine(`let ${id} = ${ctx.contextIdentifier}.${id}`) + if (ctx.identifiers.has(id)) { + writeLine( + `let ${id} = ${ctx.internalIdentifierPrefix}_get_identifier_${id}();`, + ) + } else { + writeLine(`let ${id} = ${ctx.contextIdentifier}.${id}`) + } }) writeLine(annotations.templateGlobals.end) } @@ -917,7 +943,7 @@ function genSlotTypes(root: RootNode): void { }) writeLine(annotations.diagnosticsIgnore.start) - ctx.write(`function ${ctx.internalIdentifierPrefix}slots() {`).newLine() + ctx.write(`function ${ctx.internalIdentifierPrefix}_slots() {`).newLine() indent(() => { genGlobalDeclarations(root) ctx @@ -1326,7 +1352,7 @@ function genAttrTypes(root: RootNode): void { }) } - ctx.write(`const ${ctx.internalIdentifierPrefix}attrs = (() => {`).newLine() + ctx.write(`const ${ctx.internalIdentifierPrefix}_attrs = (() => {`).newLine() indent(() => { const value = typeCastAs('{}', 'unknown') ctx.write('return ') diff --git a/packages/compiler-tsx/src/template/runtime.ts b/packages/compiler-tsx/src/template/runtime.ts index 1292a66c..d4296e7b 100644 --- a/packages/compiler-tsx/src/template/runtime.ts +++ b/packages/compiler-tsx/src/template/runtime.ts @@ -12,7 +12,8 @@ export function getRuntimeFn( | 'resolveDirective' | 'union' | 'getAttrs' - | 'getProps', + | 'getProps' + | 'unref', ): string { return `${prefix}.internal.${name}` } diff --git a/packages/compiler-tsx/src/types/TransformOptions.ts b/packages/compiler-tsx/src/types/TransformOptions.ts index 82a60888..c19cb360 100644 --- a/packages/compiler-tsx/src/types/TransformOptions.ts +++ b/packages/compiler-tsx/src/types/TransformOptions.ts @@ -11,6 +11,7 @@ export interface TransformOptions { cache?: Cache runtimeModuleName?: string typeCheckModuleName?: string + typescript: typeof import('typescript/lib/tsserverlibrary') } export interface TransformOptionsResolved extends TransformOptions { @@ -58,4 +59,9 @@ export interface TransformOptionsResolved extends TransformOptions { * SFC Descriptor. */ descriptor: SFCDescriptor + + /** + * Known identifiers. + */ + identifiers: Set } diff --git a/packages/compiler-tsx/src/vue/SourceBuilder.ts b/packages/compiler-tsx/src/vue/SourceBuilder.ts deleted file mode 100644 index d26a4f19..00000000 --- a/packages/compiler-tsx/src/vue/SourceBuilder.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { invariant } from '@vuedx/shared' -import type { DecodedSourceMap } from 'magic-string' -import type { SourceMapLike } from '../types/SourceMapLike' -import type { TransformedCode } from '../types/TransformedCode' - -import { getMappings } from './sourceMapHelpers' - -export class SourceBuilder { - private isConsumed: boolean = false - private code: string = '' - private lines: number = 0 - - private readonly sourceMap: DecodedSourceMap - - constructor(fileName: string, content: string) { - this.sourceMap = { - file: fileName, - mappings: [], - names: [], - sources: [fileName], - sourcesContent: [content], - } - } - - append(code: string): void - append(code: string, sourceMap: SourceMapLike): void - append(code: string, sourceMap?: SourceMapLike): void { - if (this.isConsumed) throw new Error('SourceBuilder is consumed') - const chunk = code + '\n' - this.code += chunk - const lines = code.split('\n').length - this.lines += lines - if (sourceMap != null) { - const mappings = getMappings(sourceMap) - invariant(mappings.length <= lines, 'Invalid source map.') - const nameOffset = this.sourceMap.names.length - if (sourceMap.names != null) this.sourceMap.names.push(...sourceMap.names) - this.sourceMap.mappings.push( - ...mappings.map((mapping) => - mapping.map((segment) => { - if (segment.length === 5) { - return [ - segment[0], - 0, - segment[2], - segment[3], - segment[4] + nameOffset, - ] as [number, number, number, number, number] - } else if (segment.length === 4) { - return [segment[0], 0, segment[2], segment[3]] as [ - number, - number, - number, - number, - ] - } - return segment - }), - ), - ) - } - - for (let i = this.sourceMap.mappings.length; i < this.lines; i++) { - this.sourceMap.mappings.push(sourceMap == null ? [[0]] : []) - } - } - - end(): TransformedCode { - this.isConsumed = true - return { - code: this.code, - map: this.sourceMap, - } - } -} diff --git a/packages/compiler-tsx/src/vue/blocks/transformScript.ts b/packages/compiler-tsx/src/vue/blocks/transformScript.ts index 8a159dd7..61fcbb5f 100644 --- a/packages/compiler-tsx/src/vue/blocks/transformScript.ts +++ b/packages/compiler-tsx/src/vue/blocks/transformScript.ts @@ -1,16 +1,14 @@ import type { SFCScriptBlock } from '@vuedx/compiler-sfc' -import { getComponentName, invariant } from '@vuedx/shared' -import { parse, transformScript as transform } from '@vuedx/transforms' -import { decode } from 'sourcemap-codec' +import { invariant } from '@vuedx/shared' +import { transformScript as transform } from '@vuedx/transforms' import type { TransformedCode } from '../../types/TransformedCode' import type { TransformOptionsResolved } from '../../types/TransformOptions' export interface ScriptBlockTransformResult extends TransformedCode { exportIdentifier: string - decoratorIdentifier?: string + name: string + inheritAttrs: boolean identifiers: string[] - selfName?: string | undefined - inheritAttrs?: boolean | undefined } export function transformScript( script: SFCScriptBlock | null, @@ -18,50 +16,24 @@ export function transformScript( ): ScriptBlockTransformResult { const content = script?.content ?? '' - const ast = parse(content, { - isScriptSetup: true, - lang: script?.lang, - }) - - const result = transform(ast, { + const result = transform(content, { internalIdentifierPrefix: options.internalIdentifierPrefix, runtimeModuleName: options.runtimeModuleName, typeIdentifier: options.typeIdentifier, - isTypeScript: - script?.lang === 'ts' || script?.lang === 'tsx' || options.isTypeScript, + lang: (script?.lang ?? 'ts') as any, + fileName: options.fileName, + lib: options.typescript, + cache: options.cache, }) invariant(result.map != null) - const decoratorIdentifier: string = `${options.internalIdentifierPrefix}RegisterSelf` - const name = result.selfName ?? getComponentName(options.fileName) - let code: string = result.code - if (options.isTypeScript) { - code += `\nfunction ${decoratorIdentifier}(arg0: T) { - const key = "${name}" as const; - return { ...arg0, [key]: ${name} };\n}`.replace(/^[ ]+/gm, ' ') - } else { - code += `\n - /** - * @template T - * @param {T} arg0 - */ - function ${decoratorIdentifier}(arg0) { - const key = /** @type {"${name}"} */ ("${name}"); - return { ...arg0, [key]: ${name} };\n}`.replace(/^[ ]+/gm, ' ') - } - return { - code, - map: { - ...result.map, - sourcesContent: result.map.sourcesContent ?? [], - mappings: decode(result.map.mappings), - }, - exportIdentifier: result.exportIdentifier, - decoratorIdentifier, + code: result.code, + map: result.map, identifiers: result.identifiers, - selfName: result.selfName, + exportIdentifier: result.componentIdentifier, + name: result.name, inheritAttrs: result.inheritAttrs, } } diff --git a/packages/compiler-tsx/src/vue/blocks/transformScriptSetup.ts b/packages/compiler-tsx/src/vue/blocks/transformScriptSetup.ts index 0fc67430..37c9073a 100644 --- a/packages/compiler-tsx/src/vue/blocks/transformScriptSetup.ts +++ b/packages/compiler-tsx/src/vue/blocks/transformScriptSetup.ts @@ -1,45 +1,45 @@ import type { SFCScriptBlock } from '@vuedx/compiler-sfc' import { invariant } from '@vuedx/shared' -import { parse, transformScriptSetup as transform } from '@vuedx/transforms' -import { decode } from 'sourcemap-codec' +import { transformScriptSetup as transform } from '@vuedx/transforms' import type { TransformedCode } from '../../types/TransformedCode' import type { TransformOptionsResolved } from '../../types/TransformOptions' export interface ScriptSetupBlockTransformResult extends TransformedCode { exportIdentifier: string - propsIdentifier?: string - emitIdentifier?: string - exposeIdentifier?: string + scopeIdentifier: string + propsIdentifier: string + emitsIdentifier: string + exposeIdentifier: string identifiers: string[] + exports: Record } export function transformScriptSetup( script: SFCScriptBlock | null, options: TransformOptionsResolved, -): ScriptSetupBlockTransformResult | null { - if (script == null) return null - const ast = parse(script.content, { isScriptSetup: true, lang: script.lang }) - const result = transform(ast, { +): ScriptSetupBlockTransformResult { + const content = script?.content ?? '' + const result = transform(content, { internalIdentifierPrefix: options.internalIdentifierPrefix, runtimeModuleName: options.runtimeModuleName, typeIdentifier: options.typeIdentifier, - isTypeScript: - script.lang === 'ts' || script.lang === 'tsx' || options.isTypeScript, + lang: (script?.lang ?? 'ts') as any, + fileName: options.fileName, + lib: options.typescript, + cache: options.cache, }) invariant(result.map != null) return { code: result.code, - map: { - ...result.map, - sourcesContent: result.map.sourcesContent ?? [], - mappings: decode(result.map.mappings), - }, - exportIdentifier: result.exportIdentifier, + map: result.map, + identifiers: result.identifiers, + exportIdentifier: result.componentIdentifier, + scopeIdentifier: result.scopeIdentifier, propsIdentifier: result.propsIdentifier, - emitIdentifier: result.emitIdentifier, + emitsIdentifier: result.emitsIdentifier, exposeIdentifier: result.exposeIdentifier, - identifiers: result.identifiers, + exports: result.exports, } } diff --git a/packages/compiler-tsx/src/vue/blocks/transformTemplate.ts b/packages/compiler-tsx/src/vue/blocks/transformTemplate.ts index c6b30647..b72b4a79 100644 --- a/packages/compiler-tsx/src/vue/blocks/transformTemplate.ts +++ b/packages/compiler-tsx/src/vue/blocks/transformTemplate.ts @@ -15,8 +15,8 @@ export function transformTemplate( template: SFCTemplateBlock | null, options: TransformOptionsResolved, ): TemplateBlockTransformResult { - const slotsIdentifier = `${options.internalIdentifierPrefix}slots` - const attrsIdentifier = `${options.internalIdentifierPrefix}attrs` + const slotsIdentifier = `${options.internalIdentifierPrefix}_slots` + const attrsIdentifier = `${options.internalIdentifierPrefix}_attrs` if (template == null) { return { code: `function ${slotsIdentifier}() { return {} }; const ${attrsIdentifier} = {};`, diff --git a/packages/compiler-tsx/src/vue/compile.ts b/packages/compiler-tsx/src/vue/compile.ts index d7f5ed28..b7710004 100644 --- a/packages/compiler-tsx/src/vue/compile.ts +++ b/packages/compiler-tsx/src/vue/compile.ts @@ -4,14 +4,17 @@ import { SFCBlock, SFCDescriptor, } from '@vuedx/compiler-sfc' -import { Cache, createCache, getComponentName } from '@vuedx/shared' +import { + Cache, + createCache, + rebaseSourceMap, + SourceTransformer, +} from '@vuedx/shared' import type { TransformOptions, TransformOptionsResolved, } from '../types/TransformOptions' import { transformCustomBlock } from './blocks/transformCustomBlock' -import { SourceBuilder } from './SourceBuilder' -import { rebaseSourceMap } from './sourceMapHelpers' import type { RootNode } from '@vue/compiler-core' import type { RawSourceMap } from 'source-map' @@ -28,7 +31,6 @@ export interface CompileOutput extends TransformedCode { template?: RootNode descriptor: SFCDescriptor errors: Array - unusedIdentifiers: string[] } export function compile( @@ -53,7 +55,7 @@ export function compileWithDecodedSourceMap( ): CompileOutput { // performance.mark('beforeTransform') const cache = options.cache ?? createCache(100) - const key = (name: string): string => `${options.fileName}::${name}` + const key = (name: string): string => `${options.fileName}::block:${name}` const previous = cache.get(key('descriptor')) as SFCDescriptor | undefined const { descriptor, errors } = parse(source) @@ -72,42 +74,63 @@ export function compileWithDecodedSourceMap( isTypeScript: options.isTypeScript ?? (lang === 'ts' || lang === 'tsx'), cache, descriptor, + identifiers: new Set(), } - const builder = new SourceBuilder(options.fileName, source) + const builder = new SourceTransformer(options.fileName, source) - const script = runIfNeeded( - key('script'), - previous?.script, - descriptor.script, - cache, - () => transformScript(descriptor.script, resolvedOptions), + const isScriptChanged = hasBlockChanged(previous?.script, descriptor.script) + + const script = runIfNeeded(key('script'), isScriptChanged, cache, () => + transformScript(descriptor.script, resolvedOptions), ) - const scriptSetup = runIfNeeded( - key('scriptSetup'), + const isScriptSetupChanged = hasBlockChanged( previous?.scriptSetup, descriptor.scriptSetup, + ) + const scriptSetup = runIfNeeded( + key('scriptSetup'), + isScriptSetupChanged, cache, () => transformScriptSetup(descriptor.scriptSetup, resolvedOptions), ) + resolvedOptions.identifiers = new Set([ + ...script.identifiers, + ...scriptSetup.identifiers, + ]) + const template = runIfNeeded( key('template'), - previous?.template, - descriptor.template, + isScriptChanged || + isScriptSetupChanged || + hasBlockChanged(previous?.template, descriptor.template), cache, () => transformTemplate(descriptor.template, resolvedOptions), ) + const name = script.name function region(name: string, fn: () => void): void { + builder.nextLine() builder.append(`//#region ${name}`) + builder.nextLine() fn() + builder.nextLine() builder.append(`//#endregion`) + builder.nextLine() } builder.append( - `import * as ${resolvedOptions.typeIdentifier} from '${resolvedOptions.typeCheckModuleName}';`, + [ + `import * as ${resolvedOptions.typeIdentifier} from '${resolvedOptions.typeCheckModuleName}';`, + `import { ${['defineComponent', 'GlobalComponents'] + .map( + (id) => `${id} as ${resolvedOptions.internalIdentifierPrefix}${id}`, + ) + .join(', ')} } from '${resolvedOptions.runtimeModuleName}';`, + ].join('\n'), ) + builder.nextLine() region('\n\n\n"]} \ No newline at end of file +{"version":3,"file":"/Users/znck/Workspace/OpenSource/vuedx/languagetools/packages/compiler-tsx/test/fixtures/ts-script-template.tsx","mappings":";;;AAAkB;AAClB;mCAEc;AACd;AACA,EAAE;;;;;;;;;;;;;;;;A,4B;A,E,2B;A,E,0B;A,E,kC;A,E,gC;A,E,4B;A,E,Q;A,I,E;A,MAIA,CAACA,G;A;A,QAEEC,K,C,CAAOC,Y,C;A,QACPC,S,C,CAAWH,G,C;A,Q,OACVA,G,C,EAAYI,I,G;A,Q,KACNJ,G,E;A,QACPK,O,C,C,mC;A,U,O;A,UAAOC,W;A,U,C,E;A,U,O;A,UACKA,W;A,U,C,E;A,U,O;A,UACCA,W;A,U,C,E;A,Q,E,C;A,QARZ,mC;A,M,C;A,QAUF,CAAGN,GAAG,C;A,MACR,E,G,C;A,I,G;A,E,C;A,A,C;A,A,kB;A,A,6B;A,A,4B;A,E,2B;A,E,0B;A,E,kC;A,E,gC;A,E,4B;A,E,2E;A,E,G;A,A,C;A,A,8B;A,A,gC;A,E,O,iC,C,gC,E;A,I,kF,C;A,E,G;A,A,K;;;;;;;;;;;","names":["<

>3","<

>5","<

>12","<

>9","<

>4","<>5|7","<

>11"],"sources":["/Users/znck/Workspace/OpenSource/vuedx/languagetools/packages/compiler-tsx/test/fixtures/ts-script-template.vue"],"sourcesContent":["\n\n\n"]} \ No newline at end of file diff --git a/packages/compiler-tsx/test/render-tsx.spec.ts b/packages/compiler-tsx/test/render-tsx.spec.ts index e798c08a..958f1dc6 100644 --- a/packages/compiler-tsx/test/render-tsx.spec.ts +++ b/packages/compiler-tsx/test/render-tsx.spec.ts @@ -4,6 +4,7 @@ import * as FS from 'fs' import { addSerializer } from 'jest-specific-snapshot' import * as Path from 'path' import { compile, CompileOptions } from '../src' +import * as typescript from 'typescript/lib/tsserverlibrary' addSerializer({ serialize(val: any) { @@ -120,6 +121,7 @@ function parseFixtures(content: string) { fileName: '/tmp/compiler-tsx/Example.vue', isTypeScript: true, ...fixture.options, + typescript, }, ) diff --git a/packages/compiler-tsx/test/vue-to-tsx.spec.ts b/packages/compiler-tsx/test/vue-to-tsx.spec.ts index c4c49c3c..ee326b10 100644 --- a/packages/compiler-tsx/test/vue-to-tsx.spec.ts +++ b/packages/compiler-tsx/test/vue-to-tsx.spec.ts @@ -2,6 +2,7 @@ import FS from 'fs' import Path from 'path' import { encode } from 'sourcemap-codec' import { compileWithDecodedSourceMap } from '../src/vue/compile' +import * as typescript from 'typescript/lib/tsserverlibrary' describe('Vue to TSX compiler', () => { const dir = Path.join(__dirname, 'fixtures') @@ -12,7 +13,7 @@ describe('Vue to TSX compiler', () => { const fixture = await FS.promises.readFile(fileName, 'utf-8') const result = compileWithDecodedSourceMap(fixture, { fileName, - + typescript, isTypeScript: true, }) expect(result.code).toMatchSnapshot(file) diff --git a/packages/shared/package.json b/packages/shared/package.json index c425e194..be6ceced 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,6 +8,7 @@ "lib" ], "dependencies": { - "@sentry/node": "^5.30.0" + "@sentry/node": "^5.30.0", + "sourcemap-codec": "^1.4.8" } } diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index f7b9735a..0e7cb7de 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -17,6 +17,7 @@ export * from './reactivity' export * as RPC from './rpc' export * from './set' export * from './string' +export * from './source' export * from './telemetry' export * from './types' export * from './performance' diff --git a/packages/shared/src/source.spec.ts b/packages/shared/src/source.spec.ts new file mode 100644 index 00000000..24dfef0e --- /dev/null +++ b/packages/shared/src/source.spec.ts @@ -0,0 +1,93 @@ +import { SourceTransformer } from './source' + +describe(SourceTransformer, () => { + it('no new lines', () => { + const transformer = new SourceTransformer('', '') + transformer.append('foo') + transformer.append('bar') + expect(transformer.end().code).toBe('foobar') + expect(transformer.end().map.mappings).toHaveLength(1) + }) + + it('trailing new lines', () => { + const transformer = new SourceTransformer('', '') + transformer.append('foo\n') + transformer.append('bar\n') + const result = transformer.end() + expect(result.code).toBe('foo\nbar\n') + expect(result.map.mappings).toHaveLength(result.code.split('\n').length) + }) + + it('leading new lines', () => { + const transformer = new SourceTransformer('', '') + transformer.append('\nfoo') + transformer.append('\nbar') + const result = transformer.end() + expect(result.code).toBe('\nfoo\nbar') + expect(result.map.mappings).toHaveLength(result.code.split('\n').length) + }) + + it('multiple lines', () => { + const transformer = new SourceTransformer('', '') + transformer.append('foo\nfoo') + transformer.append('bar\nbar') + const result = transformer.end() + expect(result.code).toBe('foo\nfoobar\nbar') + expect(result.map.mappings).toHaveLength(result.code.split('\n').length) + }) + + it('clone source', () => { + const transformer = new SourceTransformer('', 'foo-bar-baz') + transformer.append('foo') + transformer.clone(4, 7) + transformer.append('baz') + const result = transformer.end() + expect(result.code).toBe('foobarbaz') + expect(result.map.mappings).toHaveLength(result.code.split('\n').length) + expect(result.map.mappings[0][0]).toMatchInlineSnapshot(` + [ + 3, + 0, + 0, + 4, + ] + `) + }) + it('clone source multiline', () => { + const transformer = new SourceTransformer('', 'foo\nbar\nbaz') + transformer.append('foo') + transformer.clone(4, 7) + transformer.append('baz') + const result = transformer.end() + expect(result.code).toBe('foobarbaz') + expect(result.map.mappings).toHaveLength(result.code.split('\n').length) + expect(result.map.mappings[0][0]).toMatchInlineSnapshot(` + [ + 3, + 0, + 1, + 0, + ] + `) + }) + + it('generates correct source map', () => { + const transformer = new SourceTransformer('', '') + transformer.append('foo') + transformer.nextLine() + transformer.append('bar\n') + transformer.nextLine() + transformer.append('baz') + transformer.nextLine() + const result = transformer.end() + expect(result.map.mappings).toHaveLength(result.code.split('\n').length) + expect(result.map.mappings).toMatchInlineSnapshot(` + [ + [], + [], + [], + [], + ] + `) + }) +}) diff --git a/packages/shared/src/source.ts b/packages/shared/src/source.ts new file mode 100644 index 00000000..b243ccc2 --- /dev/null +++ b/packages/shared/src/source.ts @@ -0,0 +1,251 @@ +import { decode } from 'sourcemap-codec' +import { binarySearch, BinarySearchBias, first, last } from './array' +import { invariant } from './assert' + +export type SourceMapSegment = + | [generatedColumn: number] + | [ + generatedColumn: number, + sourceIndex: number, + originalLine: number, + originalColumn: number, + ] + | [ + generatedColumn: number, + sourceIndex: number, + originalLine: number, + originalColumn: number, + nameIndex: number, + ] + +export interface DecodedSourceMap { + file: string + sources: string[] + sourcesContent: string[] + names: string[] + mappings: SourceMapSegment[][] +} + +export type SourceMapLike = + | { + mappings: string + names?: string[] + } + | PartialDecodedSourceMap + +interface PartialDecodedSourceMap { + names?: string[] + mappings: SourceMapSegment[][] +} + +class LineColumnMapper { + private readonly offsets: number[] + + constructor(source: string) { + this.offsets = [] + const lines = source.split('\n') + let offset = 0 + for (const line of lines) { + this.offsets.push(offset) + + offset += line.length + 1 + } + } + + public positionAt(position: number): { line: number; column: number } { + const line = binarySearch( + position, + this.offsets, + (a, b) => a - b, + BinarySearchBias.GREATEST_LOWER_BOUND, + ) + const offset = this.offsets[line] + invariant(offset != null, 'Invalid position.') + return { line, column: position - offset } + } + + public offsetAt(line: number, column: number): number { + const offsets = this.offsets[line] + invariant(offsets != null, 'Invalid position.') + return offsets + column + } +} + +export class SourceTransformer { + private readonly source: string + private readonly sourceMap: DecodedSourceMap + private sourceLineColumnMapper?: LineColumnMapper + + private code: string = '' + + private line: number = 0 + private column: number = 0 + + constructor(fileName: string, source: string) { + this.source = source + this.sourceMap = { + file: fileName, + mappings: [], + names: [], + sources: [fileName], + sourcesContent: [source], + } + } + + nextLine(): void { + if (!this.code.endsWith('\n')) { + this.append(`\n`) + } + } + + append(code: string, sourceMap?: SourceMapLike): void { + const lines = code.split('\n') + const lastLine = last(lines) + this.code += code + + let mappings: DecodedSourceMap['mappings'] = [] + if (sourceMap != null) { + const nameOffset = this.sourceMap.names.length + if (sourceMap.names != null) this.sourceMap.names.push(...sourceMap.names) + mappings = getMappings(sourceMap).map((mapping) => + mapping.map((segment) => { + if (segment.length === 5) { + return [ + segment[0], + 0, + segment[2], + segment[3], + segment[4] + nameOffset, + ] as [number, number, number, number, number] + } else if (segment.length === 4) { + return [segment[0], 0, segment[2], segment[3]] as [ + number, + number, + number, + number, + ] + } + return segment + }), + ) + } + invariant( + mappings.length <= lines.length, + `Invalid source map: ${mappings.length} > ${ + lines.length + }:\n${code},\n${JSON.stringify(mappings, null, 2)}`, + ) + + const current = (this.sourceMap.mappings[this.line] = + this.sourceMap.mappings[this.line] ?? []) + if (mappings.length > 0) { + current.push( + ...first(mappings).map((mapping) => { + mapping[0] += this.column + return mapping + }), + ) + } + + if (lines.length === 1) { + this.column += lastLine.length + } else { + this.line += lines.length - 1 + this.column = lastLine.length + this.sourceMap.mappings.push(...mappings.slice(1)) + } + + for (let i = this.sourceMap.mappings.length; i <= this.line; i++) { + this.sourceMap.mappings.push([]) + } + } + + clone(start: number, end: number): void { + if (start >= end) return + const code = this.source.slice(start, end) + if (code.length === 0) return + const mapper = + this.sourceLineColumnMapper ?? + (this.sourceLineColumnMapper = new LineColumnMapper(this.source)) + const { line, column } = mapper.positionAt(start) + const lines = code.split('\n') + const sourceMap: PartialDecodedSourceMap = { + mappings: [[[0, 0, line, column]]], + } + for (let i = 1; i < lines.length; i++) { + if (lines[i]?.length === 0) sourceMap.mappings.push([]) + else sourceMap.mappings.push([[0, 0, line + i, 0]]) + } + this.append(code, sourceMap) + } + + end(): { code: string; map: DecodedSourceMap } { + return { + code: this.code, + map: this.sourceMap, + } + } +} + +export function getMappings( + sourceMap: SourceMapLike, +): DecodedSourceMap['mappings'] { + return typeof sourceMap.mappings === 'string' + ? decode(sourceMap.mappings) + : sourceMap.mappings +} + +export function rebaseSourceMap( + sourceMap: SourceMapLike, + startPosition?: { line: number; column: number }, +): DecodedSourceMap { + const mappings = getMappings(sourceMap) + + if (startPosition == null) { + return { + file: '', + sources: [], + sourcesContent: [], + ...sourceMap, + names: sourceMap.names ?? [], + mappings, + } + } + + const line = startPosition.line - 1 + const column = startPosition.column - 1 + + return { + file: '', + sources: [], + sourcesContent: [], + ...sourceMap, + names: sourceMap.names ?? [], + mappings: mappings.map((mapping) => + mapping.map((segment) => { + if (segment.length === 1) return segment + + let originalLine = segment[2] + let originalColumn = segment[3] + + if (originalLine === 0) { + originalColumn += column + } + + originalLine += line + + if (segment.length === 4) { + return [segment[0], segment[1], originalLine, originalColumn] + } + + return [ + segment[0], + segment[1], + originalLine, + originalColumn, + segment[4], + ] + }), + ), + } +} diff --git a/packages/transforms/package.json b/packages/transforms/package.json index 82ae19d5..ee232588 100644 --- a/packages/transforms/package.json +++ b/packages/transforms/package.json @@ -24,16 +24,9 @@ }, "homepage": "https://github.com/znck/vue-developer-experience#readme", "dependencies": { - "@vuedx/shared": "workspace:*", - "@babel/generator": "^7.19.0", - "@babel/parser": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0", - "magic-string": "^0.26.1" + "@vuedx/shared": "workspace:*" }, "devDependencies": { - "typescript": "^4.6.3", - "@types/babel__template": "^7.4.1", - "@types/babel__generator": "^7.6.4" + "typescript": "^4.6.3" } } diff --git a/packages/transforms/src/RequiredProperties.ts b/packages/transforms/src/RequiredProperties.ts deleted file mode 100644 index 72391a4c..00000000 --- a/packages/transforms/src/RequiredProperties.ts +++ /dev/null @@ -1,2 +0,0 @@ -export type RequiredProperties = Pick, K> & - Exclude diff --git a/packages/transforms/src/TransformScriptOptions.ts b/packages/transforms/src/TransformScriptOptions.ts new file mode 100644 index 00000000..1a09ce74 --- /dev/null +++ b/packages/transforms/src/TransformScriptOptions.ts @@ -0,0 +1,12 @@ +import { Cache } from '@vuedx/shared' +import TypeScript from 'typescript/lib/tsserverlibrary' + +export interface TransformScriptOptions { + fileName: string + internalIdentifierPrefix: string + typeIdentifier: string + runtimeModuleName: string + lang: 'js' | 'ts' | 'tsx' | 'jsx' + lib: typeof TypeScript + cache?: Cache +} diff --git a/packages/transforms/src/generate.ts b/packages/transforms/src/generate.ts deleted file mode 100644 index 901923d7..00000000 --- a/packages/transforms/src/generate.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - CodeGenerator, - GeneratorOptions, - GeneratorResult, -} from '@babel/generator' -import * as T from '@babel/types' - -export function generate( - node: T.Node | T.Node[], - options: GeneratorOptions = {}, -): GeneratorResult { - const nodes = Array.isArray(node) ? node : [node] - const statement = T.program( - nodes - .map((node) => T.toStatement(node)) - .filter( - (node): node is T.Statement => node !== false && T.isStatement(node), - ), - ) - - const result = new CodeGenerator(statement as any, { - comments: true, - compact: false, - concise: false, - sourceMaps: true, - retainFunctionParens: true, - sourceFileName: 'input.ts', - sourceRoot: '', - ...options, - }).generate() - - return result -} diff --git a/packages/transforms/src/index.ts b/packages/transforms/src/index.ts index 63f7791b..2211710f 100644 --- a/packages/transforms/src/index.ts +++ b/packages/transforms/src/index.ts @@ -1,8 +1,3 @@ -export * from './generate' -export * from './parse' -export * from './search/findDefinePropsStatement' -export * from './search/findLocalIdentifierName' -export * from './search/findObjectProperty' -export * from './search/findScopeBindings' -export * from './search/findTopLevelCall' -export * from './transform/transformScriptSetup' +export * from './TransformScriptOptions' +export * from './tsTransformScript' +export * from './tsTransformScriptSetup' diff --git a/packages/transforms/src/parse.ts b/packages/transforms/src/parse.ts deleted file mode 100644 index 0e6ffaa2..00000000 --- a/packages/transforms/src/parse.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { parse as parseUsingBabel, ParserOptions } from '@babel/parser' -import * as T from '@babel/types' -import { RequiredProperties } from './RequiredProperties' - -export interface ParseOptions extends ParserOptions { - isScriptSetup: boolean - lang: string -} - -export function parse( - code: string, - options: Partial = {}, -): T.File { - const { isScriptSetup = false, lang = 'js', ...config } = options - const finalOptions: RequiredProperties = { - sourceType: 'module' as const, - allowAwaitOutsideFunction: isScriptSetup, - ...config, - ranges: true, - errorRecovery: true, - plugins: [ - 'bigInt', - 'nullishCoalescingOperator', - 'optionalChaining', - 'optionalCatchBinding', - 'dynamicImport', - 'logicalAssignment', - ], - } - - if (options.plugins != null) finalOptions.plugins.push(...options.plugins) - - if (isScriptSetup) { - finalOptions.plugins.push('topLevelAwait') - } - - if (lang.startsWith('ts')) { - finalOptions.plugins.push('typescript') - } - - if (lang.endsWith('x')) { - finalOptions.plugins.push('jsx') - } - - finalOptions.plugins = Array.from(new Set(finalOptions.plugins)) - - return parseUsingBabel(code, finalOptions) as any -} diff --git a/packages/transforms/src/search/findDefinePropsStatement.ts b/packages/transforms/src/search/findDefinePropsStatement.ts deleted file mode 100644 index 2235eb4c..00000000 --- a/packages/transforms/src/search/findDefinePropsStatement.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as T from '@babel/types' -import { memoizeByFirstArg } from '@vuedx/shared' -import { findTopLevelCall } from './findTopLevelCall' - -export const findDefinePropsStatement = memoizeByFirstArg((ast: T.File) => { - const withDefaults = findTopLevelCall(ast, 'withDefaults') - - if (withDefaults != null) { - const fn = T.isCallExpression(withDefaults) - ? withDefaults - : withDefaults.init - if (T.isCallExpression(fn)) { - const args = fn.arguments - if (T.isCallExpression(args[0])) return args[0] - return null - } - } - - return findTopLevelCall(ast, 'defineProps') -}) diff --git a/packages/transforms/src/search/findLocalIdentifierName.ts b/packages/transforms/src/search/findLocalIdentifierName.ts deleted file mode 100644 index b741ed09..00000000 --- a/packages/transforms/src/search/findLocalIdentifierName.ts +++ /dev/null @@ -1,30 +0,0 @@ -import * as T from '@babel/types' - -/** - * Find local identifier name of imported identifier. - */ - -export function findLocalIdentifierName( - ast: T.File, - source: string, - exportedName: string, -): string | null { - for (const statement of ast.program.body) { - if (T.isImportDeclaration(statement)) { - if (statement.source.value === source) { - for (const specifier of statement.specifiers) { - if (T.isImportSpecifier(specifier)) { - const name = T.isIdentifier(specifier.imported) - ? specifier.imported.name - : specifier.imported.value - if (name === exportedName) { - return specifier.local.name - } - } - } - } - } - } - - return null -} diff --git a/packages/transforms/src/search/findObjectProperty.ts b/packages/transforms/src/search/findObjectProperty.ts deleted file mode 100644 index a0c9e989..00000000 --- a/packages/transforms/src/search/findObjectProperty.ts +++ /dev/null @@ -1,19 +0,0 @@ -import * as T from '@babel/types' - -export function findObjectProperty( - node: T.ObjectExpression, - propertyName: string, -): T.Expression | null { - return ( - (node.properties.find((property): property is T.ObjectProperty => { - if (T.isObjectProperty(property)) { - if (T.isStringLiteral(property.key)) { - return property.key.value === propertyName - } else if (T.isIdentifier(property.key)) { - return property.key.name === propertyName - } - } - return false - })?.value as any) ?? null - ) -} diff --git a/packages/transforms/src/search/findScopeBindings.ts b/packages/transforms/src/search/findScopeBindings.ts deleted file mode 100644 index d0563318..00000000 --- a/packages/transforms/src/search/findScopeBindings.ts +++ /dev/null @@ -1,87 +0,0 @@ -import * as T from '@babel/types' -import { memoizeByFirstArg } from '@vuedx/shared' - -export const findScopeBindings = memoizeByFirstArg( - (statements: T.Statement[]): string[] => { - const identifiers = new Set() - - const processIdentifier = (node: T.Identifier): void => { - identifiers.add(node.name) - } - const processAssignmentPattern = (node: T.AssignmentPattern): void => { - if (T.isIdentifier(node.left)) processIdentifier(node.left) - else if (T.isObjectPattern(node.left)) processObjectPattern(node.left) - else if (T.isArrayPattern(node.left)) processArrayPattern(node.left) - } - const processRestElement = (node: T.RestElement): void => { - if (T.isIdentifier(node.argument)) processIdentifier(node.argument) - } - const processObjectPattern = (node: T.ObjectPattern): void => { - node.properties.forEach((property) => { - if (T.isRestElement(property)) { - processRestElement(property) - } else if (T.isObjectProperty(property)) { - if (T.isIdentifier(property.value)) { - processIdentifier(property.value) - } else if (T.isAssignmentPattern(property.value)) { - processAssignmentPattern(property.value) - } else if (T.isRestElement(property.value)) { - processRestElement(property.value) - } else if (T.isArrayPattern(property.value)) { - processArrayPattern(property.value) - } else if (T.isObjectPattern(property.value)) { - processObjectPattern(property.value) - } - } else { - // - exaustive if branches - // property - } - }) - } - const processArrayPattern = (node: T.ArrayPattern): void => { - node.elements.forEach((element) => { - if (T.isIdentifier(element)) { - processIdentifier(element) - } else if (T.isAssignmentPattern(element)) { - processAssignmentPattern(element) - } else if (T.isRestElement(element)) { - processRestElement(element) - } else if (T.isArrayPattern(element)) { - processArrayPattern(element) - } else if (T.isObjectPattern(element)) { - processObjectPattern(element) - } - }) - } - statements.forEach((statement) => { - if (T.isImportDeclaration(statement) && statement.importKind !== 'type') { - statement.specifiers.forEach((specifier) => { - if (T.isImportSpecifier(specifier) && specifier.importKind === 'type') - return // type-only import - - identifiers.add(specifier.local.name) - }) - } else if ( - T.isFunctionDeclaration(statement) || - T.isTSDeclareFunction(statement) - ) { - if (statement.id != null) processIdentifier(statement.id) - } else if (T.isVariableDeclaration(statement)) { - statement.declarations.forEach((declaration) => { - if (T.isIdentifier(declaration.id)) processIdentifier(declaration.id) - else if (T.isObjectPattern(declaration.id)) { - processObjectPattern(declaration.id) - } else if (T.isArrayPattern(declaration.id)) { - processArrayPattern(declaration.id) - } - }) - } else if (T.isClassDeclaration(statement)) { - processIdentifier(statement.id) - } else if (T.isTSEnumDeclaration(statement)) { - processIdentifier(statement.id) - } - }) - - return Array.from(identifiers) - }, -) diff --git a/packages/transforms/src/search/findTopLevelCall.ts b/packages/transforms/src/search/findTopLevelCall.ts deleted file mode 100644 index 6b2822ce..00000000 --- a/packages/transforms/src/search/findTopLevelCall.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as T from '@babel/types' - -export function findTopLevelCall( - ast: T.File, - fn: string, -): T.CallExpression | T.VariableDeclarator | null { - const isExpression = (exp: T.Expression): exp is T.CallExpression => - T.isCallExpression(exp) && - T.isIdentifier(exp.callee) && - exp.callee.name === fn - - for (const statement of ast.program.body) { - if (T.isExpressionStatement(statement)) { - if (isExpression(statement.expression)) { - return statement.expression - } - } else if (T.isVariableDeclaration(statement)) { - for (const declaration of statement.declarations) { - if (declaration.init != null && isExpression(declaration.init)) { - return declaration - } - } - } - } - - return null -} diff --git a/packages/transforms/src/transform/transformScriptSetup.ts b/packages/transforms/src/transform/transformScriptSetup.ts deleted file mode 100644 index 2b8613a5..00000000 --- a/packages/transforms/src/transform/transformScriptSetup.ts +++ /dev/null @@ -1,490 +0,0 @@ -import { GeneratorResult } from '@babel/generator' -import * as T from '@babel/types' -import { first, invariant, memoizeByFirstArg } from '@vuedx/shared' -import { generate } from '../generate' -import { findScopeBindings } from '../search/findScopeBindings' - -export interface TransformScriptSetupOptions { - internalIdentifierPrefix: string - typeIdentifier: string - runtimeModuleName: string - isTypeScript: boolean -} - -export interface TransformScriptSetupResult extends GeneratorResult { - exportIdentifier: string - propsIdentifier?: string - emitIdentifier?: string - exposeIdentifier?: string - identifiers: string[] -} - -export const transformScriptSetup = memoizeByFirstArg( - (ast: T.File, options: TransformScriptSetupOptions) => { - const props = options.internalIdentifierPrefix + 'props' - const emit = options.internalIdentifierPrefix + 'emits' - const expose = options.internalIdentifierPrefix + 'expose' - const defineComponent = options.internalIdentifierPrefix + 'defineComponent' - const result: TransformScriptSetupResult = { - code: '', - map: null, - exportIdentifier: options.internalIdentifierPrefix + 'SetupComponent', - identifiers: [], - } - - const hoists: T.Statement[] = [ - createNamedImport( - 'defineComponent', - defineComponent, - options.runtimeModuleName, - ), - ] - const others: T.Statement[] = [] - - ast.program.body.forEach((statement) => { - if ( - T.isImportDeclaration(statement) || - T.isTSInterfaceDeclaration(statement) || - T.isTSTypeAliasDeclaration(statement) || - T.isTSEnumDeclaration(statement) - ) { - hoists.push(statement) - } else if (T.isExpressionStatement(statement)) { - if (T.isCallExpression(statement.expression)) { - if (T.isIdentifier(statement.expression.callee)) { - switch (statement.expression.callee.name) { - case 'withDefaults': { - const args = statement.expression.arguments - if (args.length >= 1) { - result.propsIdentifier = props - invariant(T.isExpression(args[0])) - // create props declaration - hoists.push(createVariable(props, args[0])) - - // replace withDefaults arguments - statement = T.cloneNode(statement, true) - invariant(T.isCallExpression(statement.expression)) - statement.expression.arguments[0] = T.identifier(props) - hoists.push(statement) - } - break - } - - case 'defineProps': - hoists.push(createVariable(props, statement.expression)) - result.propsIdentifier = props - break - case 'defineEmits': - hoists.push( - T.expressionStatement( - transformDefineEmits(statement.expression), - ), - ) - - break - case 'defineExpose': { - hoists.push( - T.expressionStatement(transformExpose(statement.expression)), - ) - break - } - default: - others.push(statement) - } - } else { - others.push(statement) - } - } else { - others.push(statement) - } - } else if (T.isVariableDeclaration(statement)) { - const declarations: T.VariableDeclarator[] = [] - const extracted: T.VariableDeclarator[] = [] - for (const declaration of statement.declarations) { - let isHandled = false - if (T.isCallExpression(declaration.init)) { - if ( - T.isIdentifier(declaration.init.callee) && - T.isIdentifier(declaration.id) - ) { - isHandled = true - switch (declaration.init.callee.name) { - case 'withDefaults': { - const args = declaration.init.arguments - if (args.length >= 1) { - result.propsIdentifier = props - invariant(T.isExpression(args[0])) - // create props declaration - hoists.push(createVariable(props, args[0])) - - // replace withDefaults arguments - const statement = T.cloneNode(declaration.init, true) - invariant(T.isCallExpression(statement)) - statement.arguments[0] = T.identifier(props) - hoists.push(createVariable(declaration.id.name, statement)) - } - - break - } - - case 'defineProps': - hoists.push(createVariable(declaration.id, declaration.init)) - result.propsIdentifier = declaration.id.name - break - case 'defineEmits': - hoists.push( - createVariable( - declaration.id, - transformDefineEmits(declaration.init), - ), - ) - - break - - default: - isHandled = false - } - } - } - - if (!isHandled) { - declarations.push(declaration) - } else { - extracted.push(declaration) - } - } - - if (extracted.length > 0) { - if (declarations.length > 0) { - others.push(T.variableDeclaration(statement.kind, declarations)) - } - } else { - others.push(statement) - } - } else if (T.isExportNamedDeclaration(statement)) { - if ( - T.isTSTypeAliasDeclaration(statement.declaration) || - T.isTSInterfaceDeclaration(statement.declaration) || - T.isTSEnumDeclaration(statement.declaration) || - statement.exportKind === 'type' - ) { - hoists.push(statement) - } else { - others.push(statement) - } - } else { - others.push(statement) - } - }) - - result.identifiers = findScopeBindings(ast.program.body) - - const exportsFromSetup = result.identifiers.slice() - if (result.exposeIdentifier != null) { - exportsFromSetup.push(result.exposeIdentifier) - } - - const arg0 = T.identifier(options.internalIdentifierPrefix + 'arg0') - const returnStatement = T.returnStatement( - T.objectExpression( - exportsFromSetup.map((id) => - T.objectProperty(T.identifier(id), T.identifier(id), false, true), - ), - ), - ) - T.addComment(returnStatement, 'leading', '', false) - T.addComment(returnStatement, 'trailing', '', false) - const setup = T.arrowFunctionExpression( - [arg0], - T.blockStatement([...others, returnStatement]), - - isAsync(others), - ) - if (result.propsIdentifier != null) { - if (options.isTypeScript) { - arg0.typeAnnotation = T.tsTypeAnnotation( - T.tsTypeQuery(T.identifier(result.propsIdentifier)), - ) - } else { - T.addComment( - setup, - 'leading', - [ - `*`, - ` * @param {typeof ${result.propsIdentifier}} ${options.internalIdentifierPrefix}arg0`, - ` `, - ].join('\n'), - false, - ) - } - } - - const component = T.callExpression(T.identifier(defineComponent), [setup]) - const node = createVariable(result.exportIdentifier, component) - const output = generate([...hoists, node]) - - return { ...result, ...output } - - function transformDefineEmits(node: T.CallExpression): T.CallExpression { - node = T.cloneNode(node, true) - - if (node.arguments.length > 0) { - if (T.isExpression(node.arguments[0])) { - hoists.push(createVariable(emit, node.arguments[0])) - node.arguments[0] = T.identifier(emit) - result.emitIdentifier = emit - } - } else if (node.typeParameters?.params?.[0] != null) { - const emitType = `${emit}Type` - hoists.push( - T.tsTypeAliasDeclaration( - T.identifier(emitType), - null, - node.typeParameters.params[0], - ), - ) - hoists.push( - createVariable( - emit, - T.tsAsExpression( - T.objectExpression([]), - T.tsTypeReference( - T.tsQualifiedName( - T.tsQualifiedName( - T.identifier(options.typeIdentifier), - T.identifier('internal'), - ), - T.identifier('EmitTypeToEmits'), - ), - - T.tsTypeParameterInstantiation([ - T.tsTypeReference(T.identifier(emitType)), - ]), - ), - ), - ), - ) - - node.typeParameters.params[0] = T.tsTypeReference( - T.identifier(emitType), - ) - - result.emitIdentifier = emit - } - - return node - } - - function transformExpose(node: T.CallExpression): T.CallExpression { - node = T.cloneNode(node, true) - - if (node.arguments.length > 0) { - if (T.isExpression(node.arguments[0])) { - hoists.push(createVariable(expose, node.arguments[0])) - node.arguments[0] = T.identifier(expose) - result.exposeIdentifier = expose - } - } else if (node.typeParameters?.params?.[0] != null) { - const exposeType = `${expose}Type` - hoists.push( - T.tsTypeAliasDeclaration( - T.identifier(exposeType), - null, - node.typeParameters.params[0], - ), - ) - - node.typeParameters.params[0] = T.tsTypeReference( - T.identifier(exposeType), - ) - // TODO: add expose for type-only usage - // result.exposeIdentifier = expose - } - - return node - } - }, -) - -export interface TransformScriptResult extends GeneratorResult { - exportIdentifier: string - selfName?: string - inheritAttrs?: boolean - identifiers: string[] -} - -export const transformScript = memoizeByFirstArg( - (ast: T.File, options: TransformScriptSetupOptions) => { - const result: TransformScriptResult = { - code: '', - map: null, - exportIdentifier: options.internalIdentifierPrefix + 'Script_Component', - identifiers: [], - } - - const defineComponent = - options.internalIdentifierPrefix + 'Script_defineComponent' - let exportDefaultDecl: T.ExportDefaultDeclaration | undefined - - ast.program.body.forEach((statement) => { - if (T.isExportDefaultDeclaration(statement)) { - exportDefaultDecl = statement - } else if (T.isExportNamedDeclaration(statement)) { - if (T.isVariableDeclaration(statement.declaration)) { - statement.declaration.declarations.forEach((decl) => { - if (T.isIdentifier(decl.id)) { - if (decl.id.name === 'inheritAttrs') { - if (T.isBooleanLiteral(decl.init)) { - result.inheritAttrs = decl.init.value - } - } else if (decl.id.name === 'name') { - if (T.isStringLiteral(decl.init)) { - result.selfName = decl.init.value - } - } - } - }) - } - } - }) - - const statements: T.Statement[] = [] - - let definition: T.CallExpression | undefined - if (exportDefaultDecl != null) { - if (T.isCallExpression(exportDefaultDecl.declaration)) { - definition = exportDefaultDecl.declaration - if (exportDefaultDecl.declaration.arguments.length > 0) { - const arg = first(exportDefaultDecl.declaration.arguments) - if (T.isObjectExpression(arg)) { - arg.properties.forEach((prop) => { - if (T.isObjectProperty(prop)) { - if (T.isIdentifier(prop.key)) { - if (prop.key.name === 'inheritAttrs') { - if (T.isBooleanLiteral(prop.value)) { - result.inheritAttrs = prop.value.value - } - } else if (prop.key.name === 'name') { - if (T.isStringLiteral(prop.value)) { - result.selfName = prop.value.value - } - } - } - } - }) - } - } - } else if (T.isExpression(exportDefaultDecl.declaration)) { - statements.push( - createNamedImport( - 'defineComponent', - defineComponent, - options.runtimeModuleName, - ), - ) - definition = T.callExpression(T.identifier(defineComponent), [ - exportDefaultDecl.declaration, - ]) - - if (T.isObjectExpression(exportDefaultDecl.declaration)) { - exportDefaultDecl.declaration.properties.forEach((prop) => { - if (T.isObjectProperty(prop)) { - if (T.isIdentifier(prop.key)) { - if (prop.key.name === 'inheritAttrs') { - if (T.isBooleanLiteral(prop.value)) { - result.inheritAttrs = prop.value.value - } - } else if (prop.key.name === 'name') { - if (T.isStringLiteral(prop.value)) { - result.selfName = prop.value.value - } - } - } - } - }) - } - } - } - - if (definition == null) { - statements.push( - createNamedImport( - 'defineComponent', - defineComponent, - options.runtimeModuleName, - ), - ) - definition = T.callExpression(T.identifier(defineComponent), [ - T.objectExpression([]), - ]) - - statements.push( - T.variableDeclaration('const', [ - T.variableDeclarator( - T.identifier(result.exportIdentifier), - definition, - ), - ]), - ) - } - - result.identifiers = findScopeBindings(ast.program.body) - - const output = generate([ - ...statements, - ...ast.program.body.map((statement) => - statement === exportDefaultDecl - ? T.variableDeclaration('const', [ - T.variableDeclarator( - T.identifier(result.exportIdentifier), - definition, - ), - ]) - : statement, - ), - ]) - - return { ...result, ...output } - }, -) - -function createNamedImport( - name: string, - localName: string, - source: string, -): T.ImportDeclaration { - return T.importDeclaration( - [T.importSpecifier(T.identifier(localName), T.identifier(name))], - T.stringLiteral(source), - ) -} - -function createVariable( - name: string | T.LVal, - init: T.Expression, -): T.VariableDeclaration { - return T.variableDeclaration('const', [ - T.variableDeclarator( - typeof name === 'string' ? T.identifier(name) : name, - init, - ), - ]) -} - -function isAsync(statements: T.Statement[]): boolean { - for (const statement of statements) { - if (T.isExpressionStatement(statement)) { - if (T.isAwaitExpression(statement.expression)) { - return true - } - } else if (T.isVariableDeclaration(statement)) { - for (const declaration of statement.declarations) { - if (T.isAwaitExpression(declaration.init)) { - return true - } - } - } - } - - return false -} diff --git a/packages/transforms/src/tsTransformScript.ts b/packages/transforms/src/tsTransformScript.ts new file mode 100644 index 00000000..3ca0f312 --- /dev/null +++ b/packages/transforms/src/tsTransformScript.ts @@ -0,0 +1,172 @@ +import { + DecodedSourceMap, + getComponentName, + invariant, + SourceTransformer, +} from '@vuedx/shared' +import type TypeScript from 'typescript/lib/tsserverlibrary' +import { TransformScriptOptions } from './TransformScriptOptions' +export interface TransformScriptResult { + code: string + map: DecodedSourceMap + identifiers: string[] + componentIdentifier: string + name: string + inheritAttrs: boolean +} + +export function transformScript( + source: string, + options: TransformScriptOptions, +): TransformScriptResult { + const key = `${options.fileName}:script:program` + const ts = options.lib + const inputFile = `input.${options.lang}` + const compilerHost: TypeScript.CompilerHost = { + fileExists: () => true, + getCanonicalFileName: (filename) => filename, + getCurrentDirectory: () => '', + getDefaultLibFileName: () => 'lib.d.ts', + getNewLine: () => '\n', + getSourceFile: (filename) => { + if (filename !== inputFile) return + + return ts.createSourceFile( + filename, + source, + ts.ScriptTarget.Latest, + true, + getScriptKind(options.lang), + ) + }, + readFile: () => undefined, + useCaseSensitiveFileNames: () => true, + writeFile: () => undefined, + } + + const program = ts.createProgram( + [inputFile], + { + noResolve: true, + target: ts.ScriptTarget.Latest, + jsx: options.lang.endsWith('x') ? ts.JsxEmit.Preserve : undefined, + }, + compilerHost, + options.cache?.get(key) as TypeScript.Program, + ) + options.cache?.set(key, program) + const sourceFile = program.getSourceFile(inputFile) + invariant(sourceFile != null, 'Source file not found.') + + let defaultExport: TypeScript.ExportAssignment | undefined + let inheritAttrs: boolean = true + let name: string = getComponentName(options.fileName) + + const vars = { + defineComponent: `${options.internalIdentifierPrefix}defineComponent`, + Component: `${options.internalIdentifierPrefix}_Script_Component`, + } + + const code = new SourceTransformer(inputFile, source) + + findNodes(sourceFile) + + const identifiers = program + .getTypeChecker() + .getSymbolsInScope(sourceFile, ts.SymbolFlags.Value) + .map((sym) => sym.getName()) + + if (defaultExport != null) { + const needsDefineComponent = ts.isObjectLiteralExpression( + defaultExport.expression, + ) + + code.clone(0, defaultExport.getFullStart()) + code.nextLine() + code.append(`const ${vars.Component} = `) + if (needsDefineComponent) { + code.append(`${vars.defineComponent}(`) + } + code.clone( + defaultExport.expression.getFullStart(), + defaultExport.expression.getEnd(), + ) + if (needsDefineComponent) { + code.append(');') + code.nextLine() + } + code.clone(defaultExport.expression.getEnd(), source.length) + } else { + code.nextLine() + code.clone(0, source.length) + code.nextLine() + code.append(`const ${vars.Component} = ${vars.defineComponent}({});`) + } + + code.nextLine() + + const result = code.end() + + return { + code: result.code, + map: result.map, + identifiers, + componentIdentifier: vars.Component, + name, + inheritAttrs, + } + + function getScriptKind( + lang: TransformScriptOptions['lang'], + ): TypeScript.ScriptKind { + switch (lang) { + case 'js': + return ts.ScriptKind.JS + case 'ts': + return ts.ScriptKind.TS + case 'tsx': + return ts.ScriptKind.TSX + case 'jsx': + return ts.ScriptKind.JSX + default: + throw new Error(`Unknown lang`) + } + } + + function findNodes(sourceFile: TypeScript.SourceFile): void { + sourceFile.statements.forEach((statement) => { + if (ts.isExportAssignment(statement)) { + defaultExport = statement + } + + if (ts.canHaveModifiers(statement)) { + const modifiers = ts.getModifiers(statement) + if ( + modifiers?.some( + (modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword, + ) === true + ) { + if (ts.isVariableStatement(statement)) { + statement.declarationList.declarations.forEach((declaration) => { + if (ts.isIdentifier(declaration.name)) { + if ( + declaration.name.text === 'name' && + declaration.initializer != null && + ts.isStringLiteral(declaration.initializer) + ) { + name = declaration.initializer.getText().slice(1, -1) + } else if ( + declaration.name.text === 'inheritAttrs' && + declaration.initializer != null + ) { + inheritAttrs = + declaration.initializer.kind === ts.SyntaxKind.TrueKeyword + } + } + }) + } + } + } + }) + } +} diff --git a/packages/transforms/src/tsTransformScriptSetup.ts b/packages/transforms/src/tsTransformScriptSetup.ts new file mode 100644 index 00000000..20a4d76e --- /dev/null +++ b/packages/transforms/src/tsTransformScriptSetup.ts @@ -0,0 +1,373 @@ +import { + DecodedSourceMap, + first, + invariant, + SourceTransformer, +} from '@vuedx/shared' +import type TypeScript from 'typescript/lib/tsserverlibrary' +import { TransformScriptOptions } from './TransformScriptOptions' +export interface TransformScriptSetupResult { + code: string + map: DecodedSourceMap + identifiers: string[] + propsIdentifier: string + emitsIdentifier: string + exposeIdentifier: string + scopeIdentifier: string + componentIdentifier: string + exports: Record +} + +export function transformScriptSetup( + source: string, + options: TransformScriptOptions, +): TransformScriptSetupResult { + const key = `${options.fileName}:scriptSetup:program` + const ts = options.lib + const inputFile = `input.${options.lang}` + const compilerHost: TypeScript.CompilerHost = { + fileExists: () => true, + getCanonicalFileName: (filename) => filename, + getCurrentDirectory: () => '', + getDefaultLibFileName: () => 'lib.d.ts', + getNewLine: () => '\n', + getSourceFile: (filename) => { + if (filename !== inputFile) return + + return ts.createSourceFile( + filename, + source, + ts.ScriptTarget.Latest, + true, + getScriptKind(options.lang), + ) + }, + readFile: () => undefined, + useCaseSensitiveFileNames: () => true, + writeFile: () => undefined, + } + + const program = ts.createProgram( + [inputFile], + { + noResolve: true, + target: ts.ScriptTarget.Latest, + jsx: options.lang.endsWith('x') ? ts.JsxEmit.Preserve : undefined, + }, + compilerHost, + options.cache?.get(key) as TypeScript.Program, + ) + options.cache?.set(key, program) + const sourceFile = program.getSourceFile(inputFile) + invariant(sourceFile != null, 'Source file not found.') + + let firstStatement: TypeScript.Statement | undefined + let internalPropsIdentifier: TypeScript.Identifier | undefined + let internalPropsInitializer: TypeScript.Expression | undefined + let propsIdentifier: TypeScript.Identifier | undefined + let propsType: TypeScript.TypeNode | undefined + let propsOptions: TypeScript.Node | undefined + let emitsType: TypeScript.Node | undefined + let emitsOptions: TypeScript.Node | undefined + let exposeOptions: TypeScript.Node | undefined + const exportedNodes: Array< + | TypeScript.ExportDeclaration + | TypeScript.TypeAliasDeclaration + | TypeScript.InterfaceDeclaration + | TypeScript.EnumDeclaration + > = [] + const exportedNames: Record = {} + + const vars = { + internalProps: `${options.internalIdentifierPrefix}_ScriptSetup_internalProps`, + scope: `${options.internalIdentifierPrefix}_ScriptSetup_scope`, + Component: `${options.internalIdentifierPrefix}_ScriptSetup_Component`, + emits: `${options.internalIdentifierPrefix}_ScriptSetup_emits`, + props: `${options.internalIdentifierPrefix}_ScriptSetup_props`, + expose: `${options.internalIdentifierPrefix}_ScriptSetup_expose`, + } + const code = new SourceTransformer(inputFile, source) + + findNodes(sourceFile) + + const identifiers = program + .getTypeChecker() + .getSymbolsInScope(sourceFile, ts.SymbolFlags.Value) + .map((sym) => sym.getName()) + + const offset = + firstStatement == null ? source.length : firstStatement.getFullStart() + + if (exportedNodes.length > 0) { + genExportedNodes(0, offset) + } else { + code.clone(0, offset) + } + // annotate range + code.append( + ` ;const ${vars.scope} = ${options.typeIdentifier}.internal.scope(async () => {`, + ) + + if (exportedNodes.length > 0) { + genExportedNodes(offset, source.length) + } else { + code.clone(offset, source.length) + } + + code.nextLine() + + if (exportedNodes.length > 0) { + for (const node of exportedNodes) { + if (ts.isExportDeclaration(node)) { + if (node.exportClause != null && ts.isNamedExports(node.exportClause)) { + for (const specifier of node.exportClause.elements) { + const name = (specifier.propertyName ?? specifier.name).getText() + const internalName = `${options.internalIdentifierPrefix}_export_${name}` + exportedNames[name] = internalName + // TODO: check if typeof is required or not. + code.append( + `const ${internalName} = null as unknown as typeof ${specifier.name.getText()};`, + ) + code.nextLine() + } + } + } else { + const name = node.name.getText() + const internalName = `${options.internalIdentifierPrefix}_export_${name}` + exportedNames[name] = internalName + code.append(`const ${internalName} = null as unknown as ${name};`) + code.nextLine() + } + } + } + + if (internalPropsIdentifier == null && internalPropsInitializer != null) { + code.clone(offset, internalPropsInitializer.getStart()) + code.append(`const ${vars.internalProps} = `) + code.clone( + internalPropsInitializer.getStart(), + internalPropsInitializer.getEnd(), + ) + code.append(';\n') + } + // define props + if (propsIdentifier != null) { + code.append(`const ${vars.props} = ${propsIdentifier.text};\n`) + } else if (propsType != null) { + code.append(`const ${vars.props} = defineProps<`) + code.clone(propsType.getStart(), propsType.getEnd()) + code.append(`>();\n`) + } else if (propsOptions != null) { + code.append(`const ${vars.props} = defineProps(`) + code.clone(propsOptions.getStart(), propsOptions.getEnd()) + code.append(`);\n`) + } else { + code.append(`const ${vars.props} = defineProps({});\n`) + } + + // define emits + if (emitsType != null) { + code.append( + `const ${vars.emits} = ({} as unknown as ${options.typeIdentifier}.internal.EmitTypeToEmits<`, + ) + code.clone(emitsType.getStart(), emitsType.getEnd()) + code.append(`>);\n`) + } else if (emitsOptions != null) { + code.append(`const ${vars.emits} = (`) + code.clone(emitsOptions.getStart(), emitsOptions.getEnd()) + code.append(`);\n`) + } else { + code.append(`const ${vars.emits} = ({});\n`) + } + + // define expose + if (exposeOptions != null) { + code.append(`const ${vars.expose} = (`) + code.clone(exposeOptions.getStart(), exposeOptions.getEnd()) + code.append(`);\n`) + } else { + code.append(`const ${vars.expose} = {};\n`) + } + + if (internalPropsIdentifier == null && internalPropsInitializer == null) { + code.append(`const ${vars.internalProps} = {};\n`) + } + + code.append( + [ + `const ${vars.Component} = ${ + options.internalIdentifierPrefix + }defineComponent((_: typeof ${ + internalPropsIdentifier?.getText() ?? vars.internalProps + })=> {});\n`, + ].join('\n'), + ) + + code.append(`\n`) + const result = code.end() + + return { + code: result.code, + map: result.map, + identifiers, + componentIdentifier: vars.Component, + propsIdentifier: vars.props, + emitsIdentifier: vars.emits, + exposeIdentifier: vars.expose, + scopeIdentifier: vars.scope, + exports: exportedNames, + } + + function genExportedNodes(start: number, end: number): void { + let cursor = start + for (const node of exportedNodes) { + const s = node.getStart() + const e = node.getEnd() + if (s > cursor && s < end) { + code.clone(cursor, s) + if (!ts.isExportDeclaration(node)) { + const modifier = getExportModifier(node) + if (modifier != null) { + code.clone(modifier.getEnd(), node.getEnd()) + } + } + cursor = e + } + } + + code.clone(cursor, end) + } + + function getExportModifier( + node: TypeScript.Node, + ): TypeScript.ExportKeyword | null { + if (!ts.canHaveModifiers(node)) return null + const modifier = node.modifiers?.find( + (modifier): modifier is ts.ExportKeyword => + modifier.kind === ts.SyntaxKind.ExportKeyword, + ) + if (modifier == null) return null + return modifier as TypeScript.ExportKeyword + } + + function getScriptKind( + lang: TransformScriptOptions['lang'], + ): TypeScript.ScriptKind { + switch (lang) { + case 'js': + return ts.ScriptKind.JS + case 'ts': + return ts.ScriptKind.TS + case 'tsx': + return ts.ScriptKind.TSX + case 'jsx': + return ts.ScriptKind.JSX + default: + throw new Error(`Unknown lang`) + } + } + + function findNodes(sourceFile: TypeScript.SourceFile): void { + sourceFile.statements.forEach((statement) => { + if (!ts.isImportDeclaration(statement)) { + if (firstStatement == null) { + firstStatement = statement + } + } + + if (ts.isVariableStatement(statement)) { + statement.declarationList.declarations.forEach((declaration) => { + if (declaration.initializer != null) { + if (isFnCall(declaration.initializer, 'defineProps')) { + if (ts.isIdentifier(declaration.name)) { + internalPropsIdentifier = declaration.name + propsIdentifier = declaration.name + } else { + internalPropsInitializer = declaration.initializer + } + } else if (isFnCall(declaration.initializer, 'withDefaults')) { + if (ts.isIdentifier(declaration.name)) { + internalPropsIdentifier = declaration.name + } else { + internalPropsInitializer = declaration.initializer + } + + const definePropsExp = declaration.initializer.arguments[0] + if (definePropsExp != null) { + if (isFnCall(definePropsExp, 'defineProps')) { + processProps(definePropsExp) + } + } + } else if (isFnCall(declaration.initializer, 'defineEmits')) { + processEmits(declaration.initializer) + } else if (isFnCall(declaration.initializer, 'defineExpose')) { + processExpose(declaration.initializer) + } + } + }) + } else if (ts.isExpressionStatement(statement)) { + if (isFnCall(statement.expression, 'defineProps')) { + internalPropsInitializer = statement.expression + processProps(statement.expression) + } else if (isFnCall(statement.expression, 'withDefaults')) { + internalPropsInitializer = statement.expression + const definePropsExp = statement.expression.arguments[0] + if (definePropsExp != null) { + if (isFnCall(definePropsExp, 'defineProps')) { + processProps(definePropsExp) + } + } + } else if (isFnCall(statement.expression, 'defineEmits')) { + processEmits(statement.expression) + } else if (isFnCall(statement.expression, 'defineExpose')) { + processExpose(statement.expression) + } + } else if (ts.isExportDeclaration(statement)) { + if (statement.isTypeOnly) exportedNodes.push(statement) + // TODO: support `export { type foo }` + } else if ( + ts.isTypeAliasDeclaration(statement) || + ts.isInterfaceDeclaration(statement) || + ts.isEnumDeclaration(statement) + ) { + const modifier = getExportModifier(statement) + if (modifier != null) { + exportedNodes.push(statement) + } + } + }) + + function isFnCall( + node: TypeScript.Node, + name: string, + ): node is TypeScript.CallExpression { + return ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.escapedText === name + ) + } + + function processProps(node: TypeScript.CallExpression): void { + if (node.typeArguments != null && node.typeArguments.length > 0) { + propsType = first(node.typeArguments) + } else if (node.arguments != null && node.arguments.length > 0) { + propsOptions = first(node.arguments) + } + } + + function processEmits(node: TypeScript.CallExpression): void { + if (node.typeArguments != null && node.typeArguments.length > 0) { + emitsType = first(node.typeArguments) + } else if (node.arguments != null && node.arguments.length > 0) { + emitsOptions = first(node.arguments) + } + } + + function processExpose(node: TypeScript.CallExpression): void { + if (node.arguments != null && node.arguments.length > 0) { + exposeOptions = first(node.arguments) + } + } + } +} diff --git a/packages/transforms/test/index.spec.ts b/packages/transforms/test/index.spec.ts deleted file mode 100644 index fc98ac4c..00000000 --- a/packages/transforms/test/index.spec.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { parse } from '@babel/parser' -import type * as T from '@babel/types' -import { findScopeBindings } from '../src' - -const getAST = (code: string) => - parse(code, { - sourceType: 'module', - plugins: ['typescript', 'importAssertions'], - errorRecovery: true, - }).program.body as unknown as T.Statement[] - -describe('findScopeBindings', () => { - test(`imports`, () => { - const ast = getAST( - ` - import { a } from 'a' - import { b as c } from 'b' - import * as d from 'd' - import { default as e } from 'e' - import f, { g } from 'f' - import h from 'h' - import type i from 'i' - import type { j } from 'j' - import k from 'k.css' assert { type: 'text/css' } - `, - ) - - expect(findScopeBindings(ast)).toMatchObject( - expect.arrayContaining(['a', 'c', 'd', 'e', 'f', 'g', 'h', 'k']), - ) - }) - - test(`declarations`, () => { - const ast = getAST( - ` - var a, b = {} - let c, d = {} - const e, f = {} - `, - ) - - expect(findScopeBindings(ast)).toMatchObject( - expect.arrayContaining(['a', 'b', 'c', 'd', 'e', 'f']), - ) - }) - - test(`destructure object`, () => { - const ast = getAST( - ` - const { - a, - b = a, - c: { d }, - e: [f], - g: { h: { i } }, - [a + d]: k, - ...j - } = {} - `, - ) - - expect(findScopeBindings(ast)).toMatchObject( - expect.arrayContaining(['a', 'b', 'd', 'f', 'i', 'j', 'k']), - ) - }) - - test(`destructure array`, () => { - const ast = getAST( - ` - const [ - a, - b = a, - { d }, - [f], - { h: { i } }, - ...j - ] = [] - `, - ) - - expect(findScopeBindings(ast)).toMatchObject( - expect.arrayContaining(['a', 'b', 'd', 'f', 'i', 'j']), - ) - }) - - test(`functions`, () => { - const ast = getAST( - ` - function a() {} - const b = function() {} - const c = () => {} - `, - ) - - expect(findScopeBindings(ast)).toMatchObject( - expect.arrayContaining(['a', 'b', 'c']), - ) - }) - test(`ignore types`, () => { - const ast = getAST( - ` - type a = {} - interface b {} - abstract class c {} - declare const d: string - declare function e(): void - enum f { } - const enum g { } - `, - ) - - expect(findScopeBindings(ast)).toMatchObject( - expect.arrayContaining(['c', 'd', 'e', 'f', 'g']), - ) - }) - - test(`ignore nested`, () => { - const ast = getAST( - ` - function a() { - const b = {} - } - `, - ) - - expect(findScopeBindings(ast)).toMatchObject(expect.arrayContaining(['a'])) - }) -}) diff --git a/packages/transforms/tsconfig.json b/packages/transforms/tsconfig.json index 564a5990..2a5cbb66 100644 --- a/packages/transforms/tsconfig.json +++ b/packages/transforms/tsconfig.json @@ -1,4 +1,4 @@ { "extends": "../../tsconfig.base.json", - "include": ["src"] + "include": ["src/index.ts"] } diff --git a/packages/vue-languageservice/src/features/DefinitionService.ts b/packages/vue-languageservice/src/features/DefinitionService.ts index 0fee351c..b061a207 100644 --- a/packages/vue-languageservice/src/features/DefinitionService.ts +++ b/packages/vue-languageservice/src/features/DefinitionService.ts @@ -178,20 +178,24 @@ export class DefinitionService this.ts.service .getDefinitionAtPosition( file.generatedFileName, - templateDeclaration.initializer.start, + templateDeclaration.initializer.start + 1, ) ?.flatMap((definition) => this.processDefinitionInfo(definition), ) ?? [] ) } else if (templateDeclaration.kind === 'identifier') { + const span = this.fs.getTextSpan( + file, + templateDeclaration.initializer, + ) return ( this.ts.service .getDefinitionAtPosition( file.generatedFileName, templateDeclaration.initializer.start + templateDeclaration.initializer.length - - 1, // check at the end of the line + (span.endsWith(');') ? 2 : 1), // check at the end of the line ) ?.flatMap((definition) => this.processDefinitionInfo(definition), diff --git a/packages/vue-languageservice/src/features/DiagnosticsService.ts b/packages/vue-languageservice/src/features/DiagnosticsService.ts index 63e803de..fbd59dc7 100644 --- a/packages/vue-languageservice/src/features/DiagnosticsService.ts +++ b/packages/vue-languageservice/src/features/DiagnosticsService.ts @@ -120,31 +120,6 @@ export class DiagnosticsService }) } - const setupVariables = new Map( - this.declarations - .getTemplateDeclaration(file.originalFileName) - .declarations.filter((declaration) => declaration.kind === 'setup') - .map((declaration) => [declaration.id, declaration]), - ) - - file.snapshot.unusedIdentifiers.forEach((identifier) => { - const declaration = setupVariables.get(identifier) - if (declaration == null || declaration.kind === 'component') return - if (declaration.references.length > 0) return - const span = file.findOriginalTextSpan(declaration.initializer) - if (span == null) return - const diagnostic: TypeScript.DiagnosticWithLocation = { - category: this.ts.lib.DiagnosticCategory.Suggestion, - code: 6133, - source: 'VueDX/Compiler', - file: fakeSourceFile, - ...span, - messageText: `'${declaration.id}' is declared but its value is never read.`, // TODO: localize. - reportsUnnecessary: true, - } - diagnostics.push(diagnostic) - }) - diagnostics.push( ...this.declarations .getUndefinedGlobals(fileName) diff --git a/packages/vue-languageservice/src/services/FilesystemService.ts b/packages/vue-languageservice/src/services/FilesystemService.ts index 6f0d64b8..b7b5c88d 100644 --- a/packages/vue-languageservice/src/services/FilesystemService.ts +++ b/packages/vue-languageservice/src/services/FilesystemService.ts @@ -108,6 +108,7 @@ export class FilesystemService implements Disposable { const file = VueSFCDocument.create(fileName, this.provider.read(fileName), { isTypeScript: this.ts.isTypeScriptProject, + typescript: this.ts.lib, }) const registerFileUpdate = ( diff --git a/packages/vue-languageservice/src/services/PluginSideChannel.ts b/packages/vue-languageservice/src/services/PluginSideChannel.ts index ce49c92b..1639e459 100644 --- a/packages/vue-languageservice/src/services/PluginSideChannel.ts +++ b/packages/vue-languageservice/src/services/PluginSideChannel.ts @@ -4,6 +4,7 @@ import { parseFileName, toFileName, } from '@vuedx/shared' +import { TextSpan } from '@vuedx/vue-virtual-textdocument' import { inject, injectable } from 'inversify' import { FilesystemService } from './FilesystemService' import { TypescriptContextService } from './TypescriptContextService' @@ -48,6 +49,24 @@ export class PluginSideChannel { return undefined } + public async findGeneratedTextSpan( + fileName: string, + textSpan: TextSpan, + ): Promise { + const file = this.fs.getVueFile(fileName) + if (file == null) return null + return file.findGeneratedTextSpan(textSpan) + } + + public async findOriginalTextSpan( + fileName: string, + textSpan: TextSpan, + ): Promise { + const file = this.fs.getVueFile(fileName) + if (file == null) return null + return file.findOriginalTextSpan(textSpan) + } + public async getRelatedVirtualFiles(fileName: string): Promise { const file = this.fs.getVueFile(fileName) if (file == null) return [] diff --git a/packages/vue-languageservice/src/services/TemplateDeclarationsService.ts b/packages/vue-languageservice/src/services/TemplateDeclarationsService.ts index bfe18e3f..e85efe5c 100644 --- a/packages/vue-languageservice/src/services/TemplateDeclarationsService.ts +++ b/packages/vue-languageservice/src/services/TemplateDeclarationsService.ts @@ -67,7 +67,10 @@ export class TemplateDeclarationsService { const file = this.fs.getVueFile(fileName) if (file == null) return null const declarations = this.getTemplateDeclaration(fileName) - const { line } = file.generated.positionAt(offset) + const { line, character } = file.generated.positionAt(offset) + this.logger.debug( + `Line(${line}:${character}): ${this.fs.getLineText(file, offset)}`, + ) return declarations.byLine.get(line) ?? null } @@ -83,9 +86,9 @@ export class TemplateDeclarationsService { const contents = file.getText() const ContextVariableRE = - /(?(?let )(?[A-Za-z$_][A-Za-z0-9$_]*) = (?:[^.]+)\.)(?\k)/ + /(?(?let )(?[A-Za-z$_][A-Za-z0-9$_]*) = )(?.*)/ const HoistedVariableRE = - /(?(?const )(?[A-Za-z$_][A-Za-z0-9$_]*) = (?.*))/ + /(?(?const )(?[A-Za-z$_][A-Za-z0-9$_]*) = )(?.*)/ findAnnotatedTextRanges( contents, annotations.templateGlobals.start, diff --git a/packages/vue-languageservice/types/3.x.d.ts b/packages/vue-languageservice/types/3.x.d.ts index c06dff39..fdbd4325 100644 --- a/packages/vue-languageservice/types/3.x.d.ts +++ b/packages/vue-languageservice/types/3.x.d.ts @@ -9,7 +9,7 @@ import { checkOnDirective, } from './shared/checkDirectiveOn' import { checkInterpolation } from './shared/checkInterpolation' -import { checkRef } from './shared/checkRef' +import { checkRef, unref } from './shared/checkRef' import { resolveComponent } from './shared/components' import { resolveDirective } from './shared/directives' @@ -18,14 +18,14 @@ import { MergeAttrs, PropsOf } from './shared/Props' import { renderList } from './shared/renderList' import { renderSlot } from './shared/renderSlot' import { Slots, SlotsFrom, GetSlotProps, checkSlots } from './shared/Slots' -import { first, flat, union, merge, getNameOption } from './shared/utils' +import { first, flat, union, merge, getNameOption, scope } from './shared/utils' import { EmitsToProps, EmitTypeToEmits } from './shared/emits' import {} from './shared/jsx' export type version = '3.x' export namespace internal { - export { first, flat, union, merge, getNameOption } + export { first, flat, union, merge, getNameOption, scope, unref } export { resolveComponent, resolveDirective, getElementType } export { renderList, renderSlot, Slots, GetSlotProps } export { defineComponent } diff --git a/packages/vue-languageservice/types/shared/checkRef.d.ts b/packages/vue-languageservice/types/shared/checkRef.d.ts index a8e08c2e..a8f331b0 100644 --- a/packages/vue-languageservice/types/shared/checkRef.d.ts +++ b/packages/vue-languageservice/types/shared/checkRef.d.ts @@ -1,4 +1,4 @@ -import type { Ref } from '@vue/runtime-core' +import type { Ref, unref } from '@vue/runtime-core' type RefValue = T extends (value: infer V) => unknown ? V : T @@ -6,3 +6,4 @@ export function checkRef( ref: T | ((value: T) => unknown) | null, element: RefValue, ): Ref +export { unref } diff --git a/packages/vue-languageservice/types/shared/utils.d.ts b/packages/vue-languageservice/types/shared/utils.d.ts index 362ee5a9..a7d28cf8 100644 --- a/packages/vue-languageservice/types/shared/utils.d.ts +++ b/packages/vue-languageservice/types/shared/utils.d.ts @@ -174,6 +174,8 @@ export function flat( depth?: D, ): Array> +export function scope(fn: () => Promise): T + export function first(items: T[]): T export function union(...args: T): TupleToUnion diff --git a/packages/vue-virtual-textdocument/src/VueSFCDocument.ts b/packages/vue-virtual-textdocument/src/VueSFCDocument.ts index 27b86bbb..000a7afa 100644 --- a/packages/vue-virtual-textdocument/src/VueSFCDocument.ts +++ b/packages/vue-virtual-textdocument/src/VueSFCDocument.ts @@ -290,7 +290,6 @@ export class VueSFCDocument implements TextDocument { ), mappingsByGeneratedOrder: [], mappingsByOriginalOrder: [], - unusedIdentifiers: [], } } } @@ -376,6 +375,7 @@ export class VueSFCDocument implements TextDocument { const prefixLength = findCommonPrefixLength(originalString, generatedString) if (generatedStart + prefixLength < spanInGeneratedText.start) return null // no mapping + // TODO: original position should be contained in a block return { start: originalStart + Math.abs(generatedStart - spanInGeneratedText.start), @@ -384,55 +384,17 @@ export class VueSFCDocument implements TextDocument { } public findGeneratedTextSpan(spanInOriginalText: TextSpan): TextSpan | null { - const position = this.original.positionAt(spanInOriginalText.start) - const low = this.findMapping( - 'original', - position, - BinarySearchBias.GREATEST_LOWER_BOUND, - ) + const start = this.generatedOffsetAt(spanInOriginalText.start) + if (start == null) return null + if (spanInOriginalText.length === 0) return { start, length: 0 } - if (low == null) return null - const result = this._processMappingUsingMeta( - 'original', - spanInOriginalText, - low, - ) - if (result != null) return result - - const originalStart = this.original.offsetAt({ - line: low[MappingKey.OriginalLine], - character: low[MappingKey.OriginalColumn], - }) - const start = - this.generated.offsetAt({ - line: low[MappingKey.GeneratedLine], - character: low[MappingKey.GeneratedColumn], - }) + - // source mappings are prefix based, so we assume the original - // and generated text have the same prefix. - Math.abs(originalStart - spanInOriginalText.start) - - const high = this.findMapping( - 'original', - position, - BinarySearchBias.LEAST_UPPER_BOUND, - ) - if (high != null) { - const end = this.generated.offsetAt({ - line: high[MappingKey.GeneratedLine], - character: high[MappingKey.GeneratedColumn], - }) - - return { - start, - length: Math.min(end - start, spanInOriginalText.length), - } - } + // TODO: collapse text span end to the end of the block + const end = + this.generatedOffsetAt( + spanInOriginalText.start + spanInOriginalText.length, + ) ?? start - return { - start, - length: spanInOriginalText.length, - } + return { start: Math.min(start, end), length: Math.abs(end - start) } } private _processMappingUsingMeta( @@ -598,15 +560,41 @@ export class VueSFCDocument implements TextDocument { } public generatedOffsetAt(offset: number): number | null { - const span = this.findGeneratedTextSpan({ start: offset, length: 1 }) - if (span == null) return null - return span.start + const position = this.original.positionAt(offset) + const low = this.findMapping( + 'original', + position, + BinarySearchBias.GREATEST_LOWER_BOUND, + ) + + if (low == null) return null + const result = this._processMappingUsingMeta( + 'original', + { start: offset, length: 0 }, + low, + ) + if (result != null) return result.start + + const originalStart = this.original.offsetAt({ + line: low[MappingKey.OriginalLine], + character: low[MappingKey.OriginalColumn], + }) + const start = + this.generated.offsetAt({ + line: low[MappingKey.GeneratedLine], + character: low[MappingKey.GeneratedColumn], + }) + + // source mappings are prefix based, so we assume the original + // and generated text have the same prefix. + Math.abs(originalStart - offset) + + return start } static create( fileName: string, content: string, - options: Omit = {}, + options: Omit, version: number = 0, ): VueSFCDocument { return new VueSFCDocument( diff --git a/packages/vue-virtual-textdocument/test/__snapshots__/sourcemap.spec.ts.snap b/packages/vue-virtual-textdocument/test/__snapshots__/sourcemap.spec.ts.snap index f4d12fe8..8a688404 100644 --- a/packages/vue-virtual-textdocument/test/__snapshots__/sourcemap.spec.ts.snap +++ b/packages/vue-virtual-textdocument/test/__snapshots__/sourcemap.spec.ts.snap @@ -2,18 +2,25 @@ exports[`sourcemaps attrs types 1`] = ` import * as __VueDX_TypeCheck from 'vuedx~runtime'; +import { defineComponent as __VueDX_defineComponent, GlobalComponents as __VueDX_GlobalComponents } from 'vue'; //#region