diff --git a/README.md b/README.md index 194d186..cd41f5e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,11 @@ # babel-plugin-explicit-exports-references -Transforms all internal references to a module's exports such that each reference starts with `module.exports` instead of directly referencing an internal name. This enables easy mocking of specific (exported) functions in Jest with Babel/TypeScript, even when the mocked functions call each other in the same module. +Transforms all internal references to a module's exports such that each +reference starts with `module.exports` instead of directly referencing an +internal name. This enables easy mocking of specific (exported) functions in +Jest with Babel/TypeScript, even when the mocked functions call each other in +the same module. (more description incoming) @@ -104,9 +108,6 @@ information. https://webpack.js.org/guides/tree-shaking/#mark-the-file-as-side-effect-free [exports-main-key]: https://github.com/nodejs/node/blob/8d8e06a345043bec787e904edc9a2f5c5e9c275f/doc/api/packages.md#package-entry-points -[tree-shaking]: https://webpack.js.org/guides/tree-shaking -[local-pkg]: - https://github.com/nodejs/node/blob/8d8e06a345043bec787e904edc9a2f5c5e9c275f/doc/api/packages.md#type [choose-new-issue]: https://github.com/Xunnamius/babel-plugin-explicit-exports-references/issues/new/choose [pr-compare]: diff --git a/src/index.ts b/src/index.ts index 3354a16..8209332 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,18 +9,19 @@ let globalScope: NodePath['scope']; function updateExportRefs( path: NodePath, - mode: 'named' | 'default' + mode: 'named' | 'default', + transformAssignExpr: boolean ): void; function updateExportRefs( path: { from: NodePath; to: string }, - mode: 'named' | 'default' + mode: 'named' | 'default', + transformAssignExpr: boolean ): void; function updateExportRefs( path: NodePath | { from: NodePath; to: string }, - mode: 'named' | 'default' + mode: 'named' | 'default', + transformAssignExpr: boolean ): void { - debug(`mode: ${mode}`); - // @ts-expect-error: need to discriminate between input types const idPath = (path.isIdentifier?.() ? path : path.from) as NodePath; const localName = idPath.node.name; @@ -32,12 +33,18 @@ function updateExportRefs( ...(globalBinding?.constantViolations || []) ]; - debug( - `updating ${refPaths?.length || 0} references to ${mode} export "${localName}"` + - (exportedName != localName ? ` (exported as "${exportedName}")` : '') - ); + const numRefs = refPaths?.length || 0; + const dbg = debug.extend(`mode-${mode}:updating`); + + if (numRefs) { + dbg( + `potentially updating ${numRefs} references to ${mode} export "${localName}"` + + (exportedName != localName ? ` (exported as "${exportedName}")` : '') + ); + } else dbg('no references to update'); - refPaths?.forEach((refPath) => { + refPaths?.forEach((refPath, ndx) => { + const dbg2 = dbg.extend(`ref-${exportedName}-${(ndx + 1).toString()}`); if ( !!refPath.find( (path) => @@ -46,87 +53,113 @@ function updateExportRefs( path.isExportDefaultSpecifier() ) ) { - debug('(an export specifier reference was skipped)'); + dbg2('reference skipped: part of an export specifier'); return; } if (!!refPath.find((path) => path.isTSType())) { - debug('(an TypeScript type reference was skipped)'); + dbg2('reference skipped: TypeScript type reference'); return; } - const wasReplaced = !!(refPath.isIdentifier() - ? refPath - : refPath.isAssignmentExpression() - ? refPath.get('left') - : undefined - )?.replaceWith( - template.expression.ast`module.exports.${mode == 'default' ? mode : exportedName}` - ); - - if (!wasReplaced) debug(`(unsupported reference type "${refPath.type}" was skipped)`); + if (refPath.isIdentifier()) { + dbg2('transforming type "identifier"'); + refPath.replaceWith( + template.expression.ast`module.exports.${mode == 'default' ? mode : exportedName}` + ); + } else if (transformAssignExpr && refPath.isAssignmentExpression()) { + dbg2('transforming type "assignment expression"'); + refPath + .get('left') + // TODO: needs to be more resilient, but we'll repeat this here for now + .replaceWith( + template.expression.ast`module.exports.${ + mode == 'default' ? mode : exportedName + }` + ); + } else dbg2(`reference skipped: unsupported type "${refPath.type}"`); }); } -export default function (): PluginObj { +export default function (): PluginObj< + PluginPass & { opts: { transformAssignExpr: boolean } } +> { return { name: 'explicit-exports-references', visitor: { Program(programPath) { globalScope = programPath.scope; }, - ExportDefaultDeclaration(exportPath) { + ExportDefaultDeclaration(exportPath, state) { const declaration = exportPath.get('declaration'); - debug(`encountered default export`); + const transformAssignExpr = state.opts.transformAssignExpr; + const dbg = debug.extend('mode-default'); + + debug(`encountered default export declaration`); if (declaration.isFunctionDeclaration() || declaration.isClassDeclaration()) { const id = declaration.get('id') as NodePath; - if (id?.node?.name) updateExportRefs(id, 'default'); - else debug('default declaration is anonymous, ignoring'); - } else debug('(ignored)'); + if (id?.node?.name) updateExportRefs(id, 'default', transformAssignExpr); + else dbg('default declaration is anonymous, ignored'); + } else dbg('default declaration not function or class, ignored'); }, - ExportNamedDeclaration(exportPath) { + ExportNamedDeclaration(exportPath, state) { const declaration = exportPath.get('declaration'); const specifiers = exportPath.get('specifiers'); + const transformAssignExpr = state.opts.transformAssignExpr; + const dbg = debug.extend('mode-named'); if (!declaration.node && !specifiers.length) { - debug('ignored empty named export declaration'); + dbg('ignored empty named export declaration'); return; } - debug(`encountered named export`); + debug(`encountered named export node`); + dbg(`processing declaration`); if (declaration.node) { if (declaration.isFunctionDeclaration() || declaration.isClassDeclaration()) { - updateExportRefs(declaration.get('id') as NodePath, 'named'); + updateExportRefs( + declaration.get('id') as NodePath, + 'named', + transformAssignExpr + ); } else if (declaration.isVariableDeclaration()) { declaration.get('declarations').forEach((declarator) => { const id = declarator.get('id'); - if (id.isIdentifier()) updateExportRefs(id, 'named'); + if (id.isIdentifier()) updateExportRefs(id, 'named', transformAssignExpr); else if (id.isObjectPattern()) { id.get('properties').forEach((propPath) => { if (propPath.isObjectProperty()) { const propId = propPath.get('value'); - if (propId.isIdentifier()) updateExportRefs(propId, 'named'); + if (propId.isIdentifier()) + updateExportRefs(propId, 'named', transformAssignExpr); } else if (propPath.isRestElement()) { const arg = propPath.get('argument'); - if (arg.isIdentifier()) updateExportRefs(arg, 'named'); + if (arg.isIdentifier()) + updateExportRefs(arg, 'named', transformAssignExpr); } }); } }); - } else debug('(ignored)'); + } else { + dbg( + 'named declaration is not a function, class, or variable declaration; ignored' + ); + } } + specifiers.length && dbg(`processing ${specifiers.length} specifiers`); + // ? Later exports take precedence over earlier ones specifiers.forEach((specifier) => { if (!specifier.isExportSpecifier()) { - debug(`(ignored export specifier type "${specifier.type}")`); + dbg(`ignored export specifier type "${specifier.type}"`); } else { const local = specifier.get('local'); const exported = specifier.get('exported'); - debug(`encountered specifier "${local} as ${exported}"`); + dbg(`encountered specifier "${local} as ${exported}"`); if (exported.isIdentifier()) { const exportedName = exported.node.name; @@ -135,11 +168,12 @@ export default function (): PluginObj { from: local, to: exportedName }, - exportedName == 'default' ? 'default' : 'named' + exportedName == 'default' ? 'default' : 'named', + transformAssignExpr ); } else { - debug( - '(ignored export specifier because module string names are not supported)' + dbg( + 'ignored export specifier because module string names are not supported' ); } } diff --git a/test/__fixtures__/transforms-assignment-expressions/code.ts b/test/__fixtures__/transforms-assignment-expressions/code.ts new file mode 100644 index 0000000..8c67bfb --- /dev/null +++ b/test/__fixtures__/transforms-assignment-expressions/code.ts @@ -0,0 +1,70 @@ +function internalfn1() { + fn1(); + void Promise.all([1].map((_) => [fn2]).map((a) => a[0]())); +} + +export function fn1() { + global.console.log('hello, world!'); +} + +export async function fn2() { + fn1(); + internalfn1(); +} + +export async function fn3() { + const f = fn1; + await fn2(); + f(); + internalfn1(); +} + +internalfn1(); +void fn3(); + +export let var1: string; +var1 = 'hello, world!'; +var1 = 'goodbye, world!'; +export let var2 = 2; +var2 = 3; +export let var3: boolean, var4: boolean; +export let var5: boolean | string, var6: number; +var5 = true; +var5 = var1; +var6 = 6; +var6 = var2; + +export const var7 = 7, + var8 = 8; +export function fn4() { + return var7 + var8; +} + +let var9 = 9; +var9 += 1; +const var10 = var9; + +export class Class1 { + val() { + return var9; + } +} + +export class Class2 { + val() { + return var10 + var6; + } +} + +class Class3 { + val() { + return new Class1().val() + new Class2().val() + new Class3().life(); + } + + life() { + return 42; + } +} + +new Class3(); +new Class2(); diff --git a/test/__fixtures__/transforms-assignment-expressions/options.json b/test/__fixtures__/transforms-assignment-expressions/options.json new file mode 100644 index 0000000..7697296 --- /dev/null +++ b/test/__fixtures__/transforms-assignment-expressions/options.json @@ -0,0 +1,4 @@ +{ + "fixtureOutputExt": ".mjs", + "transformAssignExpr": true +} diff --git a/test/__fixtures__/transforms-assignment-expressions/output.mjs b/test/__fixtures__/transforms-assignment-expressions/output.mjs new file mode 100644 index 0000000..827765d --- /dev/null +++ b/test/__fixtures__/transforms-assignment-expressions/output.mjs @@ -0,0 +1,66 @@ +function internalfn1() { + module.exports.fn1(); + void Promise.all([1].map((_) => [module.exports.fn2]).map((a) => a[0]())); +} + +export function fn1() { + global.console.log('hello, world!'); +} +export async function fn2() { + module.exports.fn1(); + internalfn1(); +} +export async function fn3() { + const f = module.exports.fn1; + await module.exports.fn2(); + f(); + internalfn1(); +} +internalfn1(); +void module.exports.fn3(); +export let var1; +module.exports.var1 = 'hello, world!'; +module.exports.var1 = 'goodbye, world!'; +export let var2 = 2; +module.exports.var2 = 3; +export let var3, var4; +export let var5, var6; +module.exports.var5 = true; +module.exports.var5 = module.exports.var1; +module.exports.var6 = 6; +module.exports.var6 = module.exports.var2; +export const var7 = 7, + var8 = 8; +export function fn4() { + return module.exports.var7 + module.exports.var8; +} +let var9 = 9; +var9 += 1; +const var10 = var9; +export class Class1 { + val() { + return var9; + } +} +export class Class2 { + val() { + return var10 + module.exports.var6; + } +} + +class Class3 { + val() { + return ( + new module.exports.Class1().val() + + new module.exports.Class2().val() + + new Class3().life() + ); + } + + life() { + return 42; + } +} + +new Class3(); +new module.exports.Class2(); diff --git a/test/__fixtures__/transforms-primitive-exports/output.mjs b/test/__fixtures__/transforms-primitive-exports/output.mjs index 827765d..4006705 100644 --- a/test/__fixtures__/transforms-primitive-exports/output.mjs +++ b/test/__fixtures__/transforms-primitive-exports/output.mjs @@ -19,16 +19,16 @@ export async function fn3() { internalfn1(); void module.exports.fn3(); export let var1; -module.exports.var1 = 'hello, world!'; -module.exports.var1 = 'goodbye, world!'; +var1 = 'hello, world!'; +var1 = 'goodbye, world!'; export let var2 = 2; -module.exports.var2 = 3; +var2 = 3; export let var3, var4; export let var5, var6; -module.exports.var5 = true; -module.exports.var5 = module.exports.var1; -module.exports.var6 = 6; -module.exports.var6 = module.exports.var2; +var5 = true; +var5 = module.exports.var1; +var6 = 6; +var6 = module.exports.var2; export const var7 = 7, var8 = 8; export function fn4() { diff --git a/test/index.test.ts b/test/index.test.ts index 120361f..45658db 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -7,8 +7,7 @@ const babelOptions = { plugins: [ '@babel/plugin-syntax-module-string-names', '@babel/plugin-proposal-export-default-from', - '@babel/plugin-proposal-function-bind', - '@babel/plugin-transform-typescript' + '@babel/plugin-proposal-function-bind' ], presets: [ [