2023-02-02 20:12:37 +08:00

480 lines
15 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { flatMap, camelCase, get } from 'lodash';
import { COMMON_CHUNK_NAME } from '../../const/generator';
import {
BuilderComponentPlugin,
BuilderComponentPluginFactory,
ChunkType,
CodeGeneratorError,
DependencyType,
FileType,
ICodeChunk,
ICodeStruct,
IDependency,
IExternalDependency,
IInternalDependency,
IWithDependency,
} from '../../types';
import { isValidIdentifier, isValidComponentName } from '../../utils/validate';
// TODO: main 这个信息到底怎么用,是不是外部包不需要使用?
const DEP_MAIN_BLOCKLIST = ['lib', 'lib/index', 'es', 'es/index', 'main'];
const DEFAULT_EXPORT_NAME = '__default__';
function groupDepsByPack(deps: IDependency[]): Record<string, IDependency[]> {
const depMap: Record<string, IDependency[]> = {};
const addDep = (pkg: string, dep: IDependency) => {
if (!depMap[pkg]) {
depMap[pkg] = [];
}
depMap[pkg].push(dep);
};
deps.forEach((dep) => {
if (dep.dependencyType === DependencyType.Internal) {
addDep(`${(dep as IInternalDependency).moduleName}${dep.main ? `/${dep.main}` : ''}`, dep);
} else {
let depMain = '';
// TODO: 部分类型的 main 暂时认为没用
if (dep.main && DEP_MAIN_BLOCKLIST.indexOf(dep.main) < 0) {
depMain = dep.main;
}
if (depMain.substring(0, 1) === '/') {
depMain = depMain.substring(1);
}
addDep(`${(dep as IExternalDependency).package}${depMain ? `/${depMain}` : ''}`, dep);
}
});
return depMap;
}
interface IDependencyItem {
exportName: string;
aliasName?: string;
isDefault?: boolean;
subName?: string;
nodeIdentifier?: string; // 与使用处的映射关系,理论上是不可变更的,如需变更需要提供额外信息
source: IDependency;
}
interface IExportItem {
exportName: string;
aliasNames: string[];
isDefault?: boolean;
needOriginExport: boolean;
}
function getDependencyIdentifier(info: IDependencyItem): string {
return info.aliasName || info.exportName;
}
function getExportNameOfDep(dep: IDependency): string {
if (dep.destructuring) {
return (
dep.exportName ||
dep.componentName ||
throwNewError('destructuring dependency must have exportName or componentName')
);
}
if (!dep.subName) {
return (
dep.componentName ||
dep.exportName ||
throwNewError('dependency item must have componentName or exportName')
);
}
return (
dep.exportName ||
`__$${camelCase(
get(dep, 'moduleName') ||
get(dep, 'package') ||
throwNewError('dep.moduleName or dep.package is undefined'),
)}_default`
);
}
function throwNewError(msg: string): never {
throw new Error(msg);
}
function buildPackageImport(
pkg: string,
deps: IDependency[],
targetFileType: string,
useAliasName: boolean,
): ICodeChunk[] {
// 如果压根没有包,则不生成对应的 import 语句(生成了没有任何意义)
if (!pkg || pkg === 'undefined' || pkg === 'null') {
// TODO: 要不要加个 warning
return [];
}
const chunks: ICodeChunk[] = [];
const exportItems: Record<string, IExportItem> = {};
const defaultExportNames: string[] = [];
const depsInfo: IDependencyItem[] = deps.map((dep) => {
const info: IDependencyItem = {
exportName: getExportNameOfDep(dep),
isDefault: !dep.destructuring,
subName: dep.subName || undefined,
nodeIdentifier: dep.componentName || undefined,
source: dep,
};
// 下面 5 个逻辑是清理不必要的冗余信息,做到数据结构归一化
if (info.isDefault) {
if (defaultExportNames.indexOf(info.exportName) < 0) {
defaultExportNames.push(info.exportName);
}
}
if (!info.subName) {
if (info.nodeIdentifier === info.exportName) {
info.nodeIdentifier = undefined;
}
if (info.isDefault) {
info.aliasName = info.nodeIdentifier || info.exportName;
info.exportName = DEFAULT_EXPORT_NAME;
}
if (info.nodeIdentifier) {
info.aliasName = info.nodeIdentifier;
info.nodeIdentifier = undefined;
}
} else {
if (info.isDefault) {
info.aliasName = info.exportName;
info.exportName = DEFAULT_EXPORT_NAME;
}
if (info.nodeIdentifier === `${info.exportName}.${info.subName}`) {
info.nodeIdentifier = undefined;
}
}
return info;
});
// 建立 export 项目的列表
depsInfo.forEach((info) => {
if (!exportItems[info.exportName]) {
exportItems[info.exportName] = {
exportName: info.exportName,
isDefault: info.isDefault,
aliasNames: [],
needOriginExport: false,
};
}
if (!info.nodeIdentifier && !info.aliasName) {
exportItems[info.exportName].needOriginExport = true;
}
});
// 建立别名字典
depsInfo.forEach((info) => {
if (info.aliasName) {
const { aliasNames } = exportItems[info.exportName];
if (aliasNames.indexOf(info.aliasName) < 0) {
aliasNames.push(info.aliasName);
}
}
});
// fix: 父组件ImportAliasDefine, 与子组件import的父组件冲突情况
depsInfo.forEach((info) => {
if (info.nodeIdentifier) {
const exportItem = exportItems[info.exportName];
if (!exportItem.needOriginExport && exportItem.aliasNames.length > 0) {
// eslint-disable-next-line no-param-reassign
info.aliasName = exportItem.aliasNames[0];
}
}
});
// 发现 nodeIdentifier 与 exportName 或者 aliasName 冲突的场景
const nodeIdentifiers = depsInfo.map((info) => info.nodeIdentifier).filter(Boolean);
const conflictInfos = flatMap(Object.keys(exportItems), (exportName) => {
const exportItem = exportItems[exportName];
const usedNames = [
...exportItem.aliasNames,
...(exportItem.needOriginExport || exportItem.aliasNames.length <= 0 ? [exportName] : []),
];
const conflictNames = usedNames.filter((n) => nodeIdentifiers.indexOf(n) >= 0);
if (conflictNames.length > 0) {
return [
...(conflictNames.indexOf(exportName) >= 0 ? [[exportName, true, exportItem]] : []),
...conflictNames.filter((n) => n !== exportName).map((n) => [n, false, exportItem]),
];
}
return [];
});
const conflictExports = conflictInfos.filter((c) => c[1]).map((c) => c[0] as string);
const conflictAlias = conflictInfos.filter((c) => !c[1]).map((c) => c[0] as string);
const solutions: Record<string, string> = {};
depsInfo.forEach((info) => {
if (info.aliasName && conflictAlias.indexOf(info.aliasName) >= 0) {
// find solution
let solution = solutions[info.aliasName];
if (!solution) {
solution = `${info.aliasName}Alias`;
const conflictItem = (conflictInfos.find((c) => c[0] === info.aliasName) ||
[])[2] as IExportItem;
conflictItem.aliasNames = conflictItem.aliasNames.filter((a) => a !== info.aliasName);
conflictItem.aliasNames.push(solution);
solutions[info.aliasName] = solution;
}
// eslint-disable-next-line no-param-reassign
info.aliasName = solution;
}
if (conflictExports.indexOf(info.exportName) >= 0) {
// find solution
let solution = solutions[info.exportName];
if (!solution) {
solution = `${info.exportName}Export`;
const conflictItem = (conflictInfos.find((c) => c[0] === info.exportName) ||
[])[2] as IExportItem;
conflictItem.aliasNames.push(solution);
conflictItem.needOriginExport = false;
solutions[info.exportName] = solution;
}
// eslint-disable-next-line no-param-reassign
info.aliasName = solution;
}
});
// 判断是否所有依赖都有合法的 Identifier
depsInfo.forEach((info) => {
const name = info.aliasName || info.exportName;
if (!isValidIdentifier(name)) {
throw new CodeGeneratorError(`Invalid Identifier [${name}]`);
}
if (info.nodeIdentifier && !isValidComponentName(info.nodeIdentifier)) {
throw new CodeGeneratorError(`Invalid Identifier [${info.nodeIdentifier}]`);
}
});
const aliasDefineStatements: Record<string, string> = {};
if (useAliasName) {
Object.keys(exportItems).forEach((exportName) => {
const aliasList = exportItems[exportName]?.aliasNames || [];
if (aliasList.length > 0) {
const srcName = exportItems[exportName].needOriginExport ? exportName : aliasList[0];
const aliasNameList = exportItems[exportName].needOriginExport
? aliasList
: aliasList.slice(1);
aliasNameList.forEach((a) => {
if (!aliasDefineStatements[a]) {
aliasDefineStatements[a] = `const ${a} = ${srcName};`;
}
});
}
});
}
function getDefaultExportName(info: IDependencyItem): string {
if (info.isDefault) {
return defaultExportNames[0];
}
return info.exportName;
}
depsInfo.forEach((info) => {
// 如果是子组件,则导出父组件,并且根据自组件命名规则,判断是否需要定义标识符
if (info.nodeIdentifier) {
// 前提,存在 nodeIdentifier 一定是有 subName 的,不然前面会优化掉
const ownerName = getDependencyIdentifier(info);
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ImportAliasDefine,
content: useAliasName ? `const ${info.nodeIdentifier} = ${ownerName}.${info.subName};` : '',
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport, COMMON_CHUNK_NAME.InternalDepsImport],
ext: {
originalName: `${getDefaultExportName(info)}.${info.subName}`,
aliasName: info.nodeIdentifier,
dependency: info.source,
},
});
} else if (info.aliasName) {
// default 方式的导入会生成单独de import 语句,无需生成赋值语句
if (info.isDefault && defaultExportNames.find((n) => n === info.aliasName)) {
delete aliasDefineStatements[info.aliasName];
return;
}
let contentStatement = '';
if (aliasDefineStatements[info.aliasName]) {
contentStatement = aliasDefineStatements[info.aliasName];
delete aliasDefineStatements[info.aliasName];
}
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ImportAliasDefine,
content: contentStatement,
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport, COMMON_CHUNK_NAME.InternalDepsImport],
ext: {
originalName: getDefaultExportName(info),
aliasName: info.aliasName,
dependency: info.source,
},
});
}
});
// 可能会剩余一些存在二次转换的定义
Object.keys(aliasDefineStatements).forEach((a) => {
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ImportAliasDefine,
content: aliasDefineStatements[a],
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport, COMMON_CHUNK_NAME.InternalDepsImport],
});
});
const exportItemList = Object.keys(exportItems).map((k) => exportItems[k]);
const defaultExport = exportItemList.filter((item) => item.isDefault);
const otherExports = exportItemList.filter((item) => !item.isDefault);
const statementL = ['import'];
if (defaultExport.length > 0) {
if (useAliasName) {
statementL.push(defaultExportNames[0]);
} else {
statementL.push(defaultExport[0].aliasNames[0]);
}
if (otherExports.length > 0) {
statementL.push(', ');
}
}
if (otherExports.length > 0) {
const items = otherExports.map((item) => {
return !useAliasName || item.needOriginExport || item.aliasNames.length <= 0
? item.exportName
: `${item.exportName} as ${item.aliasNames[0]}`;
});
statementL.push(`{ ${items.join(', ')} }`);
}
statementL.push('from');
const getInternalDependencyModuleId = () => `@/${(deps[0] as IInternalDependency).type}/${pkg}`;
if (deps[0].dependencyType === DependencyType.Internal) {
// TODO: Internal Deps path use project slot setting
statementL.push(`'${getInternalDependencyModuleId()}';`);
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.InternalDepsImport,
content: statementL.join(' '),
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
});
} else {
statementL.push(`'${pkg}';`);
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: statementL.join(' '),
linkAfter: [],
});
}
// 处理下一些额外的 default 方式的导入
if (defaultExportNames.length > 1) {
if (deps[0].dependencyType === DependencyType.Internal) {
defaultExportNames.slice(1).forEach((exportName) => {
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.InternalDepsImport,
content: `import ${exportName} from '${getInternalDependencyModuleId()}';`,
linkAfter: [COMMON_CHUNK_NAME.ExternalDepsImport],
});
});
} else {
defaultExportNames.slice(1).forEach((exportName) => {
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ExternalDepsImport,
content: `import ${exportName} from '${pkg}';`,
linkAfter: [],
});
chunks.push({
type: ChunkType.STRING,
fileType: targetFileType,
name: COMMON_CHUNK_NAME.ImportAliasDefine,
content: '',
linkAfter: [],
ext: {
aliasName: exportName,
originalName: exportName,
dependency: {
package: pkg,
componentName: exportName,
},
},
});
});
}
}
return chunks;
}
export interface PluginConfig {
fileType?: string; // 导出的文件类型
useAliasName?: boolean; // 是否使用 componentName 重命名组件 identifier
filter?: (deps: IDependency[]) => IDependency[]; // 支持过滤能力
}
const pluginFactory: BuilderComponentPluginFactory<PluginConfig> = (config?: PluginConfig) => {
const cfg = {
fileType: FileType.JS,
useAliasName: true,
...(config || {}),
};
const plugin: BuilderComponentPlugin = async (pre: ICodeStruct) => {
const next: ICodeStruct = {
...pre,
};
const ir = next.ir as IWithDependency;
if (ir && ir.deps && ir.deps.length > 0) {
const deps = cfg.filter ? cfg.filter(ir.deps) : ir.deps;
const packs = groupDepsByPack(deps);
Object.keys(packs).forEach((pkg) => {
const chunks = buildPackageImport(pkg, packs[pkg], cfg.fileType, cfg.useAliasName);
next.chunks.push(...chunks);
});
}
return next;
};
return plugin;
};
export default pluginFactory;