Skip to content

Commit

Permalink
feat: findTypeExports for finding type exports (#156)
Browse files Browse the repository at this point in the history
  • Loading branch information
danielroe committed Mar 10, 2023
1 parent 8368caa commit a48974d
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 24 deletions.
109 changes: 86 additions & 23 deletions src/analyze.ts
Expand Up @@ -62,8 +62,12 @@ export const DYNAMIC_IMPORT_RE =

export const EXPORT_DECAL_RE =
/\bexport\s+(?<declaration>(async function|function|let|const enum|const|enum|var|class))\s+(?<name>[\w$]+)/g;
export const EXPORT_DECAL_TYPE_RE =
/\bexport\s+(?<declaration>(interface|type|declare (async function|function|let|const enum|const|enum|var|class)))\s+(?<name>[\w$]+)/g;
const EXPORT_NAMED_RE =
/\bexport\s+{(?<exports>[^}]+?)[\s,]*}(\s*from\s*["']\s*(?<specifier>(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][^\n;]*)?/g;
const EXPORT_NAMED_TYPE_RE =
/\bexport\s+type\s+{(?<exports>[^}]+?)[\s,]*}(\s*from\s*["']\s*(?<specifier>(?<="\s*)[^"]*[^\s"](?=\s*")|(?<='\s*)[^']*[^\s'](?=\s*'))\s*["'][^\n;]*)?/g;
const EXPORT_NAMED_DESTRUCT =
/\bexport\s+(let|var|const)\s+(?:{(?<exports1>[^}]+?)[\s,]*}|\[(?<exports2>[^\]]+?)[\s,]*])\s+=/gm;
const EXPORT_STAR_RE =
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 [];
Expand Down Expand Up @@ -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)
Expand Down
80 changes: 79 additions & 1 deletion test/exports.test.ts
Expand Up @@ -4,6 +4,7 @@ import {
findExports,
findExportNames,
resolveModuleExportNames,
findTypeExports,
} from "../src";

describe("findExports", () => {
Expand Down Expand Up @@ -212,7 +213,7 @@ export { type AType, type B as BType, foo } from 'foo'
});
});

describe("fineExportNames", () => {
describe("findExportNames", () => {
it("findExportNames", () => {
expect(
findExportNames(`
Expand Down Expand Up @@ -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",
},
]
`);
});
});

0 comments on commit a48974d

Please sign in to comment.