1
0
mirror of synced 2026-04-03 06:28:35 +08:00

refactor: make it public

This commit is contained in:
roymondchen
2022-02-17 14:47:39 +08:00
commit bc8b9f5225
360 changed files with 52539 additions and 0 deletions

View File

@@ -0,0 +1,416 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const { execSync } = require('child_process');
const { exit } = require('process');
const fs = require('fs');
const path = require('path');
const { parse, prettyPrint, types } = require('recast');
const n = types.namedTypes;
const pluginList = {};
let cwd = __dirname;
const pageRoot = () => path.resolve(cwd, '..');
const entryPath = () => path.resolve(pageRoot(), './src');
const makeCamelCase = function (name) {
if (typeof name !== 'string') {
return '';
}
return name.replace(/-(\w)/g, ($0, $1) => $1.toUpperCase());
};
const parseEntry = function ({ ast, package, indexPath }) {
if (!ast.program) {
console.log(`${package} 入口文件不合法`);
return exit(1);
}
const tokens = getASTTokenByTraverse({ ast, indexPath });
let { config, value, event } = tokens;
const { importComponentSource, importComponentToken } = tokens;
if (!config) {
console.log(`${package} config 文件声明不合法`);
return exit(1);
}
if (!value) {
console.log(`${package} value 文件声明不合法`);
return exit(1);
}
if (!event) {
// event 非必须,不需要 exit
console.log(`${package} event 文件声明缺失`);
}
const findIndex = importComponentToken.indexOf(exportDefaultToken);
let component = '';
if (findIndex > -1) {
component = path.resolve(path.dirname(indexPath), importComponentSource[findIndex]);
}
if (!component) {
console.log(`${package} component 文件声明不合法`);
return exit(1);
}
const reg = /^.*[/\\]node_modules[/\\](.*)/;
[, config] = config.match(reg) || [, config];
[, value] = value.match(reg) || [, value];
[, component] = component.match(reg) || [, component];
[, event] = event.match(reg) || [, event];
return {
config,
value,
component,
event,
};
};
const getASTTokenByTraverse = ({ ast, indexPath }) => {
let config = '';
let value = '';
let event = '';
const importComponentToken = [];
const importComponentSource = [];
types.visit(ast, {
visitImportDeclaration(p) {
const { node } = p;
const { specifiers, source } = node;
importComponentToken.push(specifiers[0].local.name);
importComponentSource.push(source.value);
this.traverse(p);
},
visitExportNamedDeclaration(p) {
const { node } = p;
const { specifiers, source } = node;
const name = specifiers[0].exported.name.toLowerCase();
if (name === 'value') {
value = path.resolve(path.dirname(indexPath), source.value);
} else if (name === 'config') {
config = path.resolve(path.dirname(indexPath), source.value);
} else if (name === 'event') {
event = path.resolve(path.dirname(indexPath), source.value);
}
this.traverse(p);
},
visitExportDefaultDeclaration(p) {
const { node } = p;
const { declaration } = node;
exportDefaultToken = declaration.name;
this.traverse(p);
},
});
return {
config,
value,
event,
importComponentToken,
importComponentSource,
};
};
const generateEntry = function ({ entries, type = 'build', componentFileAffix }) {
const commonArgs = { entries, componentFileAffix };
generateEntryFile({ entryFile: 'comp-entry.ts', type: 'component', ...commonArgs });
if (type === 'build') {
generateEntryFile({ entryFile: 'config-entry.ts', type: 'config', ...commonArgs });
generateEntryFile({ entryFile: 'value-entry.ts', type: 'value', ...commonArgs });
generateEntryFile({ entryFile: 'event-entry.ts', type: 'event', ...commonArgs });
}
};
const generateEntryFile = function ({ entries, entryFile, type, componentFileAffix }) {
const list = [];
const importDeclarations = [];
entries.forEach((entry) => {
if (!entry.entry[type]) return;
const name = makeCamelCase(entry.type);
importDeclarations.push(
`import ${name} from '${entry.entry[type]}${
type === 'component' && !entry.entry[type].includes(componentFileAffix) ? componentFileAffix : ''
}'`,
);
list.push(`'${entry.type}': ${name}`);
});
const exportToken = `${type}s`;
const capitalToken = exportToken.charAt(0).toUpperCase() + exportToken.slice(1);
let jsString = '';
let exportData = `window.magicPreset${capitalToken} = ${exportToken};`;
if (type === 'component') {
const pList = [];
Object.keys(pluginList).forEach((pluginType) => {
const name = makeCamelCase(pluginType);
importDeclarations.push(`import ${name} from '${pluginList[pluginType]}'`);
pList.push(`'${pluginType}': ${name}`);
});
exportData = `const plugins = {${pList.join(',')}};
const entry = {${exportToken}, plugins};
window.magicPreset${capitalToken} = entry;
export default entry;`;
jsString += `${importDeclarations.join(';')}
const ${exportToken}: Record<string, any> = {
${list.join(',')}
}
${exportData}`;
} else {
jsString += `${importDeclarations.join(';')}
(function(){
const ${exportToken}: Record<string, any> = {
${list.join(',')}
}
${exportData}
})()`;
}
fs.writeFileSync(
path.resolve(entryPath(), entryFile),
prettyPrint(parse(jsString, { parser: require('recast/parsers/typescript') }), {
tabWidth: 2,
trailingComma: true,
quote: 'single',
}).code,
{ encoding: 'utf-8' },
);
};
const installPackage = function (package) {
try {
execSync(`node -e "require.resolve('${package}')"`, { stdio: 'ignore' });
} catch (e) {
execSync(`npm install ${package} --registry https://mirrors.tencent.com/npm/`, {
stdio: 'inherit',
cwd: pageRoot(),
});
}
};
const getIndexPath = function (entry) {
if (fs.lstatSync(entry).isFile()) {
return entry;
}
if (fs.lstatSync(entry).isDirectory()) {
const files = fs.readdirSync(entry);
const [index] = files.filter((file) => file.split('.')[0] === 'index');
return path.resolve(entry, index);
}
return entry;
};
const typeComponent = 1;
const typePlugin = 2;
const typeComponentPackage = 3;
const getComponentPackageImports = function ({ result, properties, indexPath, importDeclarations }) {
result.type = typeComponentPackage;
result.imports = [];
properties.forEach((property) => {
const [propertyMatch] = importDeclarations.filter((i) => {
const [specifier] = i.specifiers;
if (n.ImportDefaultSpecifier.check(specifier) && specifier.local.name === property.value.name) {
return true;
}
return false;
});
if (propertyMatch) {
result.imports.push({
type: property.key.name,
name: propertyMatch.specifiers[0].local.name,
indexPath: getIndexPath(path.resolve(path.dirname(indexPath), propertyMatch.source.value)),
});
}
});
return result;
};
const isPlugin = function (properties) {
const [match] = properties.filter((property) => property.key.name === 'install');
return !!match;
};
/**
* 1 判断是否组件包
* 2 判断是组件还是插件
* 3 组件插件分开写入 comp-entry.ts
* @param {*} ast
* @param {String} indexPath
* @return {Object} { type: '', imports: [] } 返回传入组件的类型。如果是组件包imports 中包含所有子组件的入口文件路径
*/
const typeAssertion = function ({ ast, indexPath }) {
const result = {
type: '',
imports: [],
};
const { importDeclarations, variableDeclarations, exportDefaultName, exportDefaultNode } =
getAssertionTokenByTraverse(ast);
if (exportDefaultName) {
importDeclarations.every((node) => {
const [specifier] = node.specifiers;
// 从 import 语句中找到 export default 的变量,认为是组件
if (n.ImportDefaultSpecifier.check(specifier) && specifier.local.name === exportDefaultName) {
result.type = typeComponent;
return false;
}
return true;
});
if (result.type) return result;
variableDeclarations.every((node) => {
const [variable] = node.declarations;
// 从声明变量语句中找到 export default 的变量,认为是组件包
if (
n.Identifier.check(variable.id) &&
variable.id.name === exportDefaultName &&
n.ObjectExpression.check(variable.init)
) {
if (isPlugin(variable.init.properties)) {
result.type = typePlugin;
return false;
}
// 从组件包声明中找到对应子组件入口文件
getComponentPackageImports({ result, properties: variable.init.properties, indexPath, importDeclarations });
}
return true;
});
}
if (exportDefaultNode) {
if (isPlugin(exportDefaultNode.properties)) {
result.type = typePlugin;
} else {
getComponentPackageImports({ result, properties: variable.init.properties, indexPath, importDeclarations });
}
}
return result;
};
const getAssertionTokenByTraverse = (ast) => {
const importDeclarations = [];
const variableDeclarations = [];
let exportDefaultName = '';
let exportDefaultNode = undefined;
types.visit(ast, {
visitImportDeclaration(p) {
importDeclarations.push(p.node);
this.traverse(p);
},
visitVariableDeclaration(p) {
variableDeclarations.push(p.node);
this.traverse(p);
},
visitExportDefaultDeclaration(p) {
const { node } = p;
const { declaration } = node;
// 导出的是变量名
if (n.Identifier.check(declaration)) {
exportDefaultName = declaration.name;
}
// 导出的是对象的字面量
if (n.ObjectExpression.check(declaration)) {
exportDefaultNode = declaration;
}
this.traverse(p);
},
});
return {
importDeclarations,
variableDeclarations,
exportDefaultName,
exportDefaultNode,
};
};
const start = function ({ type, componentFileAffix, units, workingDir }) {
cwd = workingDir;
const entries = [];
Object.keys(units).forEach((componentType) => {
const package = units[componentType];
installPackage(package);
const indexPath = require.resolve(package);
const indexCode = fs.readFileSync(indexPath, { encoding: 'utf-8', flag: 'r' });
const ast = parse(indexCode, { parser: require('recast/parsers/typescript') });
const result = typeAssertion({ ast, indexPath });
if (result.type === typeComponent) {
// 组件
const entry = parseEntry({ ast, package, indexPath });
entries.push({ type: componentType, entry });
} else if (result.type === typeComponentPackage) {
// 组件包
result.imports.forEach((i) => {
const affixReg = new RegExp(`${componentFileAffix}$`);
if (affixReg.test(i.indexPath)) {
entries.push({ type: i.type, entry: { component: i.indexPath } });
} else {
const indexCode = fs.readFileSync(i.indexPath, { encoding: 'utf-8', flag: 'r' });
const ast = parse(indexCode);
const entry = parseEntry({ ast, package: `${package} | ${i.name}`, indexPath: i.indexPath });
entries.push({ type: i.type, entry });
}
});
} else if (result.type === typePlugin) {
// 插件
pluginList[componentType] = package;
}
});
generateEntry({ entries, type, componentFileAffix });
};
module.exports = start;