src/mergers/rollup.mjs
import recast from "recast";
import parser from "recast/parsers/babel.js";
import transform from "@babel/core/lib/transform.js";
import { StringContentEntry } from "content-entry";
import { Merger } from "../merger.mjs";
export class Rollup extends Merger {
static get pattern() {
return "**/rollup.config.*js";
}
static get options() {
return {
...super.options,
messagePrefix: "chore(rollup): ",
optionalDevDependencies: [
/\@rollup\/plugin\-.*/,
/rollup\-plugin\-.*/,
/babel-preset.*/,
"builtin-modules",
/^postcss-import$/
]
};
}
static async usedDevDependencies(into, entry) {
const content = await entry.string;
const ast = recast.parse(content, {
parser: {
parse: source =>
transform.transform(source, {
code: false,
ast: true,
sourceMap: false
}).ast
}
});
for (const decl of ast.program.body) {
if (decl.type === "ImportDeclaration") {
into.add(decl.source.value);
}
}
return into;
}
static async *commits(
context,
destinationEntry,
sourceEntry,
options = this.options
) {
const name = destinationEntry.name;
const templateContent = await sourceEntry.string;
if (await destinationEntry.isEmpty) {
yield {
message: `${options.messagePrefix}add missing ${destinationEntry.name} from template`,
entries: [
new StringContentEntry(
destinationEntry.name,
context.expand(templateContent, options.expand)
)
]
};
return;
}
const original = await destinationEntry.string;
let messages = [];
const templateAST = recast.parse(templateContent, parser);
const ast = recast.parse(original, parser);
removeUseStrict(ast);
const exp = exportDefaultDeclaration(ast);
const templateExp = exportDefaultDeclaration(templateAST);
if (exp?.properties !== undefined && templateExp !== undefined) {
let output, dest;
const banner = removePropertiesKey(exp.properties, "banner");
for (const p of exp.properties) {
switch (p.key.name) {
case "targets":
dest = p.value.elements[0].properties[0]; //.find(x => x.name === 'dest');
const op = findProperty(templateExp.properties, "output");
if (op !== undefined) {
p.key.name = "output";
p.value = op.value;
output = p;
}
break;
case "entry":
const ip = findProperty(templateExp.properties, "input");
if (ip !== undefined) {
p.key.name = "input";
p.value = ip.value;
}
}
}
if (findProperty(exp.properties, "input") === undefined) {
exp.properties.push(findProperty(templateExp.properties, "input"));
}
const originalOutput = findProperty(exp.properties, "output");
const templateOutput = findProperty(templateExp.properties, "output");
if (originalOutput === undefined) {
exp.properties.push(templateOutput);
} else {
mergeKeys(
templateOutput,
originalOutput,
[
"format",
"file",
"dir",
"name",
"globals",
"paths",
"banner",
"footer",
"intro",
"outro",
"sourcemap",
"sourcemapFile",
"interop",
"extend",
"exports",
"amd",
"indent",
"strict",
"freeze",
"legacy",
"namespaceToStringTag"
],
messages,
options
);
if (output !== undefined) {
if (banner !== undefined) {
output.value.properties.push(banner);
}
if (dest !== undefined) {
const file = findProperty(output.value.properties, "file");
if (file !== undefined) {
file.value = dest.value;
}
}
}
removePropertiesKey(exp.properties, "format");
removePropertiesKey(exp.properties, "sourceMap");
removePropertiesKey(exp.properties, "dest");
}
}
const originalImports = importDeclarationsByLocalName(ast);
const templateImports = importDeclarationsByLocalName(templateAST);
const addedImports = [];
templateImports.forEach((value, key) => {
if (originalImports.get(key) === undefined) {
ast.program.body = [value, ...ast.program.body];
addedImports.push(key);
}
});
if (addedImports.length > 0) {
messages.push(`${options.messagePrefix}import ${addedImports.join(",")}`);
}
const addedPlugins = [];
const originalPlugins = pluginsFromExpression(exp);
const templatePlugins = pluginsFromExpression(templateExp);
templatePlugins.forEach(templatePlugin => {
if (
templatePlugin &&
templatePlugin.callee !== undefined &&
originalPlugins.find(
op => op?.callee?.name === templatePlugin.callee.name
) === undefined
) {
originalPlugins.push(templatePlugin);
addedPlugins.push(templatePlugin.callee.name);
}
});
if (addedPlugins.length > 0) {
messages.push(`${options.messagePrefix}add ${addedPlugins.join(",")}`);
}
const merged = recast.print(ast).code;
if (original !== merged) {
yield {
entries: [new StringContentEntry(name, merged)],
message: messages.join("\n")
};
}
}
}
function removeUseStrict(ast) {
for (const i in ast.program.body) {
const decl = ast.program.body[i];
if (
decl.type === "ExpressionStatement" &&
decl.expression.type === "Literal" &&
decl.expression.value === "use strict"
) {
ast.program.body.splice(i, 1);
return;
}
}
}
function importDeclarationsByLocalName(ast) {
const declarations = new Map();
for (const decl of ast.program.body) {
if (decl.type === "ImportDeclaration") {
for (const spec of decl.specifiers) {
declarations.set(spec.local.name, decl);
}
}
}
return declarations;
}
function exportDefaultDeclaration(ast) {
for (const decl of ast.program.body) {
if (decl.type === "ExportDefaultDeclaration") {
return decl.declaration;
}
}
return undefined;
}
function pluginsFromExpression(exp) {
const plugins = exp?.properties?.find(p => p?.key.name === "plugins");
if (plugins !== undefined) {
return plugins.value.elements;
}
return [];
}
function findProperty(properties, name) {
if (properties !== undefined) {
const prop = properties.find(prop => prop?.key.name === name);
return prop;
}
return undefined;
}
function removePropertiesKey(properties, name) {
if (properties === undefined) {
return undefined;
}
const toBeRemoved = properties.findIndex(x => x && x.key.name === name);
if (toBeRemoved >= 0) {
const slot = properties[toBeRemoved];
properties.splice(toBeRemoved, 1);
return slot;
}
return undefined;
}
function mergeKeys(source, dest, knownKeys, messages, options) {
const mergedKeys = [];
if (source !== undefined) {
knownKeys.forEach(key => {
const destProp = findProperty(dest.value.properties, key);
const sourceProp = findProperty(source.value.properties, key);
if (sourceProp !== undefined && destProp === undefined) {
mergedKeys.push(key);
dest.value.properties.push(sourceProp);
}
});
}
if (mergedKeys.length > 0) {
messages.push(
`${options.messagePrefix}add to output ${mergedKeys.join(",")}`
);
}
return mergedKeys;
}