feat: support localizing

This commit is contained in:
gengyang 2020-03-31 20:20:20 +08:00
parent 770a1b6c34
commit e1faa8452c
15 changed files with 8864 additions and 228 deletions

View File

@ -20,6 +20,7 @@
"jest-watch-typeahead": "^0.3.1",
"js-yaml": "^3.13.1",
"json-schema-to-typescript": "^8.2.0",
"tslib": "^1.11.1",
"typescript": "^3.8.3"
},
"scripts": {

View File

@ -46,7 +46,7 @@ export async function genManifest(
package: matScanModel.pkgName,
version: matScanModel.pkgVersion,
exportName: matParsedModel.componentName,
main: matScanModel.mainEntry,
main: matScanModel.entryFilePath,
destructuring: false,
subName: '',
},

View File

@ -10,17 +10,17 @@ import generate from './generate';
import parse from './parse';
import localize from './localize';
export default async function(
options: IMaterializeOptions,
): Promise<ComponentMeta[]> {
export default async function(options: IMaterializeOptions): Promise<ComponentMeta[]> {
const { accesser = 'local' } = options;
if (accesser === 'online') {
const { entry, cwd } = await localize(options);
const entry = await localize(options);
options.entry = entry;
options.cwd = cwd;
}
const scanedModel = await scan(options);
const parsedModel = await parse(scanedModel.modules[0]);
const parsedModel = await parse({
filePath: scanedModel.entryFilePath,
fileContent: scanedModel.entryFileContent,
});
const result = await generate(scanedModel, parsedModel);
return result;
}

View File

@ -33,13 +33,13 @@ export async function createFakePackage(params: {
pkgJsonFilePath,
JSON.stringify({
name: params.pkgName,
version: params.pkgVersion,
version: params.pkgVersion || '0.0.0',
dependencies: {
[params.pkgName]: params.pkgVersion,
[params.pkgName]: params.pkgVersion || 'latest',
},
}),
);
debugger;
// 安装依赖
const npmClient = params.npmClient || 'tnpm';
await spawn(npmClient, ['i'], { stdio: 'inherit', cwd: tempDir } as any);
@ -68,12 +68,12 @@ export async function createTempDir(): Promise<string> {
* @returns {{ [key: string]: any }}
* @memberof OnlineAccesser
*/
export function getPkgNameAndVersion(
pkgNameWithVersion: string,
): { [key: string]: any } {
export function getPkgNameAndVersion(pkgNameWithVersion: string): { [key: string]: any } {
const matches = pkgNameWithVersion.match(/(@\d+\.\d+\.\d+)$/);
if (!matches) {
throw new OtterError(`Illegal semver version: ${pkgNameWithVersion}`);
return {
name: pkgNameWithVersion,
};
}
const semverObj = semver.coerce(matches[0]);
const name = pkgNameWithVersion.replace(matches[0], '');
@ -84,12 +84,7 @@ export function getPkgNameAndVersion(
}
// 将问题转化为本地物料化场景
export default async function localize(
options: IMaterializeOptions,
): Promise<{
cwd: string;
entry: string;
}> {
export default async function localize(options: IMaterializeOptions): Promise<string> {
// 创建临时目录
const tempDir = await createTempDir();
// 创建组件包
@ -101,8 +96,5 @@ export default async function localize(
npmClient: options.npmClient,
});
return {
cwd: tempDir,
entry: join(tempDir, 'node_modules', name),
};
return join(tempDir, 'node_modules', name);
}

View File

@ -5,10 +5,7 @@ import { IMaterialParsedModel, IMaterialScanModel } from '../types';
import resolver from './resolver';
import handlers from './handlers';
export default function parse(params: {
fileContent: string;
filePath: string;
}): Promise<IMaterialParsedModel[]> {
export default function parse(params: { fileContent: string; filePath: string }): Promise<IMaterialParsedModel[]> {
const { fileContent, filePath } = params;
const result = reactDocs.parse(
fileContent,

View File

@ -49,10 +49,7 @@ export function transformType(type: any) {
} = type;
if (properties.length === 0) {
result.type = 'object';
} else if (
properties.length === 1 &&
typeof properties[0].key === 'object'
) {
} else if (properties.length === 1 && typeof properties[0].key === 'object') {
result.type = 'objectOf';
const v = transformType(properties[0].value);
if (typeof v.type === 'string') result.value = v.type;
@ -81,7 +78,7 @@ export function transformType(type: any) {
break;
case 'exact':
case 'shape':
result.value = Object.keys(value).map(n => {
result.value = Object.keys(value).map((n) => {
// tslint:disable-next-line:variable-name
const { name: _name, ...others } = value[n];
return transformItem(n, {
@ -110,13 +107,7 @@ export function transformType(type: any) {
}
export function transformItem(name: string, item: any) {
const {
description,
flowType,
type = flowType,
required,
defaultValue,
} = item;
const { description, flowType, type = flowType, required, defaultValue } = item;
const result: any = {
name,
propType: transformType({
@ -133,6 +124,9 @@ export function transformItem(name: string, item: any) {
result.defaultValue = value;
} catch (e) {}
}
if (result.propType === undefined) {
delete result.propType;
}
return result;
}

View File

@ -1,58 +1,53 @@
import { IMaterializeOptions, IMaterialScanModel, SourceType } from './types';
import { pathExists, readFile } from 'fs-extra';
import { pathExists, readFile, lstatSync } from 'fs-extra';
import { join } from 'path';
import { debug } from './otter-core';
const log = debug.extend('mat');
export default async function scan(
options: IMaterializeOptions,
): Promise<IMaterialScanModel> {
export default async function scan(options: IMaterializeOptions): Promise<IMaterialScanModel> {
const model: IMaterialScanModel = {
pkgName: '',
pkgVersion: '',
mainEntry: '',
sourceType: SourceType.MODULE,
modules: [],
entryFilePath: '',
entryFileContent: '',
};
log('options', options);
// 入口文件路径
let entryFilePath = null;
const cwd = options.cwd ? options.cwd : '';
const entry = options.entry;
const isDepsMode = cwd !== entry;
const pkgJsonPath = join(cwd, 'package.json');
// 判断是否存在 package.json
if (!(await pathExists(pkgJsonPath))) {
throw new Error(`Cannot find package.json. ${pkgJsonPath}`);
let entryFilePath = options.entry;
const stats = lstatSync(entryFilePath);
debugger;
if (!stats.isFile()) {
let mainFilePath = '';
const pkgJsonPath = join(entryFilePath, 'package.json');
// 判断是否存在 package.json
if (!(await pathExists(pkgJsonPath))) {
throw new Error(`Cannot find package.json. ${pkgJsonPath}`);
}
// 读取 package.json
let pkgJson = await resolvePkgJson(pkgJsonPath);
model.pkgName = pkgJson.name;
model.pkgVersion = pkgJson.version;
if (pkgJson.module) {
// 支持 es module
model.sourceType = SourceType.MODULE;
mainFilePath = pkgJson.module;
} else if (pkgJson.main) {
// 支持 commonjs
model.sourceType = SourceType.MAIN;
mainFilePath = pkgJson.main;
} else {
mainFilePath = './index.js';
}
entryFilePath = join(entryFilePath, mainFilePath);
}
// 读取 package.json
let pkgJson = await resolvePkgJson(pkgJsonPath);
model.pkgName = pkgJson.name;
model.pkgVersion = pkgJson.version;
if (isDepsMode) {
pkgJson = await resolvePkgJson(join(entry, 'package.json'));
}
if (pkgJson.module) {
// 支持 es module
model.sourceType = SourceType.MODULE;
entryFilePath = pkgJson.module;
} else if (pkgJson.main) {
// 支持 commonjs
model.sourceType = SourceType.MAIN;
entryFilePath = pkgJson.main;
} else {
entryFilePath = './index.js';
}
entryFilePath = join(isDepsMode ? entry : cwd, entryFilePath);
log('entryFilePath', entryFilePath);
const entryFile = await loadFile(entryFilePath);
log('entryFile', entryFile);
model.mainEntry = entryFilePath;
const entryFileContent = await loadFile(entryFilePath);
log('entryFile', entryFileContent);
model.entryFilePath = entryFilePath;
model.entryFileContent = entryFileContent;
// 记录入口文件
model.modules.push({
filePath: entryFilePath,
fileContent: entryFile,
});
log('model', model);
return model;
}
@ -65,9 +60,7 @@ export async function loadFile(filePath: string): Promise<string> {
return content.toString();
}
export async function resolvePkgJson(
pkgJsonPath: string,
): Promise<{ [k: string]: any }> {
export async function resolvePkgJson(pkgJsonPath: string): Promise<{ [k: string]: any }> {
const content = await loadFile(pkgJsonPath);
const json = JSON.parse(content);
return json;

View File

@ -2,15 +2,12 @@
*
*/
interface IMaterialScanModel {
/** 入口文件地址 */
mainEntry: string;
/** 标记物料组件包所使用的模块规范 */
sourceType: 'module' | 'main';
/** 每个文件对应的文件内容 */
modules: {
filePath: string;
fileContent: string;
}[];
/** 入口文件路径 */
entryFilePath: string;
/** 入口文件内容 */
entryFileContent: string;
/** 当前包名 */
pkgName: string;
/** 当前包版本 */

View File

@ -7,7 +7,7 @@ import IExtensionConfigManifest from './IExtensionConfigManifest';
*/
interface IMaterializeOptions {
/**
*
* ()
*
* /usr/project/src/container/DemoMaterial
* @ali/demo-material@0.0.1
@ -22,13 +22,6 @@ interface IMaterializeOptions {
*/
accesser: 'local' | 'online';
/**
* accesser=local /usr/.../demo-project
* @type {string}
* @memberof IMaterializeOptions
*/
cwd?: string;
/**
*
*/

File diff suppressed because it is too large Load Diff

View File

@ -9,22 +9,17 @@ Generated by [AVA](https://avajs.dev).
> Snapshot 1
{
mainEntry: '/Users/gengyang/code/frontend/low-code/ali-lowcode-engine/packages/material-parser/test/fixtures/multiple-exported-component/es/index.js',
modules: [
{
fileContent: `import AIMakeBlank from './basic/AIMakeBlank';␊
import AIMakeIcon from './basic/AIMakeIcon';␊
import AIMakeImage from './basic/AIMakeImage';␊
import AIMakeLink from './basic/AIMakeLink';␊
import AIMakePlaceholder from './basic/AIMakePlaceholder';␊
import AIMakeText from './basic/AIMakeText';␊
import Root from './basic/Root';␊
export { AIMakeBlank, AIMakeIcon, AIMakeImage, AIMakeLink, AIMakePlaceholder, AIMakeText, Root };␊
`,
filePath: '/Users/gengyang/code/frontend/low-code/ali-lowcode-engine/packages/material-parser/test/fixtures/multiple-exported-component/es/index.js',
},
],
entryFileContent: `import AIMakeBlank from './basic/AIMakeBlank';␊
import AIMakeIcon from './basic/AIMakeIcon';␊
import AIMakeImage from './basic/AIMakeImage';␊
import AIMakeLink from './basic/AIMakeLink';␊
import AIMakePlaceholder from './basic/AIMakePlaceholder';␊
import AIMakeText from './basic/AIMakeText';␊
import Root from './basic/Root';␊
export { AIMakeBlank, AIMakeIcon, AIMakeImage, AIMakeLink, AIMakePlaceholder, AIMakeText, Root };␊
`,
entryFilePath: '/Users/gengyang/code/frontend/low-code/ali-lowcode-engine-v0.8/packages/material-parser/test/fixtures/multiple-exported-component/es/index.js',
pkgName: 'multiple-exported-component',
pkgVersion: '1.0.0',
sourceType: 'module',
@ -35,97 +30,92 @@ Generated by [AVA](https://avajs.dev).
> Snapshot 1
{
mainEntry: '/Users/gengyang/code/frontend/low-code/ali-lowcode-engine/packages/material-parser/test/fixtures/single-exported-component/es/index.js',
modules: [
{
fileContent: `import _classCallCheck from "@babel/runtime/helpers/classCallCheck";␊
import _createClass from "@babel/runtime/helpers/createClass";␊
import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";␊
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";␊
import _inherits from "@babel/runtime/helpers/inherits";␊
/* eslint-disable react/no-unused-prop-types */␊
/* eslint-disable react/require-default-props */␊
import React from 'react';␊
import PropTypes from 'prop-types';␊
import "./main.css";␊
const Demo =␊
/* #__PURE__ */␊
function (_React$Component) {␊
_inherits(Demo, _React$Component);␊
function Demo() {␊
_classCallCheck(this, Demo);␊
return _possibleConstructorReturn(this, _getPrototypeOf(Demo).apply(this, arguments));␊
}␊
_createClass(Demo, [{␊
key: "render",␊
value: function render() {␊
return React.createElement("div", null, " Test ");␊
},␊
}]);␊
return Demo;␊
}(React.Component);␊
Demo.propTypes = {␊
optionalArray: PropTypes.array,␊
optionalBool: PropTypes.bool,␊
optionalFunc: PropTypes.func,␊
optionalNumber: PropTypes.number,␊
optionalObject: PropTypes.object,␊
optionalString: PropTypes.string,␊
optionalSymbol: PropTypes.symbol,␊
// Anything that can be rendered: numbers, strings, elements or an array␊
// (or fragment) containing these types.␊
optionalNode: PropTypes.node,␊
// A React element (ie. <MyComponent />).␊
optionalElement: PropTypes.element,␊
// A React element type (ie. MyComponent).␊
optionalElementType: PropTypes.elementType,␊
// You can also declare that a prop is an instance of a class. This uses␊
// JS's instanceof operator.␊
optionalMessage: PropTypes.instanceOf(Demo),␊
// You can ensure that your prop is limited to specific values by treating␊
// it as an enum.␊
optionalEnum: PropTypes.oneOf(['News', 'Photos']),␊
// An object that could be one of many types␊
optionalUnion: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Demo)]),␊
// An array of a certain type␊
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),␊
// An object with property values of a certain type␊
optionalObjectOf: PropTypes.objectOf(PropTypes.number),␊
// You can chain any of the above with `isRequired` to make sure a warning␊
// is shown if the prop isn't provided.␊
// An object taking on a particular shape␊
optionalObjectWithShape: PropTypes.shape({␊
optionalProperty: PropTypes.string,␊
requiredProperty: PropTypes.number.isRequired,␊
}),␊
optionalObjectWithShape2: PropTypes.shape({␊
optionalProperty: PropTypes.string,␊
requiredProperty: PropTypes.number.isRequired,␊
}).isRequired,␊
// An object with warnings on extra properties␊
optionalObjectWithStrictShape: PropTypes.exact({␊
optionalProperty: PropTypes.string,␊
requiredProperty: PropTypes.number.isRequired,␊
}),␊
requiredFunc: PropTypes.func.isRequired,␊
// A value of any data type␊
requiredAny: PropTypes.any.isRequired,␊
};␊
Demo.defaultProps = {␊
optionalNumber: 123,␊
};␊
export default Demo;`,
filePath: '/Users/gengyang/code/frontend/low-code/ali-lowcode-engine/packages/material-parser/test/fixtures/single-exported-component/es/index.js',
},
],
entryFileContent: `import _classCallCheck from "@babel/runtime/helpers/classCallCheck";␊
import _createClass from "@babel/runtime/helpers/createClass";␊
import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";␊
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";␊
import _inherits from "@babel/runtime/helpers/inherits";␊
/* eslint-disable react/no-unused-prop-types */␊
/* eslint-disable react/require-default-props */␊
import React from 'react';␊
import PropTypes from 'prop-types';␊
import "./main.css";␊
const Demo =␊
/* #__PURE__ */␊
function (_React$Component) {␊
_inherits(Demo, _React$Component);␊
function Demo() {␊
_classCallCheck(this, Demo);␊
return _possibleConstructorReturn(this, _getPrototypeOf(Demo).apply(this, arguments));␊
}␊
_createClass(Demo, [{␊
key: "render",␊
value: function render() {␊
return React.createElement("div", null, " Test ");␊
},␊
}]);␊
return Demo;␊
}(React.Component);␊
Demo.propTypes = {␊
optionalArray: PropTypes.array,␊
optionalBool: PropTypes.bool,␊
optionalFunc: PropTypes.func,␊
optionalNumber: PropTypes.number,␊
optionalObject: PropTypes.object,␊
optionalString: PropTypes.string,␊
optionalSymbol: PropTypes.symbol,␊
// Anything that can be rendered: numbers, strings, elements or an array␊
// (or fragment) containing these types.␊
optionalNode: PropTypes.node,␊
// A React element (ie. <MyComponent />).␊
optionalElement: PropTypes.element,␊
// A React element type (ie. MyComponent).␊
optionalElementType: PropTypes.elementType,␊
// You can also declare that a prop is an instance of a class. This uses␊
// JS's instanceof operator.␊
optionalMessage: PropTypes.instanceOf(Demo),␊
// You can ensure that your prop is limited to specific values by treating␊
// it as an enum.␊
optionalEnum: PropTypes.oneOf(['News', 'Photos']),␊
// An object that could be one of many types␊
optionalUnion: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.instanceOf(Demo)]),␊
// An array of a certain type␊
optionalArrayOf: PropTypes.arrayOf(PropTypes.number),␊
// An object with property values of a certain type␊
optionalObjectOf: PropTypes.objectOf(PropTypes.number),␊
// You can chain any of the above with `isRequired` to make sure a warning␊
// is shown if the prop isn't provided.␊
// An object taking on a particular shape␊
optionalObjectWithShape: PropTypes.shape({␊
optionalProperty: PropTypes.string,␊
requiredProperty: PropTypes.number.isRequired,␊
}),␊
optionalObjectWithShape2: PropTypes.shape({␊
optionalProperty: PropTypes.string,␊
requiredProperty: PropTypes.number.isRequired,␊
}).isRequired,␊
// An object with warnings on extra properties␊
optionalObjectWithStrictShape: PropTypes.exact({␊
optionalProperty: PropTypes.string,␊
requiredProperty: PropTypes.number.isRequired,␊
}),␊
requiredFunc: PropTypes.func.isRequired,␊
// A value of any data type␊
requiredAny: PropTypes.any.isRequired,␊
};␊
Demo.defaultProps = {␊
optionalNumber: 123,␊
};␊
export default Demo;`,
entryFilePath: '/Users/gengyang/code/frontend/low-code/ali-lowcode-engine-v0.8/packages/material-parser/test/fixtures/single-exported-component/es/index.js',
pkgName: 'single-exported-component',
pkgVersion: '1.0.0',
sourceType: 'module',

View File

@ -12,7 +12,6 @@ const tsComponent = getFromFixtures('ts-component');
test('materialize single exported component by local', async t => {
const options: IMaterializeOptions = {
cwd: singleExportedComptPath,
entry: singleExportedComptPath,
accesser: 'local',
};
@ -24,7 +23,6 @@ test('materialize single exported component by local', async t => {
test('materialize multiple exported component by local', async t => {
const options: IMaterializeOptions = {
cwd: multiExportedComptPath,
entry: multiExportedComptPath,
accesser: 'local',
};
@ -56,6 +54,17 @@ test('materialize multiple exported component by online', async t => {
t.snapshot(actual);
});
test('materialize @ali/lowcode-editor-skeleton by online', async t => {
const options: IMaterializeOptions = {
entry: '@ali/lowcode-editor-skeleton',
accesser: 'online',
};
const actual = await parse(options);
t.snapshot(actual);
});
test('ts component by local', async t => {
const options: IMaterializeOptions = {
entry: tsComponent,

View File

@ -8,7 +8,6 @@ const singleExportedComptPath = getFromFixtures('single-exported-component');
test.serial('scan multiple exported component', async t => {
const actual = await scan({
cwd: multiExportedComptPath,
entry: multiExportedComptPath,
accesser: 'local',
});
@ -18,7 +17,6 @@ test.serial('scan multiple exported component', async t => {
test.serial('scan single exported component', async t => {
const actual = await scan({
cwd: singleExportedComptPath,
entry: singleExportedComptPath,
accesser: 'local',
});