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", "jest-watch-typeahead": "^0.3.1",
"js-yaml": "^3.13.1", "js-yaml": "^3.13.1",
"json-schema-to-typescript": "^8.2.0", "json-schema-to-typescript": "^8.2.0",
"tslib": "^1.11.1",
"typescript": "^3.8.3" "typescript": "^3.8.3"
}, },
"scripts": { "scripts": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,6 @@ const tsComponent = getFromFixtures('ts-component');
test('materialize single exported component by local', async t => { test('materialize single exported component by local', async t => {
const options: IMaterializeOptions = { const options: IMaterializeOptions = {
cwd: singleExportedComptPath,
entry: singleExportedComptPath, entry: singleExportedComptPath,
accesser: 'local', accesser: 'local',
}; };
@ -24,7 +23,6 @@ test('materialize single exported component by local', async t => {
test('materialize multiple exported component by local', async t => { test('materialize multiple exported component by local', async t => {
const options: IMaterializeOptions = { const options: IMaterializeOptions = {
cwd: multiExportedComptPath,
entry: multiExportedComptPath, entry: multiExportedComptPath,
accesser: 'local', accesser: 'local',
}; };
@ -56,6 +54,17 @@ test('materialize multiple exported component by online', async t => {
t.snapshot(actual); 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 => { test('ts component by local', async t => {
const options: IMaterializeOptions = { const options: IMaterializeOptions = {
entry: tsComponent, entry: tsComponent,

View File

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