Skip to content

Commit

Permalink
feat: basic function
Browse files Browse the repository at this point in the history
  • Loading branch information
so1ve committed Aug 21, 2023
1 parent c1bea6d commit 96c499c
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 8 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ $ yarn add -D unplugin-vue-complex-types
$ pnpm add -D unplugin-vue-complex-types
```

## TODOs

- [ ] Improve performance (use cache and reuse typescript programs) - Very poor performance right now!
- [ ] Add more tests
- [ ] Allow receive a tsconfig

## 🚀 Usage

<details>
Expand Down
8 changes: 6 additions & 2 deletions src/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import type { Options } from "./types";

export default createUnplugin<Options | undefined>((_options) => ({
name: "unplugin-vue-complex-types",
transform(code) {
return transform(code);
transform(code, id) {
if (!id.endsWith(".vue")) {
return;
}

return transform(code, id);
},
}));
53 changes: 49 additions & 4 deletions src/core/transform.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,58 @@
import MagicString from "magic-string";
import type { TransformResult } from "unplugin";
import { parse } from "vue/compiler-sfc";
import { parse as parseSfc } from "vue/compiler-sfc";

export function transform(code: string): TransformResult {
import {
findDefinePropsCall,
getProgramAndSourceFile,
normalizePath,
printVueAcceptableTypeLiteralFromSymbols,
vueFilepathToVirtualFilepath,
} from "./utils";

function transformSetupCode(
code: string,
s: MagicString,
id: string,
offset: number,
) {
const { program, sourceFile } = getProgramAndSourceFile(
code,
vueFilepathToVirtualFilepath(id),
);
const typeChecker = program.getTypeChecker();
const definePropsCall = findDefinePropsCall(sourceFile);
if (!definePropsCall) {
return;
}
const typeArgument = definePropsCall.typeArguments![0];
const type = typeChecker.getTypeAtLocation(typeArgument);
const vueAcceptableTypeString = printVueAcceptableTypeLiteralFromSymbols(
type.getProperties(),
typeChecker,
);
s.overwrite(
offset + typeArgument.getStart(sourceFile),
offset + typeArgument.getEnd(),
vueAcceptableTypeString,
);
}

export function transform(code: string, id: string): TransformResult {
const s = new MagicString(code);
const parsed = parse(code);
if (parsed.descriptor.scriptSetup?.lang !== "ts") {
const parsed = parseSfc(code);
if (
!parsed.descriptor.scriptSetup ||
parsed.descriptor.scriptSetup.lang !== "ts"
) {
return;
}

const start = parsed.descriptor.scriptSetup.loc.start.offset;
const end = parsed.descriptor.scriptSetup.loc.end.offset;
const scriptSetupCode = code.slice(start, end);
transformSetupCode(scriptSetupCode, s, normalizePath(id), start);

return {
code: s.toString(),
map: s.generateMap({ hires: true }),
Expand Down
79 changes: 79 additions & 0 deletions src/core/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import * as ts from "typescript";

export const normalizePath = (id: string) => id.replace(/\\/g, "/");

export const vueFilepathToVirtualFilepath = (filepath: string) =>
filepath.replace(/\.vue$/, ".vue-complex-types.vue.ts");

export function getProgramAndSourceFile(code: string, id: string) {
const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.Latest);
const defaultCompilerHost = ts.createCompilerHost(
// TODO: receive a tsconfig
{},
);
const customCompilerHost: ts.CompilerHost = {
...defaultCompilerHost,
getSourceFile: (name, languageVersion) => {
if (vueFilepathToVirtualFilepath(name) === id) {
return sourceFile;
}

return defaultCompilerHost.getSourceFile(name, languageVersion);
},
};
const program = ts.createProgram([id], {}, customCompilerHost);

return { program, sourceFile };
}

const vueAcceptableTypes = ["string", "number", "boolean", "bigint", "symbol"];
function toVueAcceptableType(typeString: string) {
if (vueAcceptableTypes.includes(typeString)) {
return typeString;
}
if (typeString.endsWith("[]")) {
return "any[]";
}

return "object";
}

export function printVueAcceptableTypeLiteralFromSymbols(
symbols: ts.Symbol[],
typeChecker: ts.TypeChecker,
) {
const codes = ["{"];
function push(code: string) {
codes.push(` ${code}`);
}
for (const symbol of symbols) {
for (const decl of symbol.getDeclarations() ?? []) {
if (ts.isPropertySignature(decl)) {
const type = typeChecker.getTypeAtLocation(decl);
const typeString = typeChecker.typeToString(type);
push(`${decl.name.getText()}: ${toVueAcceptableType(typeString)};`);
}
}
}
codes.push("}");

return codes.join("\n");
}

export function findDefinePropsCall(node: ts.Node) {
let definePropsCall: ts.CallExpression | undefined;
function traverse(node: ts.Node) {
if (
ts.isCallExpression(node) &&
node.expression.getText() === "defineProps" &&
node.typeArguments
) {
definePropsCall = node;
}

node.forEachChild(traverse);
}
traverse(node);

return definePropsCall;
}
9 changes: 9 additions & 0 deletions test/__fixtures__/a.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<script setup lang="ts">
import type { A } from "./foo";
defineProps<
{
msg: string;
} & A
>();
</script>
11 changes: 11 additions & 0 deletions test/__fixtures__/foo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export interface A {
foo: B;
bar: C;
baz: D;
}

interface B {}
type C = number extends boolean ? string : number;
interface D {
aaa: 1;
}
29 changes: 29 additions & 0 deletions test/__snapshots__/index.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html

exports[`should > exported 1`] = `
{
"code": "<script setup lang=\\"ts\\">
import type { A } from \\"./foo\\";
defineProps<
{
msg: string;
foo: object;
bar: number;
baz: object;
}
>();
</script>
",
"map": SourceMap {
"file": undefined,
"mappings": "AAAA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACxB,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAC/B;AACA,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACZ,CAAC;;;;;CAEK;AACN,CAAC,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;",
"names": [],
"sources": [
"",
],
"sourcesContent": undefined,
"version": 3,
},
}
`;
9 changes: 8 additions & 1 deletion test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { join } from "node:path";

import { describe, expect, it } from "vitest";

import { transform } from "../src/core/transform";
import AVue from "./__fixtures__/a.vue?raw";

describe("should", () => {
it("exported", () => {
expect(1).toBe(1);
expect(
transform(AVue, join(__dirname, "__fixtures__", "a.vue")),
).toMatchSnapshot();
});
});
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"strictNullChecks": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"skipDefaultLibCheck": true
"skipDefaultLibCheck": true,
"types": ["vite/client"]
}
}

0 comments on commit 96c499c

Please sign in to comment.