diff --git a/packages/vite-plugin-doctest/package.json b/packages/vite-plugin-doctest/package.json index 43f86e0..48a1df2 100644 --- a/packages/vite-plugin-doctest/package.json +++ b/packages/vite-plugin-doctest/package.json @@ -41,14 +41,13 @@ "devDependencies": { "remark-parse": "^10.0.2", "tsup": "^7.2.0", - "typescript": "^5.2.2", "unified": "^10.1.2", "vite": "^4.4.9", "vitest": "^0.34.0" }, "dependencies": { - "@babel/parser": "^7.22.16", "magic-string": "^0.30.3", + "typescript": "^4.0.0 || ^5.0.0", "vite": "^4.0.0 || ^5.0.0-0" } } diff --git a/packages/vite-plugin-doctest/src/transformers/typescript.ts b/packages/vite-plugin-doctest/src/transformers/typescript.ts index bba70d5..e9b95c9 100644 --- a/packages/vite-plugin-doctest/src/transformers/typescript.ts +++ b/packages/vite-plugin-doctest/src/transformers/typescript.ts @@ -1,17 +1,15 @@ -import { parse } from "@babel/parser"; import MagicString from "magic-string"; -import { extractCode, extractTestComment, vitestExports } from "./utils"; +import typescript from "typescript"; +import { extractCode, vitestExports } from "./utils"; export function transform(code: string, id: string) { - const comments = - parse(code, { - plugins: id.match(/\.[cm]?tsx?$/) ? ["typescript"] : [], - sourceType: "module", - }).comments ?? []; - const tests = comments - .filter((c) => c.type === "CommentBlock" && c.value.startsWith("*")) - .flatMap((c) => extractTestComment(c.value)) - .flatMap(extractCode); + const node = typescript.createSourceFile( + id, + code, + typescript.ScriptTarget.ESNext, + ); + const jsDocs = findJSDoc(node); + const tests = jsDocs.flatMap(extractTestComment).flatMap(extractCode); if (tests.length === 0) return code; const s = new MagicString(code); @@ -36,3 +34,35 @@ export function transform(code: string, id: string) { map: s.generateMap({ hires: true, source: id }), }; } + +function findJSDoc(node: typescript.Node): typescript.JSDoc[] { + const nodes: typescript.JSDoc[] = []; + // HACK: typescript.Node has no jsDoc property + if ("jsDoc" in node && Array.isArray(node.jsDoc) && node.jsDoc.length > 0) { + nodes.push( + ...node.jsDoc.flatMap((doc) => (typescript.isJSDoc(doc) ? [doc] : [])), + ); + } + node.forEachChild((child) => nodes.push(...findJSDoc(child))); + return nodes; +} + +function extractTestComment(jsDoc: typescript.JSDoc): string[] { + return ( + jsDoc.tags?.flatMap((tag) => { + if (tag.comment == null) return []; + const commentText = + typeof tag.comment === "string" + ? tag.comment + : tag.comment.map((c) => c.text).join("\n"); + // tagName should be `import.meta.vitest`. But it splits into `import` and `.meta.vitest`. + if ( + tag.tagName.text === "import" && + commentText.startsWith(".meta.vitest") + ) { + return [commentText]; + } + return []; + }) ?? [] + ); +} diff --git a/packages/vite-plugin-doctest/src/transformers/utils.ts b/packages/vite-plugin-doctest/src/transformers/utils.ts index 9286bb8..0d03797 100644 --- a/packages/vite-plugin-doctest/src/transformers/utils.ts +++ b/packages/vite-plugin-doctest/src/transformers/utils.ts @@ -24,14 +24,6 @@ export const vitestExports = [ "vitest", ]; -export function extractTestComment(jsDoc: string): string[] { - const testTagMatcher = /^import\.meta\.vitest\b/; - if (!jsDoc.startsWith("*")) return []; - const tags = jsDoc.replace(/^[ \t]*\*[ \t]*/gm, "").split(/\n@/g).slice(1); - const testTags = tags.filter((tag) => tag.match(testTagMatcher)); - return testTags.map((tagComment) => tagComment.replace(testTagMatcher, "")); -} - type TestCode = { name?: string; code: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3dcf457..624192b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,12 +20,12 @@ importers: packages/vite-plugin-doctest: dependencies: - '@babel/parser': - specifier: ^7.22.16 - version: 7.22.16 magic-string: specifier: ^0.30.3 version: 0.30.3 + typescript: + specifier: ^4.0.0 || ^5.0.0 + version: 5.2.2 vite: specifier: ^4.0.0 || ^5.0.0-0 version: 4.4.9(@types/node@20.6.0) @@ -36,15 +36,12 @@ importers: tsup: specifier: ^7.2.0 version: 7.2.0(typescript@5.2.2) - typescript: - specifier: ^5.2.2 - version: 5.2.2 unified: specifier: ^10.1.2 version: 10.1.2 vitest: specifier: ^0.34.0 - version: 0.34.4(@vitest/ui@0.34.4) + version: 0.34.4 test: devDependencies: @@ -75,14 +72,10 @@ packages: chalk: 2.4.2 dev: true - /@babel/helper-string-parser@7.22.5: - resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==} - engines: {node: '>=6.9.0'} - dev: false - /@babel/helper-validator-identifier@7.22.15: resolution: {integrity: sha512-4E/F9IIEi8WR94324mbDUMo074YTheJmd7eZF5vITTeYchqAi6sYXRLHUVsmkdmY4QjfKTcB2jB7dVP3NaBElQ==} engines: {node: '>=6.9.0'} + dev: true /@babel/highlight@7.22.13: resolution: {integrity: sha512-C/BaXcnnvBCmHTpz/VGZ8jgtE2aYlW4hxDhseJAWZb7gqGM/qtCK6iZUb0TyKFf7BOUsBH7Q7fkRsDRhg1XklQ==} @@ -93,14 +86,6 @@ packages: js-tokens: 4.0.0 dev: true - /@babel/parser@7.22.16: - resolution: {integrity: sha512-+gPfKv8UWeKKeJTUxe59+OobVcrYHETCsORl61EmSkmgymguYk/X5bp7GuUIXaFsc6y++v8ZxPsLSSuujqDphA==} - engines: {node: '>=6.0.0'} - hasBin: true - dependencies: - '@babel/types': 7.22.17 - dev: false - /@babel/runtime@7.22.15: resolution: {integrity: sha512-T0O+aa+4w0u06iNmapipJXMV4HoUir03hpx3/YqXXhu9xim3w+dVphjFWl1OH8NbZHw5Lbm9k45drDkgq2VNNA==} engines: {node: '>=6.9.0'} @@ -108,15 +93,6 @@ packages: regenerator-runtime: 0.14.0 dev: true - /@babel/types@7.22.17: - resolution: {integrity: sha512-YSQPHLFtQNE5xN9tHuZnzu8vPr61wVTBZdfv1meex1NBosa4iT05k/Jw06ddJugi4bk7The/oSwQGFcksmEJQg==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/helper-string-parser': 7.22.5 - '@babel/helper-validator-identifier': 7.22.15 - to-fast-properties: 2.0.0 - dev: false - /@biomejs/biome@1.1.2: resolution: {integrity: sha512-JEVWchqo0Xhl86IJgOh0xESWnNRUXBUDByCBR8TA4lIPzm/6U6Tv77+MblNkZ8MvwCtP6PlBNGdQcGKKabtuHA==} engines: {node: '>=14.*'} @@ -3134,11 +3110,6 @@ packages: os-tmpdir: 1.0.2 dev: true - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - dev: false - /to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3291,7 +3262,6 @@ packages: resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==} engines: {node: '>=14.17'} hasBin: true - dev: true /ufo@1.3.0: resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==} @@ -3454,6 +3424,71 @@ packages: optionalDependencies: fsevents: 2.3.3 + /vitest@0.34.4: + resolution: {integrity: sha512-SE/laOsB6995QlbSE6BtkpXDeVNLJc1u2LHRG/OpnN4RsRzM3GQm4nm3PQCK5OBtrsUqnhzLdnT7se3aeNGdlw==} + engines: {node: '>=v14.18.0'} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@vitest/browser': '*' + '@vitest/ui': '*' + happy-dom: '*' + jsdom: '*' + playwright: '*' + safaridriver: '*' + webdriverio: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + playwright: + optional: true + safaridriver: + optional: true + webdriverio: + optional: true + dependencies: + '@types/chai': 4.3.6 + '@types/chai-subset': 1.3.3 + '@types/node': 20.6.0 + '@vitest/expect': 0.34.4 + '@vitest/runner': 0.34.4 + '@vitest/snapshot': 0.34.4 + '@vitest/spy': 0.34.4 + '@vitest/utils': 0.34.4 + acorn: 8.10.0 + acorn-walk: 8.2.0 + cac: 6.7.14 + chai: 4.3.8 + debug: 4.3.4 + local-pkg: 0.4.3 + magic-string: 0.30.3 + pathe: 1.1.1 + picocolors: 1.0.0 + std-env: 3.4.3 + strip-literal: 1.3.0 + tinybench: 2.5.0 + tinypool: 0.7.0 + vite: 4.4.9(@types/node@20.6.0) + vite-node: 0.34.4(@types/node@20.6.0) + why-is-node-running: 2.2.2 + transitivePeerDependencies: + - less + - lightningcss + - sass + - stylus + - sugarss + - supports-color + - terser + dev: true + /vitest@0.34.4(@vitest/ui@0.34.4): resolution: {integrity: sha512-SE/laOsB6995QlbSE6BtkpXDeVNLJc1u2LHRG/OpnN4RsRzM3GQm4nm3PQCK5OBtrsUqnhzLdnT7se3aeNGdlw==} engines: {node: '>=v14.18.0'}