mirror of
https://github.com/alibaba/lowcode-engine.git
synced 2025-12-14 13:03:07 +00:00
* fix: 解决 package.json 中误添加了没有用到的数据源类型的 handler 的包的问题 (#56) * test: 更新别的相关测试用例的期望值 * fix: 🐛 去掉 npm 上没有的依赖 (#68) * chore: 🤖 fix release:beta script * chore(release): 1.0.2-beta.0 * fix: 🐛 补充 icejs 模板中缺失的依赖包 * chore(release): 1.0.2-beta.1 * chore(release): 1.0.2
378 lines
11 KiB
TypeScript
378 lines
11 KiB
TypeScript
/**
|
|
* 解析器是对输入的固定格式数据做拆解,使其符合引擎后续步骤预期,完成统一处理逻辑的步骤。
|
|
* 本解析器面向的是标准 schema 协议。
|
|
*/
|
|
import changeCase from 'change-case';
|
|
import {
|
|
UtilItem,
|
|
NodeDataType,
|
|
NodeSchema,
|
|
ContainerSchema,
|
|
ProjectSchema,
|
|
PropsMap,
|
|
NodeData,
|
|
NpmInfo,
|
|
} from '@alilc/lowcode-types';
|
|
import {
|
|
IPageMeta,
|
|
CodeGeneratorError,
|
|
CompatibilityError,
|
|
DependencyType,
|
|
IContainerInfo,
|
|
IDependency,
|
|
IExternalDependency,
|
|
IInternalDependency,
|
|
InternalDependencyType,
|
|
IParseResult,
|
|
ISchemaParser,
|
|
INpmPackage,
|
|
IRouterInfo,
|
|
} from '../types';
|
|
|
|
import { SUPPORT_SCHEMA_VERSION_LIST } from '../const';
|
|
|
|
import { getErrorMessage } from '../utils/errors';
|
|
import { handleSubNodes } from '../utils/schema';
|
|
import { uniqueArray } from '../utils/common';
|
|
import { componentAnalyzer } from '../analyzer/componentAnalyzer';
|
|
import { ensureValidClassName } from '../utils/validate';
|
|
|
|
const defaultContainer: IContainerInfo = {
|
|
containerType: 'Component',
|
|
componentName: 'Component',
|
|
moduleName: 'Index',
|
|
fileName: 'Index',
|
|
css: '',
|
|
props: {},
|
|
};
|
|
|
|
function getRootComponentName(typeName: string, maps: Record<string, IExternalDependency>): string {
|
|
if (maps[typeName]) {
|
|
const rec = maps[typeName];
|
|
if (rec.destructuring) {
|
|
return rec.componentName || typeName;
|
|
}
|
|
|
|
const peerName = Object.keys(maps).find((depName: string) => {
|
|
const depInfo = maps[depName];
|
|
return (
|
|
depName !== typeName &&
|
|
!depInfo.destructuring &&
|
|
depInfo.package === rec.package &&
|
|
depInfo.version === rec.version &&
|
|
depInfo.main === rec.main &&
|
|
depInfo.exportName === rec.exportName &&
|
|
depInfo.subName === rec.subName
|
|
);
|
|
});
|
|
|
|
return peerName || typeName;
|
|
}
|
|
return typeName;
|
|
}
|
|
|
|
function processChildren(schema: NodeSchema): void {
|
|
if (schema.props) {
|
|
if (Array.isArray(schema.props)) {
|
|
// FIXME: is array type props description
|
|
} else {
|
|
const nodeProps = schema.props as PropsMap;
|
|
if (nodeProps.children) {
|
|
if (!schema.children) {
|
|
// eslint-disable-next-line no-param-reassign
|
|
schema.children = nodeProps.children as NodeDataType;
|
|
} else {
|
|
let _children: NodeData[] = [];
|
|
|
|
if (Array.isArray(schema.children)) {
|
|
_children = _children.concat(schema.children);
|
|
} else {
|
|
_children.push(schema.children);
|
|
}
|
|
|
|
if (Array.isArray(nodeProps.children)) {
|
|
_children = _children.concat(nodeProps.children as NodeData[]);
|
|
} else {
|
|
_children.push(nodeProps.children as NodeData);
|
|
}
|
|
|
|
// eslint-disable-next-line no-param-reassign
|
|
schema.children = _children;
|
|
}
|
|
delete nodeProps.children;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export class SchemaParser implements ISchemaParser {
|
|
validate(schema: ProjectSchema): boolean {
|
|
if (SUPPORT_SCHEMA_VERSION_LIST.indexOf(schema.version) < 0) {
|
|
throw new CompatibilityError(`Not support schema with version [${schema.version}]`);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
parse(schemaSrc: ProjectSchema | string): IParseResult {
|
|
// TODO: collect utils depends in JSExpression
|
|
const compDeps: Record<string, IExternalDependency> = {};
|
|
const internalDeps: Record<string, IInternalDependency> = {};
|
|
let utilsDeps: IExternalDependency[] = [];
|
|
|
|
const schema = this.decodeSchema(schemaSrc);
|
|
|
|
// 解析三方组件依赖
|
|
schema.componentsMap.forEach((info) => {
|
|
if (info.componentName) {
|
|
compDeps[info.componentName] = {
|
|
...info,
|
|
dependencyType: DependencyType.External,
|
|
componentName: info.componentName,
|
|
exportName: info.exportName ?? info.componentName,
|
|
version: info.version || '*',
|
|
destructuring: info.destructuring ?? false,
|
|
};
|
|
}
|
|
});
|
|
|
|
let containers: IContainerInfo[];
|
|
// Test if this is a lowcode component without container
|
|
if (schema.componentsTree.length > 0) {
|
|
const firstRoot: ContainerSchema = schema.componentsTree[0] as ContainerSchema;
|
|
|
|
if (!('fileName' in firstRoot) || !firstRoot.fileName) {
|
|
// 整个 schema 描述一个容器,且无根节点定义
|
|
const container: IContainerInfo = {
|
|
...firstRoot,
|
|
...defaultContainer,
|
|
props: firstRoot.props || defaultContainer.props,
|
|
css: firstRoot.css || defaultContainer.css,
|
|
moduleName: (firstRoot as IContainerInfo).moduleName || defaultContainer.moduleName,
|
|
children: schema.componentsTree as NodeSchema[],
|
|
};
|
|
containers = [container];
|
|
} else {
|
|
// 普通带 1 到多个容器的 schema
|
|
containers = schema.componentsTree.map((n) => {
|
|
const subRoot = n as ContainerSchema;
|
|
const container: IContainerInfo = {
|
|
...subRoot,
|
|
componentName: getRootComponentName(subRoot.componentName, compDeps),
|
|
containerType: subRoot.componentName,
|
|
moduleName: ensureValidClassName(changeCase.pascalCase(subRoot.fileName)),
|
|
};
|
|
return container;
|
|
});
|
|
}
|
|
} else {
|
|
throw new CodeGeneratorError("Can't find anything to generate.");
|
|
}
|
|
|
|
// 分析引用能力的依赖
|
|
containers = containers.map((con) => ({
|
|
...con,
|
|
analyzeResult: componentAnalyzer(con as ContainerSchema),
|
|
}));
|
|
|
|
// 建立所有容器的内部依赖索引
|
|
containers.forEach((container) => {
|
|
let type;
|
|
switch (container.containerType) {
|
|
case 'Page':
|
|
type = InternalDependencyType.PAGE;
|
|
break;
|
|
case 'Block':
|
|
type = InternalDependencyType.BLOCK;
|
|
break;
|
|
default:
|
|
type = InternalDependencyType.COMPONENT;
|
|
break;
|
|
}
|
|
|
|
const dep: IInternalDependency = {
|
|
type,
|
|
moduleName: container.moduleName,
|
|
destructuring: false,
|
|
exportName: container.moduleName,
|
|
dependencyType: DependencyType.Internal,
|
|
};
|
|
|
|
internalDeps[dep.moduleName] = dep;
|
|
});
|
|
|
|
const containersDeps = ([] as IDependency[]).concat(...containers.map((c) => c.deps || []));
|
|
// TODO: 不应该在出码部分解决?
|
|
// 处理 children 写在了 props 里的情况
|
|
containers.forEach((container) => {
|
|
if (container.children) {
|
|
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
|
|
handleSubNodes<void>(
|
|
container.children,
|
|
{
|
|
node: (i: NodeSchema) => processChildren(i),
|
|
},
|
|
{
|
|
rerun: true,
|
|
},
|
|
);
|
|
}
|
|
});
|
|
|
|
// 分析容器内部组件依赖
|
|
containers.forEach((container) => {
|
|
const depNames = this.getComponentNames(container);
|
|
// eslint-disable-next-line no-param-reassign
|
|
container.deps = uniqueArray<string>(depNames, (i: string) => i)
|
|
.map((depName) => internalDeps[depName] || compDeps[depName])
|
|
.filter(Boolean);
|
|
// container.deps = Object.keys(compDeps).map((depName) => compDeps[depName]);
|
|
});
|
|
|
|
// 分析路由配置
|
|
const routes: IRouterInfo['routes'] = containers
|
|
.filter((container) => container.containerType === 'Page')
|
|
.map((page) => {
|
|
const { meta } = page;
|
|
if (meta) {
|
|
return {
|
|
path: (meta as IPageMeta).router || `/${page.fileName}`, // 如果无法找到页面路由信息,则用 fileName 做兜底
|
|
fileName: page.fileName,
|
|
componentName: page.moduleName,
|
|
};
|
|
}
|
|
|
|
return {
|
|
path: '',
|
|
fileName: page.fileName,
|
|
componentName: page.moduleName,
|
|
};
|
|
});
|
|
|
|
const routerDeps = routes
|
|
.map((r) => internalDeps[r.componentName] || compDeps[r.componentName])
|
|
.filter((dep) => !!dep);
|
|
|
|
// 分析 Utils 依赖
|
|
let utils: UtilItem[];
|
|
if (schema.utils) {
|
|
utils = schema.utils;
|
|
utilsDeps = schema.utils
|
|
.filter(
|
|
(u): u is { name: string; type: 'npm' | 'tnpm'; content: NpmInfo } =>
|
|
u.type !== 'function',
|
|
)
|
|
.map(
|
|
(u): IExternalDependency => ({
|
|
...u.content,
|
|
componentName: u.name,
|
|
version: u.content.version || '*',
|
|
destructuring: u.content.destructuring ?? false,
|
|
exportName: u.content.exportName ?? u.name,
|
|
}),
|
|
);
|
|
} else {
|
|
utils = [];
|
|
}
|
|
|
|
// 分析项目 npm 依赖
|
|
let npms: INpmPackage[] = [];
|
|
containers.forEach((con) => {
|
|
const p = (con.deps || [])
|
|
.map((dep) => {
|
|
return dep.dependencyType === DependencyType.External ? dep : null;
|
|
})
|
|
.filter((dep) => dep !== null);
|
|
const npmInfos: INpmPackage[] = p.filter(Boolean).map((i) => ({
|
|
package: (i as IExternalDependency).package,
|
|
version: (i as IExternalDependency).version,
|
|
}));
|
|
npms.push(...npmInfos);
|
|
});
|
|
|
|
npms.push(
|
|
...utilsDeps.map((utilsDep) => ({
|
|
package: utilsDep.package,
|
|
version: utilsDep.version,
|
|
})),
|
|
);
|
|
|
|
npms = uniqueArray<INpmPackage>(npms, (i) => i.package).filter(Boolean);
|
|
|
|
return {
|
|
containers,
|
|
globalUtils: {
|
|
utils,
|
|
deps: utilsDeps,
|
|
},
|
|
globalI18n: schema.i18n,
|
|
globalRouter: {
|
|
routes,
|
|
deps: routerDeps,
|
|
},
|
|
project: {
|
|
css: schema.css,
|
|
constants: schema.constants,
|
|
config: schema.config || {},
|
|
meta: schema.meta || {},
|
|
i18n: schema.i18n,
|
|
containersDeps,
|
|
utilsDeps,
|
|
packages: npms || [],
|
|
dataSourcesTypes: this.collectDataSourcesTypes(schema),
|
|
},
|
|
};
|
|
}
|
|
|
|
getComponentNames(children: NodeDataType): string[] {
|
|
return handleSubNodes<string>(
|
|
children,
|
|
{
|
|
node: (i: NodeSchema) => i.componentName,
|
|
},
|
|
{
|
|
rerun: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
decodeSchema(schemaSrc: string | ProjectSchema): ProjectSchema {
|
|
let schema: ProjectSchema;
|
|
if (typeof schemaSrc === 'string') {
|
|
try {
|
|
schema = JSON.parse(schemaSrc);
|
|
} catch (error) {
|
|
throw new CodeGeneratorError(
|
|
`Parse schema failed: ${getErrorMessage(error) || 'unknown reason'}`,
|
|
);
|
|
}
|
|
} else {
|
|
schema = schemaSrc;
|
|
}
|
|
return schema;
|
|
}
|
|
|
|
private collectDataSourcesTypes(schema: ProjectSchema): string[] {
|
|
const dataSourcesTypes = new Set<string>();
|
|
|
|
// 数据源的默认类型为 fetch
|
|
const defaultDataSourceType = 'fetch';
|
|
|
|
// 收集应用级别的数据源
|
|
schema.dataSource?.list?.forEach((ds) => {
|
|
dataSourcesTypes.add(ds.type || defaultDataSourceType);
|
|
});
|
|
|
|
// 收集容器级别的数据源(页面/组件/区块)
|
|
schema.componentsTree.forEach((rootNode) => {
|
|
rootNode.dataSource?.list?.forEach((ds) => {
|
|
dataSourcesTypes.add(ds.type || defaultDataSourceType);
|
|
});
|
|
});
|
|
|
|
return Array.from(dataSourcesTypes.values());
|
|
}
|
|
}
|
|
|
|
export default SchemaParser;
|