Skip to content

Commit 6fc0498

Browse files
wwsunwwsun
and
wwsun
authored
feat: support local components (#78)
* docs: update * feat: parse local components * fix: update * refactor: module exports to exportList * fix: update workspace --------- Co-authored-by: wwsun <ww.sww@outlook.com>
1 parent f2919c1 commit 6fc0498

File tree

21 files changed

+1613
-1995
lines changed

21 files changed

+1613
-1995
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ gantt
5353

5454
## 💻 Development
5555

56-
### Recommended Development Environment
56+
### Environment
5757

58-
- Node.js >= 16.0.0
59-
- Yarn >= 1.22.0
58+
- Node `>= 18`
59+
- Yarn `>= 1.22 && < 2`
6060

6161
### Development Quick Start
6262

README.zh-CN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ gantt
5555

5656
### 推荐开发环境
5757

58-
- Node.js >= 16.0.0
59-
- Yarn >= 1.22.0
58+
- Node `>= 18`
59+
- Yarn `>= 1.22 && < 2`
6060

6161
### 本地开发调试方法
6262

apps/playground/src/helpers/mock-files.ts

Lines changed: 54 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,18 @@ const tangoConfigJson = {
7575
},
7676
};
7777

78+
const helperCode = `
79+
export function registerComponentPrototype(proto) {
80+
if (!proto) return;
81+
if (!window.localTangoComponentPrototypes) {
82+
window.localTangoComponentPrototypes = {};
83+
}
84+
if (proto.name) {
85+
window.localTangoComponentPrototypes[proto.name] = proto;
86+
}
87+
}
88+
`;
89+
7890
const routesCode = `
7991
import Index from "./pages/list";
8092
@@ -143,7 +155,7 @@ import {
143155
FormilyForm,
144156
} from "@music163/antd";
145157
import { Space } from '@music163/antd';
146-
import { MyButton } from '../components/button';
158+
import { LocalButton } from '../components';
147159
148160
class App extends React.Component {
149161
render() {
@@ -153,7 +165,7 @@ class App extends React.Component {
153165
</Section>
154166
<Section>
155167
<Space>
156-
<MyButton />
168+
<LocalButton />
157169
<Button>button</Button>
158170
<Input />
159171
</Space>
@@ -162,18 +174,52 @@ class App extends React.Component {
162174
);
163175
}
164176
}
177+
165178
export default definePage(App);
166179
`;
167180

168181
const componentsButtonCode = `
169182
import React from 'react';
183+
import { registerComponentPrototype } from '../utils';
184+
185+
export default function MyButton(props) {
186+
return <button {...props}>my button</button>
187+
}
188+
189+
registerComponentPrototype({
190+
name: 'LocalButton',
191+
title: 'Local Button',
192+
exportType: 'namedExport',
193+
package: '/src/components',
194+
props: [
195+
{ name: 'background', title: '背景色', setter: 'colorSetter' },
196+
],
197+
});
198+
`;
199+
200+
const componentsInputCode = `
201+
import React from 'react';
202+
import { registerComponentPrototype } from '../utils';
170203
171-
export function MyButton() {
172-
return <button>my button</button>
204+
export default function MyInput(props) {
205+
return <input {...props} />;
173206
}
207+
208+
registerComponentPrototype({
209+
name: 'LocalInput',
210+
title: 'Local Input',
211+
exportType: 'namedExport',
212+
package: '/src/components',
213+
props: [
214+
{ name: 'color', title: '文本色', setter: 'colorSetter' },
215+
],
216+
});
174217
`;
175218

176-
const componentsPrototypeCode = ``;
219+
const componentsEntryCode = `
220+
export { default as LocalButton } from './button';
221+
export { default as LocalInput } from './input';
222+
`;
177223

178224
const storeApp = `
179225
import { defineStore } from '@music163/tango-boot';
@@ -260,14 +306,15 @@ export const sampleFiles = [
260306
{ filename: '/src/index.js', code: entryCode },
261307
{ filename: '/src/pages/list.js', code: viewHomeCode },
262308
{ filename: '/src/components/button.js', code: componentsButtonCode },
263-
{ filename: '/src/components/prototype.js', code: componentsPrototypeCode },
309+
{ filename: '/src/components/input.js', code: componentsInputCode },
310+
{ filename: '/src/components/index.js', code: componentsEntryCode },
264311
{ filename: '/src/routes.js', code: routesCode },
265312
{ filename: '/src/stores/index.js', code: storeIndexCode },
266313
{ filename: '/src/stores/app.js', code: storeApp },
267314
{ filename: '/src/stores/counter.js', code: storeCounter },
268315
{ filename: '/src/services/index.js', code: serviceCode },
269316
{ filename: '/src/services/sub.js', code: subServiceCode },
270-
{ filename: '/src/utils/index.js', code: `export function foo() {}` },
317+
{ filename: '/src/utils/index.js', code: helperCode },
271318
];
272319

273320
export const genDefaultPage = (index: number) => ({

apps/playground/src/pages/index.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ const workspace = new Workspace({
3333
// 2. 引擎初始化
3434
const engine = createEngine({
3535
workspace,
36-
defaultActiveSidebarPanel: 'outline',
3736
});
3837

3938
// @ts-ignore
@@ -121,6 +120,9 @@ export default function App() {
121120
workspace.setComponentPrototypes(sandboxWindow.TangoAntd.prototypes);
122121
}
123122
}
123+
if (sandboxWindow.localTangoComponentPrototypes) {
124+
workspace.setComponentPrototypes(sandboxWindow.localTangoComponentPrototypes);
125+
}
124126
setMenuLoading(false);
125127
}
126128
}}

packages/core/src/helpers/ast/traverse.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
code2expression,
2323
object2node,
2424
} from './parse';
25-
import { isValidComponentName } from '../string';
25+
import { getFullPath, isValidComponentName } from '../string';
2626
import { isDefineService, isDefineStore, isTangoVariable } from '../assert';
2727
import type {
2828
IRouteData,
@@ -32,6 +32,7 @@ import type {
3232
IServiceFunctionPayload,
3333
InsertChildPositionType,
3434
IImportSpecifierData,
35+
IExportSpecifierData,
3536
} from '../../types';
3637
import { IdGenerator } from '../id-generator';
3738

@@ -1330,6 +1331,33 @@ export function traverseViewFile(ast: t.File, idGenerator: IdGenerator) {
13301331
};
13311332
}
13321333

1334+
export function traverseComponentsEntryFile(ast: t.File, baseDir?: string) {
1335+
const exportMap: Record<string, IExportSpecifierData> = {};
1336+
traverse(ast, {
1337+
ExportNamedDeclaration(path) {
1338+
const node = path.node;
1339+
let source = node2value(node.source);
1340+
if (baseDir) {
1341+
// fix relative source path
1342+
source = getFullPath(baseDir, source);
1343+
}
1344+
node.specifiers.forEach((specifier) => {
1345+
if (t.isExportSpecifier(specifier)) {
1346+
const name = keyNode2value(specifier.exported) as string;
1347+
if (name) {
1348+
exportMap[name] = {
1349+
source,
1350+
exportedName: name,
1351+
};
1352+
}
1353+
}
1354+
});
1355+
},
1356+
});
1357+
1358+
return { ast, exportMap };
1359+
}
1360+
13331361
/**
13341362
* 解析导入语句
13351363
*/

packages/core/src/helpers/string.ts

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ export function inferFileType(filename: string): FileType {
3333
return FileType.JsonViewModule;
3434
}
3535

36+
if (/\/(blocks|components)\/index\.js/.test(filename)) {
37+
return FileType.ComponentsEntryModule;
38+
}
39+
3640
if (/\/services\/.+\.js$/.test(filename)) {
3741
return FileType.ServiceModule;
3842
}
@@ -49,10 +53,6 @@ export function inferFileType(filename: string): FileType {
4953
return FileType.StoreModule;
5054
}
5155

52-
if (/\/blocks\/[\w-]+\/index\.js/.test(filename)) {
53-
return FileType.BlockEntryModule;
54-
}
55-
5656
if (/\.jsx?$/.test(filename)) {
5757
return FileType.Module;
5858
}
@@ -170,24 +170,35 @@ export function getBlockNameByFilename(filename: string) {
170170
}
171171

172172
/**
173-
* FIXME: 有问题,需要优化下
174-
* 基于 from 文件的地址计算 to 文件的相对引用路径
175-
* @param from
176-
* @param to
173+
* 合并两个路径
174+
* @param root
175+
* @param filename
176+
* @returns
177+
*/
178+
export function getFullPath(root: string, filename: string) {
179+
return path.join(root, filename);
180+
}
181+
182+
/**
183+
* 计算 targetFile 在 sourceFile 中的相对引用路径
184+
* @param sourceFile
185+
* @param targetFile
186+
* @returns
177187
*/
178-
export function getRelativePath(from: string, to: string) {
179-
const fromFolder = path.dirname(from);
180-
return path.relative(fromFolder, to);
188+
export function getRelativePath(sourceFile: string, targetFile: string) {
189+
sourceFile = path.dirname(sourceFile);
190+
return path.relative(sourceFile, targetFile);
181191
}
182192

183193
/**
184194
* 判断给定字符串是否是文件路径
185195
* @example ./pages/index.js -- yes
186196
* @example ../pages/index.js -- yes
197+
* @example ../components -- yes
187198
* @example /src/pages/index.js -- yes
188199
* @example @music163/tango-designer -- no
189200
* @param str
190201
*/
191202
export function isFilepath(str: string) {
192-
return /^(\.\.?\/|\/).*\.[a-z]+$/.test(str);
203+
return /^(\.\.?\/|\/).*(\.[a-z]+)?$/.test(str);
193204
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import path from 'path';
2+
import { action, computed, makeObservable, observable } from 'mobx';
3+
import { TangoModule } from './module';
4+
import { IWorkspace } from './interfaces';
5+
import { IExportSpecifierData, IFileConfig } from '../types';
6+
import { traverseComponentsEntryFile } from '../helpers';
7+
8+
/**
9+
* 本地组件目录的入口文件,例如 '/components/index.js' 或 `/blocks/index.js`
10+
*/
11+
export class TangoComponentsEntryModule extends TangoModule {
12+
exportList: Record<string, IExportSpecifierData>;
13+
14+
constructor(workspace: IWorkspace, props: IFileConfig) {
15+
super(workspace, props, false);
16+
this.update(props.code, false, false);
17+
makeObservable(this, {
18+
_code: observable,
19+
_cleanCode: observable,
20+
exportList: observable,
21+
code: computed,
22+
cleanCode: computed,
23+
update: action,
24+
});
25+
}
26+
27+
_analysisAst() {
28+
const baseDir = path.dirname(this.filename);
29+
const { exportMap } = traverseComponentsEntryFile(this.ast, baseDir);
30+
this.exportList = exportMap;
31+
Object.keys(this.exportList).forEach((key) => {
32+
this.workspace.componentPrototypes.set(key, {
33+
name: key,
34+
exportType: 'namedExport',
35+
package: baseDir,
36+
type: 'element',
37+
});
38+
});
39+
}
40+
}

packages/core/src/models/interfaces.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,5 +266,5 @@ export interface IWorkspace {
266266
get pages(): any[];
267267
get bizComps(): string[];
268268
get baseComps(): string[];
269-
get blocks(): any[];
269+
get localComps(): string[];
270270
}

packages/core/src/models/module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ export class TangoModule extends TangoFile {
2222
ast: t.File;
2323

2424
/**
25-
* 导入语句
25+
* 导入的依赖列表
2626
*/
27-
imports: ImportDeclarationDataType;
27+
importList: ImportDeclarationDataType;
2828

2929
constructor(workspace: IWorkspace, props: IFileConfig, isSyncCode = true) {
3030
super(workspace, props, isSyncCode);
@@ -95,7 +95,7 @@ export class TangoModule extends TangoFile {
9595

9696
_analysisAst() {
9797
const { imports } = traverseFile(this.ast);
98-
this.imports = imports;
98+
this.importList = imports;
9999
}
100100
}
101101

packages/core/src/models/service-module.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export class TangoServiceModule extends TangoModule {
5959

6060
_analysisAst() {
6161
const { imports, services, baseConfig } = traverseServiceFile(this.ast);
62-
this.imports = imports;
62+
this.importList = imports;
6363
this._serviceFunctions = services;
6464
this._baseConfig = baseConfig;
6565
if (baseConfig.namespace) {

packages/core/src/models/view-module.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
159159
this._cleanCode = ast2code(cleanAst);
160160

161161
this._importedModules = importedModules;
162-
this.imports = imports;
162+
this.importList = imports;
163163
this.importMap = buildImportMap(imports);
164164
this.variables = variables;
165165

@@ -180,7 +180,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
180180
* 依赖列表
181181
*/
182182
listImportSources() {
183-
return Object.keys(this.imports);
183+
return Object.keys(this.importList);
184184
}
185185

186186
/**
@@ -227,7 +227,7 @@ export class TangoViewModule extends TangoModule implements IViewFile {
227227
* @returns
228228
*/
229229
addImportSpecifiers(source: string, newSpecifiers: IImportSpecifierData[]) {
230-
const existSpecifiers = this.imports[source];
230+
const existSpecifiers = this.importList[source];
231231
if (existSpecifiers) {
232232
const insertedSpecifiers = newSpecifiers.filter((item) => {
233233
return !existSpecifiers.find((existItem) => existItem.localName === item.localName);

0 commit comments

Comments
 (0)