Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/gentle-bundles-detect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@arethetypeswrong/core": patch
---

Detect default exports in webpack-bundled CommonJS libraries.
111 changes: 110 additions & 1 deletion packages/core/src/internal/getProbableExports.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export interface Export {
}

export function getProbableExports(sourceFile: ts.SourceFile): Export[] {
return getEsbuildBabelSwcExports(sourceFile) ?? [];
return getEsbuildBabelSwcExports(sourceFile) ?? getWebpackBootstrapExports(sourceFile) ?? [];
}

function getEsbuildBabelSwcExports(sourceFile: ts.SourceFile): Export[] | undefined {
Expand Down Expand Up @@ -93,3 +93,112 @@ function isEsbuildExportFunction(decl: ts.Declaration | undefined) {
function isProbablyMinified(text: string): boolean {
return minifiedVariableAssignmentPattern.test(text);
}

function getWebpackBootstrapExports(sourceFile: ts.SourceFile): Export[] | undefined {
for (const statement of sourceFile.statements) {
if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression)) {
continue;
}
const assignment = statement.expression;
if (assignment.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !isModuleExports(assignment.left)) {
continue;
}

const bootstrapCall = ts.skipParentheses(assignment.right);
if (!ts.isCallExpression(bootstrapCall)) {
continue;
}

const bootstrap = ts.skipParentheses(bootstrapCall.expression);
if (
!ts.isFunctionExpression(bootstrap) ||
!ts.isBlock(bootstrap.body) ||
bootstrapCall.arguments.length !== 1 ||
!ts.isArrayLiteralExpression(bootstrapCall.arguments[0])
) {
continue;
}

const entryModuleId = getWebpackEntryModuleId(bootstrap.body);
if (entryModuleId === undefined) {
continue;
}

const entryModule = bootstrapCall.arguments[0].elements[entryModuleId];
if (!entryModule || !ts.isFunctionExpression(entryModule)) {
continue;
}

const exportsParameterName = entryModule.parameters[1]?.name;
if (!exportsParameterName || !ts.isIdentifier(exportsParameterName)) {
continue;
}
const exportsParameterText = exportsParameterName.text;

const exports: Export[] = [];
visit(entryModule.body);
return exports;

function visit(node: ts.Node) {
if (
ts.isBinaryExpression(node) &&
node.operatorToken.kind === ts.SyntaxKind.EqualsToken &&
ts.isAccessExpression(node.left) &&
ts.isIdentifier(node.left.expression) &&
node.left.expression.text === exportsParameterText
) {
const name = getNameOfAccessExpression(node.left);
if (name !== undefined) {
exports.push({ name, node });
}
}
ts.forEachChild(node, visit);
}
}
}

function getWebpackEntryModuleId(body: ts.Block): number | undefined {
for (const statement of body.statements) {
if (!ts.isReturnStatement(statement) || !statement.expression) {
continue;
}

return getWebpackEntryModuleIdFromExpression(statement.expression);
}
}

function getWebpackEntryModuleIdFromExpression(expression: ts.Expression): number | undefined {
expression = ts.skipParentheses(expression);
if (ts.isCallExpression(expression) && expression.arguments.length === 1) {
return getNumericValue(expression.arguments[0]);
}
if (ts.isBinaryExpression(expression) && expression.operatorToken.kind === ts.SyntaxKind.CommaToken) {
return getWebpackEntryModuleIdFromExpression(expression.right);
}
}

function getNumericValue(node: ts.Expression): number | undefined {
node = ts.skipParentheses(node);
if (ts.isNumericLiteral(node)) {
return Number(node.text);
}
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
return getNumericValue(node.right);
}
}

function isModuleExports(node: ts.Expression) {
return (
ts.isAccessExpression(node) &&
ts.isIdentifier(node.expression) &&
node.expression.text === "module" &&
getNameOfAccessExpression(node) === "exports"
);
}

function getNameOfAccessExpression(accessExpression: ts.AccessExpression): string | undefined {
const node = ts.getNameOfAccessExpression(accessExpression);
if (ts.isIdentifier(node) || ts.isStringLiteralLike(node)) {
return node.text;
}
}
Comment on lines +190 to +204
20 changes: 20 additions & 0 deletions packages/core/test/getProbableExports.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,26 @@ _export(exports, {
var _pluginHelper = require("@bbob/plugin-helper");
var _parse = /*#__PURE__*/ _interopRequireWildcard(require("./parse"));`,
},

webpack_cjs_default: {
expected: ["__esModule", "default"],
file: `module.exports = function(e) {
var r = {};
function t(n) {
if (r[n]) return r[n].exports;
var o = r[n] = { exports: {} };
e[n].call(o.exports, o, o.exports, t);
return o.exports;
}
return t.m = e, t.c = r, t(t.s = 0);
}([
function(e, r, t) {
"use strict";
r.__esModule = true;
r.default = function accept() {};
}
]);`,
},
};

describe("getProbableExports", () => {
Expand Down
24 changes: 24 additions & 0 deletions packages/core/test/problems/exportDefaultDisagreement.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,30 @@ var index_default = { a, b };`,
);
});

test("ignores bundled CommonJS default export with aliased exports object", () => {
assert(
isOk(
`export default function accept(): boolean;`,
`module.exports = function(e) {
var r = {};
function t(n) {
if (r[n]) return r[n].exports;
var o = r[n] = { exports: {} };
e[n].call(o.exports, o, o.exports, t);
return o.exports;
}
return t(0);
}([
function(e, r, t) {
"use strict";
r.__esModule = true;
r.default = function accept() {};
}
]);`,
),
);
});

test("types have single default export, JS has default and other named exports", () => {
assert(
isOk(
Expand Down