Skip to content

Commit

Permalink
feat: [wip] defineEmits support
Browse files Browse the repository at this point in the history
  • Loading branch information
so1ve committed Nov 7, 2023
1 parent c825439 commit 2de2ea2
Show file tree
Hide file tree
Showing 16 changed files with 162 additions and 34 deletions.
57 changes: 42 additions & 15 deletions src/core/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,19 @@ function baseCreateLanguageWorker(
const program = tsLs.getProgram()!;
const typeChecker = program.getTypeChecker();

function findScriptRangesNode(
function findScriptRangesAndNode(
filepath: string,
filter: (ranges: vue.ScriptSetupRanges) => vue.TextRange | undefined,
): { node: ts.Node; range: vue.TextRange } | undefined {
):
| {
virtualFileNode: ts.Node;
scriptNode: ts.Node;
setupRange: vue.TextRange;
scriptSetupAst: ts.SourceFile;
virtualFileAst: ts.SourceFile;
offset: number;
}
| undefined {
filepath = normalizePath(filepath);
const sourceFile = core.virtualFiles.getSource(filepath)?.root;
if (!(sourceFile instanceof vue.VueFile)) {
Expand All @@ -128,7 +137,7 @@ function baseCreateLanguageWorker(
}
const {
ast: scriptSetupAst,
startTagEnd,
startTagEnd: offset,
lang,
} = sourceFile.sfc.scriptSetup;
if (lang !== "ts" && lang !== "tsx") {
Expand All @@ -137,8 +146,8 @@ function baseCreateLanguageWorker(
// We get volar's generated virtual ts file because `scriptSetupAst`
// is the ast of `<script setup>` block, which is not in the program
// and cannot be used by type checker.
const virtualTsFile = program.getSourceFile(`${filepath}.${lang}`);
if (!virtualTsFile) {
const virtualFileAst = program.getSourceFile(`${filepath}.${lang}`);
if (!virtualFileAst) {
return;
}
const scriptSetupRanges = vue.parseScriptSetupRanges(
Expand All @@ -148,36 +157,54 @@ function baseCreateLanguageWorker(
);
const virtualTsFileScriptSetupRanges = vue.parseScriptSetupRanges(
ts,
virtualTsFile,
virtualFileAst,
vueCompilerOptions,
);
const sourceRange = filter(scriptSetupRanges);
const virtualTsRange = filter(virtualTsFileScriptSetupRanges);
if (!sourceRange || !virtualTsRange) {
return;
}
let foundNode!: ts.Node;
virtualTsFile.forEachChild(function traverse(node: ts.Node) {
let foundVirtualFileNode!: ts.Node;
virtualFileAst.forEachChild(function traverse(node: ts.Node) {
if (
node.getStart(virtualTsFile) === virtualTsRange.start &&
node.getStart(virtualFileAst) === virtualTsRange.start &&
node.getEnd() === virtualTsRange.end
) {
foundNode = node;
foundVirtualFileNode = node;
} else {
node.forEachChild(traverse);
}
});
let foundSetupNode!: ts.Node;
scriptSetupAst.forEachChild(function traverse(node: ts.Node) {
if (
node.getStart(scriptSetupAst) === sourceRange.start &&
node.getEnd() === sourceRange.end
) {
foundSetupNode = node;
} else {
node.forEachChild(traverse);
}
});

const range: vue.TextRange = {
start: startTagEnd + sourceRange.start,
end: startTagEnd + sourceRange.end,
const setupRange: vue.TextRange = {
start: offset + sourceRange.start,
end: offset + sourceRange.end,
};

return { node: foundNode, range };
return {
virtualFileNode: foundVirtualFileNode,
scriptNode: foundSetupNode,
setupRange,
scriptSetupAst,
virtualFileAst,
offset,
};
}

return {
findScriptRangesNode,
findScriptRangesNode: findScriptRangesAndNode,
__internal__: {
tsLs,
program,
Expand Down
6 changes: 3 additions & 3 deletions src/core/printer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ export class Printer {
}

private printIntersectionTypeNode(node: ts.IntersectionTypeNode): string {
return node.types.map((t) => this.print(t)).join(" & ");
return node.types.map((t) => this.printTypeArg(t)).join(" & ");
}

private printUnionTypeNode(node: ts.UnionTypeNode): string {
return node.types.map((t) => this.print(t)).join(" | ");
return node.types.map((t) => this.printTypeArg(t)).join(" | ");
}

private printTypeLiteralNode(node: ts.TypeLiteralNode): string {
Expand Down Expand Up @@ -75,7 +75,7 @@ export class Printer {
return parts.join("\n");
}

public print(node: ts.Node): string {
public printTypeArg(node: ts.Node): string {
// Intersection and Union
if (ts.isIntersectionTypeNode(node)) {
return this.printIntersectionTypeNode(node);
Expand Down
76 changes: 70 additions & 6 deletions src/core/transform.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
import MagicString from "magic-string";
import ts from "typescript";
import type { TransformResult } from "unplugin";

import { getLanguage } from "./language";
import { Printer } from "./printer";

export function transform(code: string, id: string): TransformResult {
function transformDefineProps(printer: Printer, s: MagicString, id: string) {
const language = getLanguage();
const s = new MagicString(code);
const typeChecker = language.__internal__.typeChecker;
const printer = new Printer(typeChecker);
const definePropsTypeArg = language.findScriptRangesNode(
id,
(scriptSetupRanges) => scriptSetupRanges.props.define?.typeArg,
Expand All @@ -18,11 +16,77 @@ export function transform(code: string, id: string): TransformResult {
return;
}

const { node: typeArgNode, range: typeArgRange } = definePropsTypeArg;
const { virtualFileNode: typeArgNode, setupRange: typeArgRange } =
definePropsTypeArg;

const printedType = printer.print(typeArgNode);
const printedType = printer.printTypeArg(typeArgNode);

s.overwrite(typeArgRange.start, typeArgRange.end, printedType);
}

function transformDefineEmits(printer: Printer, s: MagicString, id: string) {
const language = getLanguage();
const defineEmits = language.findScriptRangesNode(
id,
(scriptSetupRanges) => scriptSetupRanges.emits.define,
);
if (!defineEmits) {
return;
}

const {
scriptNode: defineEmitsNode,
virtualFileNode: virtualFileDefineEmitsNode,
scriptSetupAst,
offset,
} = defineEmits;

// TODO: refactor when https://github.com/vuejs/language-tools/pull/3710 is merged
const defineEmitsTypeArg =
virtualFileDefineEmitsNode &&
ts.isCallExpression(virtualFileDefineEmitsNode) &&
virtualFileDefineEmitsNode.typeArguments?.[0];

if (!defineEmitsTypeArg) {
return;
}

const tokens = defineEmitsNode.getChildren(scriptSetupAst);

// defineEmits< Arg >()
// ^ ^
const lessThanToken = tokens.find(
(t) => t.kind === ts.SyntaxKind.LessThanToken,
);
const greaterThanToken = tokens.find(
(t) => t.kind === ts.SyntaxKind.GreaterThanToken,
);
// defineEmits< Arg >()
// ^
const openParenToken = tokens.find(
(t) => t.kind === ts.SyntaxKind.OpenParenToken,
);

if (!lessThanToken || !greaterThanToken || !openParenToken) {
return;
}

const defineEmitsTypeArgRange = [
offset + lessThanToken.pos,
offset + greaterThanToken.end,
] as const;

// Remove the type argument
s.remove(...defineEmitsTypeArgRange);
}

export function transform(code: string, id: string): TransformResult {
const s = new MagicString(code);
const language = getLanguage();
const typeChecker = language.__internal__.typeChecker;
const printer = new Printer(typeChecker);
transformDefineProps(printer, s, id);
transformDefineEmits(printer, s, id);

return {
code: s.toString(),
Expand Down
4 changes: 4 additions & 0 deletions test/__fixtures__/defineEmits/basic-ts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Events {
(event: "foo"): void;
(event: 1 extends 2 ? "bar" : "baz"): void;
}
5 changes: 5 additions & 0 deletions test/__fixtures__/defineEmits/basic.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<script setup lang="ts">
import type { Events } from "./basic-ts";
defineEmits<Events>();
</script>
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
27 changes: 23 additions & 4 deletions test/__snapshots__/fixtures-compiled.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,25 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`fixtures compiled > __fixtures__/basic.vue 1`] = `
exports[`fixtures compiled > __fixtures__/defineEmits/basic.vue 1`] = `
"import { defineComponent } from 'vue';
import _export_sfc from '[NULL]/plugin-vue/export-helper';
\\"use strict\\";
var _sfc_main = /* @__PURE__ */ defineComponent({
__name: \\"basic\\",
setup(__props) {
return () => {
};
}
});
var basic = /* @__PURE__ */ _export_sfc(_sfc_main, [__FILE__]);
export { basic as default };
"
`;

exports[`fixtures compiled > __fixtures__/defineProps/basic.vue 1`] = `
"import { defineComponent } from 'vue';
import _export_sfc from '[NULL]/plugin-vue/export-helper';
Expand Down Expand Up @@ -36,7 +55,7 @@ export { basic as default };
"
`;

exports[`fixtures compiled > __fixtures__/core#8286.vue 1`] = `
exports[`fixtures compiled > __fixtures__/defineProps/core#8286.vue 1`] = `
"import { defineComponent } from 'vue';
import _export_sfc from '[NULL]/plugin-vue/export-helper';
Expand Down Expand Up @@ -239,7 +258,7 @@ export { core_8286 as default };
"
`;

exports[`fixtures compiled > __fixtures__/core#8468.vue 1`] = `
exports[`fixtures compiled > __fixtures__/defineProps/core#8468.vue 1`] = `
"import { defineComponent } from 'vue';
import _export_sfc from '[NULL]/plugin-vue/export-helper';
Expand All @@ -262,7 +281,7 @@ export { core_8468 as default };
"
`;

exports[`fixtures compiled > __fixtures__/import-from-vue.vue 1`] = `
exports[`fixtures compiled > __fixtures__/defineProps/import-from-vue.vue 1`] = `
"import { defineComponent } from 'vue';
import _export_sfc from '[NULL]/plugin-vue/export-helper';
Expand Down
17 changes: 13 additions & 4 deletions test/__snapshots__/fixtures.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`fixtures > __fixtures__/basic.vue 1`] = `
exports[`fixtures > __fixtures__/defineEmits/basic.vue 1`] = `
"<script setup lang=\\"ts\\">
import type { Events } from \\"./basic-ts\\";
defineEmits();
</script>
"
`;
exports[`fixtures > __fixtures__/defineProps/basic.vue 1`] = `
"<script
setup
lang=\\"ts\\"
Expand Down Expand Up @@ -36,7 +45,7 @@ typeLiteral: TypeLiteral
"
`;
exports[`fixtures > __fixtures__/core#8286.vue 1`] = `
exports[`fixtures > __fixtures__/defineProps/core#8286.vue 1`] = `
"<script setup lang=\\"ts\\">
import { ButtonHTMLAttributes } from \\"vue\\";
Expand Down Expand Up @@ -230,7 +239,7 @@ onTransitionstart: any
"
`;
exports[`fixtures > __fixtures__/core#8468.vue 1`] = `
exports[`fixtures > __fixtures__/defineProps/core#8468.vue 1`] = `
"<script setup lang=\\"ts\\" generic=\\"P extends Inputs\\">
import type { Inputs } from \\"./core#8468-props.ts\\";
Expand All @@ -242,7 +251,7 @@ value: string | number
"
`;
exports[`fixtures > __fixtures__/import-from-vue.vue 1`] = `
exports[`fixtures > __fixtures__/defineProps/import-from-vue.vue 1`] = `
"<script setup lang=\\"ts\\" generic=\\"T extends string\\">
import type { Foo } from \\"./import-from-vue-vue.exclude.vue\\";
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures-compiled.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import VueComplexTypes from "../src/rollup";

describe("fixtures compiled", async () => {
await testFixtures(
["__fixtures__/*.vue", "!__fixtures__/*.exclude.vue"],
["__fixtures__/**/*.vue", "!__fixtures__/**/*.exclude.vue"],
(_args, id) =>
rollupBuild(id, [
VueComplexTypes({
Expand Down
2 changes: 1 addition & 1 deletion test/fixtures.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ beforeAll(() => {

describe("fixtures", async () => {
await testFixtures(
["__fixtures__/*.vue", "!__fixtures__/*.exclude.vue"],
["__fixtures__/**/*.vue", "!__fixtures__/**/*.exclude.vue"],
async (_args, id) => {
const text = await readFile(id, { encoding: "utf-8" });

Expand Down

0 comments on commit 2de2ea2

Please sign in to comment.