'use strict';
const _ = require('lodash');
const traverse = require('babel-traverse').default;
const generate = require('babel-generator').default;
const babelTypes = require('babel-types');
const common = require('./common');
const identifier = require('./identifier');
const babelGeneratorOptions = {
quotes: 'single',
};
function formatMultilineImport(importCode) {
// format import statement to:
// import {
// name1,
// name2,
// } from './xxx';
const m = importCode.match(/\{([^}]+)\}/);
if (m) {
const arr = _.compact(m[1].split(/, */).map(_.trim));
if (arr.length) {
return importCode.replace(/\{[^}]+\}/, `{\n ${arr.join(',\n ')},\n}`);
}
}
return importCode;
}
/**
* Import from a given module source. This methods operates import statement:
* import defaultImmport, { namedImport ... } from './module-source';
* It noly supports es6 modules import but not commonJS or AMD or others...
* @param {string} ast - Which module to manage import statement.
* @param {string} moduleSource - From which module source to add import from. If not found, the create an import line.
* @index {string} defaultImport - The default import. If not need, pass it as null. The module should haven't import the default.
* @index {string|array} namedImport - The named imports. If has imported, then do nothing.
* @index {string} namespaceImport - The new function name.
* @alias module:refactor.addImportFrom
* @example
* const refactor = require('rekit-core').refactor;
* refactor.addImportFrom(file, './some-module', 'SomeModule', ['method1', 'method2']);
* // it generates: import SomeModule, { method1, method2 } from './some-module';
**/
function addImportFrom(ast, moduleSource, defaultImport, namedImport, namespaceImport) {
// Summary:
// Add import from source module. Such as import { xxx } from './x';
let names = [];
if (namedImport) {
if (typeof namedImport === 'string') {
names.push(namedImport);
} else {
names = names.concat(namedImport);
}
}
const changes = [];
const t = babelTypes;
let targetImportPos = 0;
let sourceExisted = false;
traverse(ast, {
ImportDeclaration(path) {
const node = path.node;
// multilines means whether to separate import specifiers into different lines
const multilines = node.loc.start.line !== node.loc.end.line;
targetImportPos = path.node.end + 1;
if (!node.specifiers || !node.source || node.source.value !== moduleSource) return;
sourceExisted = true;
let newNames = [];
const alreadyHaveDefaultImport = !!_.find(node.specifiers, { type: 'ImportDefaultSpecifier' });
const alreadyHaveNamespaceImport = !!_.find(node.specifiers, { type: 'ImportNamespaceSpecifier' });
if (defaultImport && !alreadyHaveDefaultImport) newNames.push(defaultImport);
if (namespaceImport && !alreadyHaveNamespaceImport) newNames.push(namespaceImport);
newNames = newNames.concat(names);
// only add names which don't exist
newNames = newNames.filter(n => !_.find(node.specifiers, s => s.local.name === n));
if (newNames.length > 0) {
const newSpecifiers = [].concat(node.specifiers);
newNames.forEach((n) => {
const local = t.identifier(n);
const imported = local; // TODO: doesn't support local alias.
if (n === defaultImport) {
newSpecifiers.unshift(t.importDefaultSpecifier(local));
} else if (n === namespaceImport) {
newSpecifiers.push(t.importNamespaceSpecifier(local));
} else {
newSpecifiers.push(t.importSpecifier(local, imported));
}
});
const newNode = Object.assign({}, node, { specifiers: newSpecifiers });
let newCode = generate(newNode, babelGeneratorOptions).code;
if (multilines) {
newCode = formatMultilineImport(newCode);
}
changes.push({
start: node.start,
end: node.end,
replacement: newCode,
});
}
}
});
if (changes.length === 0 && !sourceExisted) {
// add new import declaration if module source doesn't exist
const specifiers = [];
if (defaultImport) {
specifiers.push(t.importDefaultSpecifier(t.identifier(defaultImport)));
}
if (namespaceImport) {
specifiers.push(t.importNamespaceSpecifier(t.identifier(namespaceImport)));
}
names.forEach((n) => {
const local = t.identifier(n);
const imported = local;
specifiers.push(t.importSpecifier(local, imported));
});
const node = t.importDeclaration(specifiers, t.stringLiteral(moduleSource));
const code = generate(node, babelGeneratorOptions).code;
changes.push({
start: targetImportPos,
end: targetImportPos,
replacement: `${code}\n`,
});
}
return changes;
}
/**
* Export from a given module source. This methods operates export ... from statement:
* @param {string} ast - Which module to manage export from statement.
* @param {string} moduleSource - From which module source to add import from. If not found, the create an import line.
* @index {string} defaultExport - The default import. If not need, pass it as null. The module should haven't import the default.
* @index {string|array} namedExport - The named imports. If has imported, then do nothing.
* @alias module:refactor.addExportFrom
**/
function addExportFrom(ast, moduleSource, defaultExport, namedExport) {
// Summary:
// Add export from source module. Such as export { xxx } from './x';
let names = [];
if (namedExport) {
if (typeof namedExport === 'string') {
names.push(namedExport);
} else {
names = names.concat(namedExport);
}
}
const changes = [];
const t = babelTypes;
let targetExportPos = 0;
let sourceExisted = false;
traverse(ast, {
ExportNamedDeclaration(path) {
const node = path.node;
targetExportPos = path.node.end + 1;
if (!node.specifiers || !node.source || node.source.value !== moduleSource) return;
sourceExisted = true;
let newNames = [];
const alreadyHaveDefaultExport = !!_.find(node.specifiers, s => _.get(s, 'local.name') === 'default');
if (defaultExport && !alreadyHaveDefaultExport) newNames.push(defaultExport);
newNames = newNames.concat(names);
// only add names which don't exist
newNames = newNames.filter(n => !_.find(node.specifiers, s => (_.get(s, 'exported.name') || _.get(s, 'local.name')) === n));
if (newNames.length > 0) {
const newSpecifiers = [].concat(node.specifiers);
newNames.forEach((n) => {
const local = t.identifier(n);
const exported = local; // TODO: doesn't support local alias.
if (n === defaultExport) {
newSpecifiers.unshift(t.exportSpecifier(t.identifier('default'), exported));
} else {
newSpecifiers.push(t.exportSpecifier(local, exported));
}
});
const newNode = Object.assign({}, node, { specifiers: newSpecifiers });
const newCode = generate(newNode, babelGeneratorOptions).code;
changes.push({
start: node.start,
end: node.end,
replacement: newCode,
});
}
}
});
if (changes.length === 0 && !sourceExisted) {
const specifiers = [];
if (defaultExport) {
specifiers.push(t.exportSpecifier(t.identifier('default'), t.identifier(defaultExport)));
}
names.forEach((n) => {
const local = t.identifier(n);
const exported = local;
specifiers.push(t.exportSpecifier(local, exported));
});
const node = t.ExportNamedDeclaration(null, specifiers, t.stringLiteral(moduleSource));
const code = generate(node, babelGeneratorOptions).code;
changes.push({
start: targetExportPos,
end: targetExportPos,
replacement: `${code}\n`,
});
}
return changes;
}
function renameImportAsSpecifier(ast, oldName, newName) {
let defNode = null;
let changes = [];
traverse(ast, {
ImportSpecifier(path) {
if (_.get(path.node, 'local.name') === oldName) {
defNode = path.node.local;
}
}
});
if (defNode) {
changes = changes.concat(identifier.renameIdentifier(ast, oldName, newName, defNode));
}
return changes;
}
function renameImportSpecifier(ast, oldName, newName, moduleSource) {
// Summary:
// Rename the import(default, named) variable name and their reference.
// The simple example is to rename a component
// NOTE: only rename imported name!
// eg: import { A as A1 } from './A'; A -> B import { B as A1 } from './A';
// import fetchList from './action';
// import { fetchList as fetchTopicList } from '../topic/action';
let defNode = null;
let changes = [];
traverse(ast, {
ImportDeclaration(path) {
const node = path.node;
if (moduleSource && _.get(node, 'source.value') !== moduleSource) return;
// console.log(_.get(node, 'source.value'), moduleSource);
node.specifiers.forEach((specifier) => {
if (
(specifier.type === 'ImportDefaultSpecifier' || specifier.type === 'ImportNamespaceSpecifier')
&& _.get(specifier, 'local.name') === oldName
) {
defNode = specifier.local;
}
// only rename imported specifier
if (specifier.type === 'ImportSpecifier' && _.get(specifier, 'imported.name') === oldName) {
if (_.get(specifier, 'local.name') === oldName) {
defNode = specifier.local;
} else {
changes.push({
start: specifier.imported.start,
end: specifier.imported.end,
replacement: newName,
});
}
}
});
}
});
if (defNode) {
changes = changes.concat(identifier.renameIdentifier(ast, oldName, newName, defNode));
}
return changes;
}
function renameExportSpecifier(ast, oldName, newName, moduleSource) {
// It only rename export specifier but not references.
const changes = [];
traverse(ast, {
ExportSpecifier(path) {
const node = path.node;
if (moduleSource && _.get(path.parentPath.node, 'source.value') !== moduleSource) return;
if (_.get(node, 'local.name') === oldName) {
changes.push({
start: node.local.start,
end: node.local.end,
replacement: newName,
});
} else if (_.get(node, 'exported.name') === oldName) {
changes.push({
start: node.exported.start,
end: node.exported.end,
replacement: newName,
});
}
}
});
return changes;
}
function renameModuleSource(ast, oldModuleSource, newModuleSource) {
// Summary:
// Rename the module source for import/export xx from moduleSource.
// It only compares the string rather that resolve to the absolute path.
const changes = [];
function renameSource(path) {
const node = path.node;
if (node.source && node.source.value === oldModuleSource) {
changes.push({
start: node.source.start + 1,
end: node.source.end - 1,
replacement: newModuleSource,
});
}
}
traverse(ast, {
ImportDeclaration: renameSource,
ExportNamedDeclaration: renameSource,
});
return changes;
}
function removeImportSpecifier(ast, name) {
// Remove import specifier by local name
let names = name;
if (typeof name === 'string') {
names = [name];
}
const changes = [];
traverse(ast, {
ImportDeclaration(path) {
const node = path.node;
const multilines = node.loc.start.line !== node.loc.end.line;
if (!node.specifiers) return;
const newSpecifiers = node.specifiers.filter(s => !names.includes(s.local.name));
if (newSpecifiers.length === 0) {
// no specifiers, should delete the import statement
changes.push({
start: node.start,
end: node.end,
replacement: '',
});
} else if (newSpecifiers.length !== node.specifiers.length) {
// remove the specifier import
const newNode = Object.assign({}, node, { specifiers: newSpecifiers });
let newCode = generate(newNode, {}).code;
if (multilines) newCode = formatMultilineImport(newCode);
changes.push({
start: node.start,
end: node.end,
replacement: newCode,
});
}
}
});
return changes;
}
function removeImportBySource(ast, moduleSource) {
const changes = [];
function removeBySource(path) {
const node = path.node;
if (!node.source) return;
if (node.source.value === moduleSource) {
changes.push({
start: node.start,
end: node.end,
replacement: '',
});
}
}
traverse(ast, {
ExportNamedDeclaration: removeBySource,
ImportDeclaration: removeBySource,
});
return changes;
}
module.exports = {
addImportFrom: common.acceptFilePathForAst(addImportFrom),
addExportFrom: common.acceptFilePathForAst(addExportFrom),
renameImportSpecifier: common.acceptFilePathForAst(renameImportSpecifier),
renameImportAsSpecifier: common.acceptFilePathForAst(renameImportAsSpecifier),
renameExportSpecifier: common.acceptFilePathForAst(renameExportSpecifier),
removeImportSpecifier: common.acceptFilePathForAst(removeImportSpecifier),
// removeExportSpecifier: common.acceptFilePathForAst(removeExportSpecifier),
removeImportBySource: common.acceptFilePathForAst(removeImportBySource),
renameModuleSource: common.acceptFilePathForAst(renameModuleSource),
};