Skip to content

Instantly share code, notes, and snippets.

@bdwain
Last active October 25, 2017 05:36
Show Gist options
  • Save bdwain/47a0f5df3c6eb4712f8bd4aa07955f77 to your computer and use it in GitHub Desktop.
Save bdwain/47a0f5df3c6eb4712f8bd4aa07955f77 to your computer and use it in GitHub Desktop.
Reorganize exports rollup plugin
const { parse } = require('acorn');
const MagicString = require('magic-string');
//this parses the returned source code and removes all functions from the main export {...};
//it then changes those functions from `function foo(){...}` to `export function foo(){...}`
//if any variables in any file are named the same as an exported function, this will error out.
//A full solution would need to be included as part of rollup. https://github.com/rollup/rollup/issues/1682
function plugin(){
return {
transformBundle: function(code){
let ast = parse(code, {
ecmaVersion: 6,
sourceType: 'module'
});
let namedExports = [], namedExportsStart, namedExportsEnd;
//map of all functions declared at the top-level scope of the file (as opposed to other types of variables).
//value will includes start and end position of each, so that "export " can be prepended
let allFunctionDeclarations = {};
ast.body.forEach(node => {
if(node.type === 'FunctionDeclaration'){
allFunctionDeclarations[node.id.name] = {
start: node.start,
end: node.end
};
return;
}
else if (node.type !== 'ExportNamedDeclaration') {
return;
}
if (node.declaration) { //?? should never happen
console.warn('extra named export found');
return;
}
//node now represents the statement export {...};
//there should only be one of these in the rollup generated bundle
namedExportsStart = node.start;
namedExportsEnd = node.end;
namedExports = node.specifiers.map(s => ({local: s.local.name, exported: s.exported.name}));
});
const functionNames = Object.keys(allFunctionDeclarations);
let remainingExports = namedExports.filter(x => !functionNames.includes(x.local));
let functionExports = namedExports.filter(x => functionNames.includes(x.local));
let magicStr = new MagicString(code.toString());
//recreate the main export statement without the functions in it
let newNamedExportStr = 'export {';
remainingExports.forEach(x => {
if(x.local === x.exported){
newNamedExportStr += `${x.local}, `
}
else{
newNamedExportStr += `${x.local} as ${x.exported}, `;
}
});
//remove the trailing comma and close the statement
newNamedExportStr = newNamedExportStr.substring(0, newNamedExportStr.length - 2) + '};';
magicStr.overwrite(namedExportsStart, namedExportsEnd, newNamedExportStr);
//add the exports for each function
functionExports.forEach(x => {
if(x.exported !== x.local){ //if function foo was exported like "bar as foo", there was a conflict, which is not supported
throw new Error(`ERROR with ${x.exported}: exported function names must be unique across the entire library,
even in other files (not tests though). Their declarations must also match their eventual export names. Please rename local variables
named ${x.exported}. See https://github.com/rollup/rollup/issues/1682 for more info.`);
}
magicStr.prependLeft(allFunctionDeclarations[x.local].start, 'export ');
});
return {code: magicStr.toString()}; //ignoring source maps for this
}
};
}
module.exports = plugin;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment