From a48974dc6f29b9e2837db750154aec2b93c7f502 Mon Sep 17 00:00:00 2001 From: Daniel Roe Date: Fri, 10 Mar 2023 13:11:01 +0000 Subject: [PATCH] feat: `findTypeExports ` for finding type exports (#156) --- src/analyze.ts | 109 ++++++++++++++++++++++++++++++++++--------- test/exports.test.ts | 80 ++++++++++++++++++++++++++++++- 2 files changed, 165 insertions(+), 24 deletions(-) diff --git a/src/analyze.ts b/src/analyze.ts index eec5b10..9aeb0f0 100644 --- a/src/analyze.ts +++ b/src/analyze.ts @@ -62,8 +62,12 @@ export const DYNAMIC_IMPORT_RE = export const EXPORT_DECAL_RE = /\bexport\s+(?(async function|function|let|const enum|const|enum|var|class))\s+(?[\w$]+)/g; +export const EXPORT_DECAL_TYPE_RE = + /\bexport\s+(?(interface|type|declare (async function|function|let|const enum|const|enum|var|class)))\s+(?[\w$]+)/g; const EXPORT_NAMED_RE = /\bexport\s+{(?[^}]+?)[\s,]*}(\s*from\s*["']\s*(?(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][^\n;]*)?/g; +const EXPORT_NAMED_TYPE_RE = + /\bexport\s+type\s+{(?[^}]+?)[\s,]*}(\s*from\s*["']\s*(?(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][^\n;]*)?/g; const EXPORT_NAMED_DESTRUCT = /\bexport\s+(let|var|const)\s+(?:{(?[^}]+?)[\s,]*}|\[(?[^\]]+?)[\s,]*])\s+=/gm; const EXPORT_STAR_RE = @@ -117,16 +121,11 @@ export function findExports(code: string): ESMExport[] { }); // Find named exports - const namedExports: NamedExport[] = matchAll(EXPORT_NAMED_RE, code, { - type: "named", - }); - for (const namedExport of namedExports) { - namedExport.names = namedExport.exports - .replace(/^\r?\n?/, "") - .split(/\s*,\s*/g) - .filter((name) => !TYPE_RE.test(name)) - .map((name) => name.replace(/^.*?\sas\s/, "").trim()); - } + const namedExports: NamedExport[] = normalizeNamedExports( + matchAll(EXPORT_NAMED_RE, code, { + type: "named", + }) + ); const destructuredExports: NamedExport[] = matchAll( EXPORT_NAMED_DESTRUCT, @@ -161,26 +160,63 @@ export function findExports(code: string): ESMExport[] { // Merge and normalize exports // eslint-disable-next-line unicorn/no-array-push-push - const exports: ESMExport[] = [ + const exports: ESMExport[] = normalizeExports([ ...declaredExports, ...namedExports, ...destructuredExports, ...defaultExport, ...starExports, - ]; - for (const exp of exports) { - if (!exp.name && exp.names && exp.names.length === 1) { - exp.name = exp.names[0]; - } - if (exp.name === "default" && exp.type !== "default") { - exp._type = exp.type; - exp.type = "default"; - } - if (!exp.names && exp.name) { - exp.names = [exp.name]; - } + ]); + + // Return early when there is no export statement + if (exports.length === 0) { + return []; + } + const exportLocations = _tryGetExportLocations(code); + if (exportLocations && exportLocations.length === 0) { + return []; } + return ( + exports + // Filter false positive export matches + .filter( + (exp) => !exportLocations || _isExportStatement(exportLocations, exp) + ) + // Prevent multiple exports of same function, only keep latest iteration of signatures + .filter((exp, index, exports) => { + const nextExport = exports[index + 1]; + return ( + !nextExport || + exp.type !== nextExport.type || + !exp.name || + exp.name !== nextExport.name + ); + }) + ); +} + +export function findTypeExports(code: string): ESMExport[] { + // Find declarations like export const foo = 'bar' + const declaredExports: DeclarationExport[] = matchAll( + EXPORT_DECAL_TYPE_RE, + code, + { type: "declaration" } + ); + + // Find named exports + const namedExports: NamedExport[] = normalizeNamedExports( + matchAll(EXPORT_NAMED_TYPE_RE, code, { + type: "named", + }) + ); + + // Merge and normalize exports + const exports: ESMExport[] = normalizeExports([ + ...declaredExports, + ...namedExports, + ]); + // Return early when there is no export statement if (exports.length === 0) { return []; @@ -209,6 +245,33 @@ export function findExports(code: string): ESMExport[] { ); } +function normalizeExports(exports: ESMExport[]) { + for (const exp of exports) { + if (!exp.name && exp.names && exp.names.length === 1) { + exp.name = exp.names[0]; + } + if (exp.name === "default" && exp.type !== "default") { + exp._type = exp.type; + exp.type = "default"; + } + if (!exp.names && exp.name) { + exp.names = [exp.name]; + } + } + return exports; +} + +function normalizeNamedExports(namedExports: NamedExport[]) { + for (const namedExport of namedExports) { + namedExport.names = namedExport.exports + .replace(/^\r?\n?/, "") + .split(/\s*,\s*/g) + .filter((name) => !TYPE_RE.test(name)) + .map((name) => name.replace(/^.*?\sas\s/, "").trim()); + } + return namedExports; +} + export function findExportNames(code: string): string[] { return findExports(code) .flatMap((exp) => exp.names) diff --git a/test/exports.test.ts b/test/exports.test.ts index 8bfe3e5..4fbba3a 100644 --- a/test/exports.test.ts +++ b/test/exports.test.ts @@ -4,6 +4,7 @@ import { findExports, findExportNames, resolveModuleExportNames, + findTypeExports, } from "../src"; describe("findExports", () => { @@ -212,7 +213,7 @@ export { type AType, type B as BType, foo } from 'foo' }); }); -describe("fineExportNames", () => { +describe("findExportNames", () => { it("findExportNames", () => { expect( findExportNames(` @@ -304,3 +305,80 @@ export { foo } from 'foo1';export { bar } from 'foo2';export * as foobar from 'f expect(matches).to.have.lengthOf(3); }); }); + +describe("findTypeExports", () => { + it("finds type exports", () => { + const matches = findTypeExports( + ` + export type { Foo } from "./foo"; + export type { Bar } from "./bar"; + interface Qux {} + export type { Qux } + export type Bing = Qux + export declare function getWidget(n: number): Widget + ` + ); + expect(matches).toMatchInlineSnapshot(` + [ + { + "code": "export type Bing", + "declaration": "type", + "end": 172, + "name": "Bing", + "names": [ + "Bing", + ], + "start": 156, + "type": "declaration", + }, + { + "code": "export declare function getWidget", + "declaration": "declare function", + "end": 222, + "name": "getWidget", + "names": [ + "getWidget", + ], + "start": 189, + "type": "declaration", + }, + { + "code": "export type { Foo } from \\"./foo\\"", + "end": 43, + "exports": " Foo", + "name": "Foo", + "names": [ + "Foo", + ], + "specifier": "./foo", + "start": 11, + "type": "named", + }, + { + "code": "export type { Bar } from \\"./bar\\"", + "end": 87, + "exports": " Bar", + "name": "Bar", + "names": [ + "Bar", + ], + "specifier": "./bar", + "start": 55, + "type": "named", + }, + { + "code": "export type { Qux }", + "end": 145, + "exports": " Qux", + "name": "Qux", + "names": [ + "Qux", + ], + "specifier": undefined, + "start": 126, + "type": "named", + }, + ] + `); + }); +});