/** * 解析器是对输入的固定格式数据做拆解,使其符合引擎后续步骤预期,完成统一处理逻辑的步骤。 * 本解析器面向的是标准 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 { 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 = {}; const internalDeps: Record = {}; 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( 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(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(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( 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(); // 数据源的默认类型为 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;