Merge pull request #215 from webzard-io/refactor/moduleid

Move addModuleId to Editor
This commit is contained in:
yz-yu 2022-01-21 00:25:27 +08:00 committed by GitHub
commit 509a2585f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 280 additions and 135 deletions

View File

@ -20,7 +20,7 @@
},
"impl": [
{
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"type": "chakra_ui/v1/hstack",
"properties": {
"spacing": "24px"
@ -28,7 +28,7 @@
"traits": []
},
{
"id": "name",
"id": "{{ $moduleId }}__nameText",
"type": "core/v1/text",
"properties": {
"value": {
@ -41,7 +41,7 @@
"type": "core/v1/slot",
"properties": {
"container": {
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
@ -49,7 +49,7 @@
]
},
{
"id": "email",
"id": "{{ $moduleId }}__emailText",
"type": "core/v1/text",
"properties": {
"value": {
@ -62,7 +62,7 @@
"type": "core/v1/slot",
"properties": {
"container": {
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
@ -70,7 +70,7 @@
]
},
{
"id": "deleteButton",
"id": "{{ $moduleId }}__deleteButton",
"type": "chakra_ui/v1/button",
"properties": {
"text": {
@ -106,7 +106,7 @@
"type": "core/v1/slot",
"properties": {
"container": {
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
@ -114,7 +114,7 @@
]
},
{
"id": "editButton",
"id": "{{ $moduleId }}__editButton",
"type": "chakra_ui/v1/button",
"properties": {
"text": {
@ -150,7 +150,7 @@
"type": "core/v1/slot",
"properties": {
"container": {
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}

View File

@ -13,18 +13,18 @@
"properties": {},
"events": ["onEdit"],
"stateMap": {
"value": "input.value"
"value": "{{ $moduleId }}__input.value"
}
},
"impl": [
{
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"type": "chakra_ui/v1/hstack",
"properties": {},
"traits": []
},
{
"id": "text",
"id": "{{ $moduleId }}__text",
"type": "core/v1/text",
"properties": {
"value": {
@ -37,7 +37,7 @@
"type": "core/v1/slot",
"properties": {
"container": {
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
@ -45,11 +45,11 @@
]
},
{
"id": "inputValueText",
"id": "{{ $moduleId }}__inputValueText",
"type": "core/v1/text",
"properties": {
"value": {
"raw": "**{{ input.value }}**",
"raw": "**{{ {{ $moduleId }}__input.value }}**",
"format": "md"
}
},
@ -58,7 +58,7 @@
"type": "core/v1/slot",
"properties": {
"container": {
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
@ -66,7 +66,7 @@
]
},
{
"id": "input",
"id": "{{ $moduleId }}__input",
"type": "chakra_ui/v1/input",
"properties": {},
"traits": [
@ -74,7 +74,7 @@
"type": "core/v1/slot",
"properties": {
"container": {
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
@ -82,7 +82,7 @@
]
},
{
"id": "button",
"id": "{{ $moduleId }}__button",
"type": "chakra_ui/v1/button",
"properties": {
"text": {
@ -114,7 +114,7 @@
"type": "core/v1/slot",
"properties": {
"container": {
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}

View File

@ -18,18 +18,18 @@
},
"events": ["onSubmit"],
"stateMap": {
"value": "input.value"
"value": "{{ $moduleId }}__input.value"
}
},
"impl": [
{
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"type": "chakra_ui/v1/hstack",
"properties": {},
"traits": []
},
{
"id": "input",
"id": "{{ $moduleId }}__input",
"type": "chakra_ui/v1/input",
"properties": {},
"traits": [
@ -37,7 +37,7 @@
"type": "core/v1/slot",
"properties": {
"container": {
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}
@ -45,11 +45,11 @@
]
},
{
"id": "button",
"id": "{{ $moduleId }}__button",
"type": "chakra_ui/v1/button",
"properties": {
"text": {
"raw": "{{input.value}}",
"raw": "{{{{ $moduleId }}__input.value}}",
"format": "md"
}
},
@ -77,7 +77,7 @@
"type": "core/v1/slot",
"properties": {
"container": {
"id": "hstack",
"id": "{{ $moduleId }}__hstack",
"slot": "content"
}
}

View File

@ -2,6 +2,7 @@ import { JSONSchema7Object } from 'json-schema';
import { parseVersion } from './version';
import { Metadata } from './metadata';
import { Version } from './version';
import { ComponentSchema } from './application';
// spec
@ -10,6 +11,7 @@ export type Module = {
kind: 'Module';
metadata: Metadata;
spec: ModuleSpec;
impl: ComponentSchema[];
};
type ModuleSpec = {
@ -28,6 +30,7 @@ type CreateModuleOptions = {
version: string;
metadata: Metadata;
spec?: Partial<ModuleSpec>;
impl?: ComponentSchema[];
};
export function createModule(options: CreateModuleOptions): RuntimeModule {
@ -45,5 +48,6 @@ export function createModule(options: CreateModuleOptions): RuntimeModule {
stateMap: {},
...options.spec
},
impl: options.impl || [],
};
}

View File

@ -0,0 +1,118 @@
import { Type } from '@sinclair/typebox';
import { addModuleId, removeModuleId } from '../src/utils/addModuleId';
describe('format to module schema', () => {
it('will add module id to the expression', () => {
expect(
addModuleId({
version: 'test/v1',
kind: 'Module',
metadata: {
name: 'test',
},
spec: {
properties: {
value: Type.Object({
BE_CAREFUL: Type.Number(),
}),
},
events: [],
stateMap: {},
},
impl: [
{
id: 'input1',
type: 'test/v1/input',
properties: {},
traits: [],
},
{
id: 'BE_CAREFUL',
type: 'test/v1/child',
properties: {
test: '{{ value.BE_CAREFUL.toFixed(2) }}',
add: '{{ input1.value }} / {{ BE_CAREFUL.value }}'
},
traits: [],
},
],
}).impl
).toMatchInlineSnapshot(`
Array [
Object {
"id": "{{ $moduleId }}__input1",
"properties": Object {},
"traits": Array [],
"type": "test/v1/input",
},
Object {
"id": "{{ $moduleId }}__BE_CAREFUL",
"properties": Object {
"add": "{{ {{ $moduleId }}__input1.value }} / {{ {{ $moduleId }}__BE_CAREFUL.value }}",
"test": "{{ value.BE_CAREFUL.toFixed(2) }}",
},
"traits": Array [],
"type": "test/v1/child",
},
]
`);
});
it('will remove module id in expression', () => {
expect(
removeModuleId({
version: 'test/v1',
kind: 'Module',
metadata: {
name: 'test',
},
spec: {
properties: {
value: Type.Object({
BE_CAREFUL: Type.Number(),
}),
},
events: [],
stateMap: {},
},
impl: [
{
id: '{{ $moduleId }}__input1',
type: 'test/v1/input',
properties: {},
traits: [],
},
{
id: 'BE_CAREFUL',
type: 'test/v1/child',
properties: {
test: '{{ value.BE_CAREFUL.toFixed(2) }}',
add: '{{ {{ $moduleId }}__input1.value }} / {{ {{ $moduleId }}__BE_CAREFUL.value }}'
},
traits: [],
},
],
}).impl
).toMatchInlineSnapshot(`
Array [
Object {
"id": "input1",
"properties": Object {},
"traits": Array [],
"type": "test/v1/input",
},
Object {
"id": "BE_CAREFUL",
"properties": Object {
"add": "{{ input1.value }} / {{ BE_CAREFUL.value }}",
"test": "{{ value.BE_CAREFUL.toFixed(2) }}",
},
"traits": Array [],
"type": "test/v1/child",
},
]
`);
});
});

View File

@ -3,6 +3,8 @@ import { Application, ComponentSchema } from '@sunmao-ui/core';
import { ImplementedRuntimeModule } from '@sunmao-ui/runtime';
import { produce } from 'immer';
import { DefaultNewModule, EmptyAppSchema } from './constants';
import { addModuleId, removeModuleId } from './utils/addModuleId';
import { cloneDeep } from 'lodash-es';
export class AppStorage {
app: Application = this.getDefaultAppFromLS();
@ -35,7 +37,7 @@ export class AppStorage {
try {
const modulesFromLS = localStorage.getItem(AppStorage.ModulesLSKey);
if (modulesFromLS) {
return JSON.parse(modulesFromLS);
return JSON.parse(modulesFromLS).map(removeModuleId);
}
return [];
} catch (error) {
@ -122,7 +124,8 @@ export class AppStorage {
}
private saveModulesInLS() {
localStorage.setItem(AppStorage.ModulesLSKey, JSON.stringify(this.modules));
const modules = cloneDeep(this.modules).map(addModuleId)
localStorage.setItem(AppStorage.ModulesLSKey, JSON.stringify(modules));
}
setApp(app: Application) {

View File

@ -1,9 +1,11 @@
import { action, makeAutoObservable, observable, reaction, toJS } from 'mobx';
import { ComponentSchema } from '@sunmao-ui/core';
import { ComponentSchema, createModule } from '@sunmao-ui/core';
import { eventBus } from './eventBus';
import { AppStorage } from './AppStorage';
import { registry, stateManager } from './setup';
import { SchemaValidator } from './validator';
import { addModuleId } from './utils/addModuleId';
import { cloneDeep } from 'lodash-es';
type EditingTarget = {
kind: 'app' | 'module';
@ -41,7 +43,7 @@ class EditorStore {
get selectedComponent() {
return this.components.find(c => c.id === this._selectedComponentId);
}
// to avoid get out-of-dated value here, we should use getter to lazy load primitive type
get hoverComponentId() {
return this._hoverComponentId;
@ -119,7 +121,10 @@ class EditorStore {
clearSunmaoGlobalState() {
stateManager.clear();
// reregister all modules
this.modules.forEach(m => registry.registerModule(m, true));
this.modules.forEach(m => {
const modules = createModule(addModuleId(cloneDeep(m)));
registry.registerModule(modules, true);
});
}
saveCurrentComponents() {

View File

@ -118,6 +118,7 @@ export const EmptyAppSchema: Application = {
},
};
// need not add moduleId, because it is used in runtime of editor
export const DefaultNewModule: ImplementedRuntimeModule = {
kind: 'Module',
parsedVersion: { category: 'custom/v1', value: 'myModule' },

View File

@ -0,0 +1,116 @@
import { Module } from '@sunmao-ui/core';
import * as acorn from 'acorn';
import * as acornLoose from 'acorn-loose';
import { simple as simpleWalk } from 'acorn-walk';
const ModuleIdPrefix = '{{ $moduleId }}__';
type StringPos = {
start: number;
end: number;
replaceStr: string;
};
// add {{$moduleId}} in moduleSchema
export function addModuleId(module: Module): Module {
const ids: string[] = [];
module.impl.forEach(c => {
ids.push(c.id);
if (c.type === 'core/v1/moduleContainer') {
ids.push(c.properties.id as string);
}
if (c.type === 'chakra_ui/v1/list') {
ids.push((c.properties.template as any).id);
}
});
function traverse(tree: Record<string, any>) {
for (const key in tree) {
const val = tree[key];
if (typeof val === 'string') {
if (ids.includes(val)) {
tree[key] = `${ModuleIdPrefix}${val}`;
} else {
const newField = replaceIdsInProperty(val, ids);
tree[key] = newField;
}
} else if (typeof val === 'object') {
traverse(val);
}
}
}
traverse(module.impl);
traverse(module.spec.stateMap);
return module;
}
// remove '{{$moduleId}}__' in moduleSchema
export function removeModuleId(module: Module): Module {
function traverse(tree: Record<string, any>) {
for (const key in tree) {
const val = tree[key];
if (typeof val === 'string') {
tree[key] = val.replace(/{{ \$moduleId }}__/g, '');
} else if (typeof val === 'object') {
traverse(val);
}
}
}
traverse(module.impl);
traverse(module.spec.stateMap);
return module;
}
// example: replaceIdsInExp('{{input1.value}} + {{input2.value}}', ids: ['input1']])
function replaceIdsInProperty(property: string, ids: string[]): string {
const expRegExp = new RegExp('{{(.*?)}}', 'g');
const matches = [...property.matchAll(expRegExp)];
if (matches.length === 0) return property;
const expPos: StringPos[] = [];
matches.forEach(match => {
const newExp = replaceIdsInExp(match[1], ids);
if (newExp) {
expPos.push({
// + 2 because of '{{' length is 2
start: match.index! + 2,
end: match.index! + 2 + match[1].length,
replaceStr: newExp,
});
}
});
if (expPos.length === 0) return property;
return expPos.reverse().reduce((result, { start, end, replaceStr }) => {
return result.slice(0, start) + `${replaceStr}` + result.slice(end);
}, property);
}
// example: replaceIdsInExp('input1.value', ids: ['input1']])
function replaceIdsInExp(exp: string, ids: string[]): string | void {
const identifierPos: StringPos[] = [];
simpleWalk((acornLoose as typeof acorn).parse(exp, { ecmaVersion: 2020 }), {
Identifier: node => {
const objectName = exp.slice(node.start, node.end);
if (ids.includes(objectName)) {
identifierPos.push({
start: node.start,
end: node.end,
replaceStr: `${ModuleIdPrefix}${objectName}`,
});
}
},
});
if (identifierPos.length === 0) {
null;
}
const newExp = identifierPos.reverse().reduce((result, { start, end, replaceStr }) => {
return result.slice(0, start) + `${replaceStr}` + result.slice(end);
}, exp);
return newExp;
}

View File

@ -1,55 +0,0 @@
import { Type } from '@sinclair/typebox';
import { parseModuleSchema } from '../src/utils/parseModuleSchema';
describe('parse module schema', () => {
it('will add module id to the expression', () => {
expect(
parseModuleSchema({
version: 'test/v1',
kind: 'Module',
parsedVersion: {
category: 'test/v1',
value: 'test',
},
metadata: {
name: 'test',
},
spec: {
properties: {
value: Type.Object({
BE_CAREFUL: Type.Number(),
}),
},
events: [],
stateMap: {},
},
impl: [
{
id: 'BE_CAREFUL',
type: 'test/v1/child',
properties: {
test: '{{ value.BE_CAREFUL.toFixed(2) }}',
},
traits: [],
},
],
}).impl
).toMatchInlineSnapshot(`
Array [
Object {
"id": "{{ $moduleId }}__BE_CAREFUL",
"properties": Object {
"test": "{{ value.{{ $moduleId }}__BE_CAREFUL.toFixed(2) }}",
},
"traits": Array [],
"type": "test/v1/child",
},
]
`);
});
});

View File

@ -22,8 +22,6 @@ import {
ImplementedRuntimeTrait,
ImplementedRuntimeModule,
} from '../types';
import { parseModuleSchema } from '../utils/parseModuleSchema';
import { cloneDeep } from 'lodash-es';
export type SunmaoLib = {
components?: ImplementedRuntimeComponent<string, string, string, string>[];
@ -124,7 +122,6 @@ export class Registry {
}
registerModule(c: ImplementedRuntimeModule, overWrite = false) {
const parsedModule = parseModuleSchema(cloneDeep(c));
if (!overWrite && this.modules.get(c.version)?.has(c.metadata.name)) {
throw new Error(
`Already has module ${c.version}/${c.metadata.name} in this registry.`
@ -133,7 +130,7 @@ export class Registry {
if (!this.modules.has(c.version)) {
this.modules.set(c.version, new Map());
}
this.modules.get(c.version)?.set(c.metadata.name, parsedModule);
this.modules.get(c.version)?.set(c.metadata.name, c);
}
getModule(version: string, name: string): ImplementedRuntimeModule {

View File

@ -1,44 +0,0 @@
import { ImplementedRuntimeModule } from '../types';
// add {{$moduleId}} in moduleSchema
export function parseModuleSchema(
module: ImplementedRuntimeModule
): ImplementedRuntimeModule {
const ids: string[] = [];
module.impl.forEach(c => {
ids.push(c.id);
if (c.type === 'core/v1/moduleContainer') {
ids.push(c.properties.id as string);
}
if (c.type === 'chakra_ui/v1/list') {
ids.push((c.properties.template as any).id);
}
});
function traverse(tree: Record<string, any>) {
for (const key in tree) {
const val = tree[key];
if (typeof val === 'string') {
if (ids.includes(val)) {
tree[key] = `{{ $moduleId }}__${val}`;
} else {
for (const id of ids) {
if (val.includes(`${id}.`)) {
tree[key] = val.replace(
new RegExp(`${id}\\.`, 'g'),
`{{ $moduleId }}__${id}.`
);
break;
}
}
}
} else if (typeof val === 'object') {
traverse(val);
}
}
}
traverse(module.impl);
traverse(module.spec.stateMap);
return module;
}