tego 1.3.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/bin/tego.js +20 -0
- package/lib/config.d.ts +30 -0
- package/lib/config.js +73 -0
- package/lib/constants.d.ts +5 -0
- package/lib/constants.js +50 -0
- package/lib/default-modules.d.ts +1 -0
- package/lib/default-modules.js +51 -0
- package/lib/default-plugins.d.ts +1 -0
- package/lib/default-plugins.js +50 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +61 -0
- package/lib/plugin-presets.d.ts +21 -0
- package/lib/plugin-presets.js +339 -0
- package/lib/preload.d.ts +4 -0
- package/lib/preload.js +76 -0
- package/lib/prepare.d.ts +7 -0
- package/lib/prepare.js +120 -0
- package/lib/utils.d.ts +4 -0
- package/lib/utils.js +176 -0
- package/package.json +88 -0
- package/presets/.env.example +92 -0
- package/src/config.ts +49 -0
- package/src/constants.ts +7 -0
- package/src/default-modules.ts +24 -0
- package/src/default-plugins.ts +24 -0
- package/src/index.ts +69 -0
- package/src/plugin-presets.ts +319 -0
- package/src/preload.ts +80 -0
- package/src/prepare.ts +96 -0
- package/src/utils.ts +152 -0
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { Plugin, PluginManager } from '@tachybase/server';
|
|
2
|
+
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
|
|
5
|
+
export class PluginPresets extends Plugin {
|
|
6
|
+
#builtInPlugins = [
|
|
7
|
+
'acl',
|
|
8
|
+
'app-info',
|
|
9
|
+
'auth',
|
|
10
|
+
'backup',
|
|
11
|
+
'cloud-component',
|
|
12
|
+
'collection',
|
|
13
|
+
'cron',
|
|
14
|
+
'data-source',
|
|
15
|
+
'error-handler',
|
|
16
|
+
'event-source',
|
|
17
|
+
'file',
|
|
18
|
+
'workflow',
|
|
19
|
+
'message',
|
|
20
|
+
'pdf',
|
|
21
|
+
'ui-schema',
|
|
22
|
+
'user',
|
|
23
|
+
'web',
|
|
24
|
+
'worker-thread',
|
|
25
|
+
'env-secrets',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
get builtInPlugins() {
|
|
29
|
+
return this.#builtInPlugins;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#localPlugins = [
|
|
33
|
+
// [name, version, enabled]
|
|
34
|
+
['action-bulk-edit', '0.22.7', true],
|
|
35
|
+
['action-bulk-update', '0.22.7', true],
|
|
36
|
+
['action-custom-request', '0.22.7', true],
|
|
37
|
+
['action-duplicate', '0.22.7', true],
|
|
38
|
+
['action-export', '0.22.7', true],
|
|
39
|
+
['action-import', '0.22.7', true],
|
|
40
|
+
['action-print', '0.22.7', true],
|
|
41
|
+
['block-calendar', '0.22.7', true],
|
|
42
|
+
['block-charts', '0.22.7', true],
|
|
43
|
+
['block-gantt', '0.22.7', true],
|
|
44
|
+
['block-kanban', '0.22.7', true],
|
|
45
|
+
['block-presentation', '0.22.7', true],
|
|
46
|
+
['field-china-region', '0.22.7', true],
|
|
47
|
+
['field-formula', '0.22.7', true],
|
|
48
|
+
['field-sequence', '0.22.7', true],
|
|
49
|
+
['field-encryption', '0.23.8', true],
|
|
50
|
+
['log-viewer', '0.22.67', true],
|
|
51
|
+
['otp', '0.22.67', true],
|
|
52
|
+
['instrumentation', '1.0.18', true],
|
|
53
|
+
['full-text-search', '0.23.24', true],
|
|
54
|
+
['password-policy', '0.23.65', true],
|
|
55
|
+
['auth-pages', '0.23.65', true],
|
|
56
|
+
['manual-notification', '1.0.4', true],
|
|
57
|
+
// default disable
|
|
58
|
+
['adapter-bullmq', '0.21.76', false],
|
|
59
|
+
['adapter-red-node', '0.22.8', false],
|
|
60
|
+
['adapter-remix', '0.22.9', false],
|
|
61
|
+
['api-keys', '0.10.1', false],
|
|
62
|
+
['audit-logs', '0.22.7', false],
|
|
63
|
+
['auth-cas', '0.13.0', false],
|
|
64
|
+
['auth-dingtalk', '0.21.76', false],
|
|
65
|
+
['auth-lark', '0.22.42', false],
|
|
66
|
+
['auth-oidc', '0.9.2', false],
|
|
67
|
+
['auth-saml', '0.8.1', false],
|
|
68
|
+
['auth-sms', '0.10.0', false],
|
|
69
|
+
['auth-wechat', '0.21.89', false],
|
|
70
|
+
['auth-wecom', '0.21.76', false],
|
|
71
|
+
['block-comments', '0.22.6', false],
|
|
72
|
+
['block-map', '0.8.1', false],
|
|
73
|
+
['block-step-form', '1.0.0', false],
|
|
74
|
+
['data-source-common', '0.22.5', false],
|
|
75
|
+
['demos-game-runesweeper', '0.22.20', false],
|
|
76
|
+
['devtools', '0.22.82', false],
|
|
77
|
+
['field-markdown-vditor', '0.22.6', false],
|
|
78
|
+
['field-snapshot', '0.8.1', false],
|
|
79
|
+
['hera', '0.22.6', false],
|
|
80
|
+
['i18n-editor', '0.11.1', false],
|
|
81
|
+
['multi-app', '0.7.0', false],
|
|
82
|
+
['multi-app-share-collection', '0.9.2', false],
|
|
83
|
+
['online-user', '0.22.7', false],
|
|
84
|
+
['simple-cms', '0.22.6', false],
|
|
85
|
+
['sub-accounts', '0.22.56', false],
|
|
86
|
+
['theme-editor', '0.11.1', false],
|
|
87
|
+
['workflow-approval', '0.22.37', false],
|
|
88
|
+
['ai-chat', '0.23.8', false],
|
|
89
|
+
['department', '0.23.22', false],
|
|
90
|
+
['workflow-analysis', '0.23.41', false],
|
|
91
|
+
['api-logs', '0.23.49', false],
|
|
92
|
+
['ocr-convert', '1.0.12', false],
|
|
93
|
+
['text-copy', '1.2.11', false],
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
get localPlugins() {
|
|
97
|
+
return this.#localPlugins;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
splitNames(name: string) {
|
|
101
|
+
return (name || '').split(',').filter(Boolean);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
getBuiltInPlugins() {
|
|
105
|
+
const { PRESETS_CORE_PLUGINS } = process.env;
|
|
106
|
+
const [addPlugins, removedPlugins] = this.parseNames(PRESETS_CORE_PLUGINS);
|
|
107
|
+
return _.uniq(this.builtInPlugins.concat(addPlugins).filter((name) => !removedPlugins.includes(name)));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
parseNames(plugins: string) {
|
|
111
|
+
const addPlugins = this.splitNames(plugins).filter((name) => !name.startsWith('!') && !name.startsWith('|'));
|
|
112
|
+
const removedPlugins = this.splitNames(plugins)
|
|
113
|
+
.filter((name) => name.startsWith('!'))
|
|
114
|
+
.map((name) => name.slice(1));
|
|
115
|
+
|
|
116
|
+
const addDisabledPlugins = this.splitNames(plugins)
|
|
117
|
+
.filter((name) => name.startsWith('|'))
|
|
118
|
+
.map((name) => name.slice(1));
|
|
119
|
+
|
|
120
|
+
return [addPlugins, removedPlugins, addDisabledPlugins];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
getLocalPlugins() {
|
|
124
|
+
const { PRESETS_LOCAL_PLUGINS } = process.env;
|
|
125
|
+
let plugins = [].concat(this.localPlugins);
|
|
126
|
+
const [addPlugins, removedPlugins, addDisabledPlugins] = this.parseNames(PRESETS_LOCAL_PLUGINS);
|
|
127
|
+
|
|
128
|
+
addPlugins.forEach((plugin) => {
|
|
129
|
+
const found = plugins.find((p) => p[0] === plugin);
|
|
130
|
+
if (found) {
|
|
131
|
+
found[2] = true;
|
|
132
|
+
} else {
|
|
133
|
+
plugins.push([plugin, '0.0.0', true]);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
removedPlugins.forEach((plugin) => {
|
|
138
|
+
plugins = plugins.filter((p) => p[0] !== plugin);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
addDisabledPlugins.forEach((plugin) => {
|
|
142
|
+
const found = plugins.find((p) => p[0] === plugin);
|
|
143
|
+
if (found) {
|
|
144
|
+
found[2] = false;
|
|
145
|
+
} else {
|
|
146
|
+
plugins.push([plugin, '0.0.0', false]);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return plugins;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async getPackageJson(name) {
|
|
154
|
+
let packageName = name;
|
|
155
|
+
try {
|
|
156
|
+
packageName = await PluginManager.getPackageName(name);
|
|
157
|
+
} catch (error) {
|
|
158
|
+
packageName = name;
|
|
159
|
+
}
|
|
160
|
+
const packageJson = await PluginManager.getPackageJson(packageName);
|
|
161
|
+
return packageJson;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async allPlugins() {
|
|
165
|
+
return (
|
|
166
|
+
await Promise.all(
|
|
167
|
+
this.getBuiltInPlugins().map(async (name) => {
|
|
168
|
+
const packageJson = await this.getPackageJson(name);
|
|
169
|
+
return {
|
|
170
|
+
name,
|
|
171
|
+
packageName: packageJson.name,
|
|
172
|
+
enabled: true,
|
|
173
|
+
builtIn: true,
|
|
174
|
+
version: packageJson.version,
|
|
175
|
+
} as any;
|
|
176
|
+
}),
|
|
177
|
+
)
|
|
178
|
+
).concat(
|
|
179
|
+
await Promise.all(
|
|
180
|
+
this.getLocalPlugins().map(async (plugin) => {
|
|
181
|
+
const name = plugin[0];
|
|
182
|
+
const enabled = plugin[2];
|
|
183
|
+
const packageJson = await this.getPackageJson(name);
|
|
184
|
+
return { name, packageName: packageJson.name, version: packageJson.version, enabled };
|
|
185
|
+
}),
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async getPluginToBeUpgraded() {
|
|
191
|
+
const repository = this.app.db.getRepository<any>('applicationPlugins');
|
|
192
|
+
const items = (await repository.find()).map((item) => item.name);
|
|
193
|
+
const plugins = await Promise.all(
|
|
194
|
+
this.getBuiltInPlugins().map(async (name) => {
|
|
195
|
+
const packageJson = await this.getPackageJson(name);
|
|
196
|
+
return {
|
|
197
|
+
name,
|
|
198
|
+
packageName: packageJson.name,
|
|
199
|
+
enabled: true,
|
|
200
|
+
builtIn: true,
|
|
201
|
+
version: packageJson.version,
|
|
202
|
+
} as any;
|
|
203
|
+
}),
|
|
204
|
+
);
|
|
205
|
+
for (const plugin of this.getLocalPlugins()) {
|
|
206
|
+
if (plugin[1]) {
|
|
207
|
+
// 不在插件列表,并且插件最低版本小于当前应用版本,跳过不处理
|
|
208
|
+
if (!items.includes(plugin[0]) && (await this.app.version.satisfies(`>${plugin[1]}`))) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
const name = plugin[0];
|
|
213
|
+
const packageJson = await this.getPackageJson(name);
|
|
214
|
+
if (items.includes(plugin[0])) {
|
|
215
|
+
plugins.push({
|
|
216
|
+
name,
|
|
217
|
+
packageName: packageJson.name,
|
|
218
|
+
version: packageJson.version,
|
|
219
|
+
});
|
|
220
|
+
} else {
|
|
221
|
+
plugins.push({
|
|
222
|
+
name,
|
|
223
|
+
packageName: packageJson.name,
|
|
224
|
+
version: packageJson.version,
|
|
225
|
+
enabled: plugin[2],
|
|
226
|
+
builtIn: false,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return plugins;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async updateOrCreatePlugins() {
|
|
234
|
+
const repository = this.pm.repository;
|
|
235
|
+
const plugins = await this.getPluginToBeUpgraded();
|
|
236
|
+
try {
|
|
237
|
+
await this.db.sequelize.transaction((transaction) => {
|
|
238
|
+
return Promise.all(
|
|
239
|
+
plugins.map((values) =>
|
|
240
|
+
repository.updateOrCreate({
|
|
241
|
+
transaction,
|
|
242
|
+
values,
|
|
243
|
+
filterKeys: ['name'],
|
|
244
|
+
}),
|
|
245
|
+
),
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
} catch (err) {
|
|
249
|
+
console.error(err);
|
|
250
|
+
throw new Error('Create or update plugin error.');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async createIfNotExists() {
|
|
255
|
+
const repository = this.pm.repository;
|
|
256
|
+
const existPlugins = await repository.find();
|
|
257
|
+
const existPluginNames = existPlugins.map((item) => item.name);
|
|
258
|
+
const plugins = (await this.allPlugins()).filter((item) => !existPluginNames.includes(item.name));
|
|
259
|
+
this.filterForbidSubAppPlugin(plugins);
|
|
260
|
+
await repository.create({ values: plugins });
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async install() {
|
|
264
|
+
await this.createIfNotExists();
|
|
265
|
+
this.log.info('start install built-in plugins');
|
|
266
|
+
await this.pm.repository.init();
|
|
267
|
+
await this.pm.load();
|
|
268
|
+
await this.pm.install();
|
|
269
|
+
this.log.info('finish install built-in plugins');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async upgrade() {
|
|
273
|
+
this.log.info('update built-in plugins');
|
|
274
|
+
await this.forbidSubAppPlugin();
|
|
275
|
+
await this.updateOrCreatePlugins();
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
getForbidSubAppPlugin() {
|
|
279
|
+
if (this.app.name === 'main') {
|
|
280
|
+
return [];
|
|
281
|
+
}
|
|
282
|
+
const { FORBID_SUB_APP_PLUGINS } = process.env;
|
|
283
|
+
return FORBID_SUB_APP_PLUGINS ? FORBID_SUB_APP_PLUGINS.split(',') : [];
|
|
284
|
+
}
|
|
285
|
+
// 从环境变量读取禁止子应用装载的插件
|
|
286
|
+
async forbidSubAppPlugin() {
|
|
287
|
+
if (this.app.name === 'main') {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const forbidPlugins = this.getForbidSubAppPlugin();
|
|
291
|
+
const repository = this.pm.repository;
|
|
292
|
+
await repository.update({
|
|
293
|
+
values: {
|
|
294
|
+
subView: false,
|
|
295
|
+
enabled: false,
|
|
296
|
+
},
|
|
297
|
+
filter: {
|
|
298
|
+
name: {
|
|
299
|
+
$in: forbidPlugins,
|
|
300
|
+
},
|
|
301
|
+
},
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async filterForbidSubAppPlugin(plugins: any[]) {
|
|
306
|
+
if (this.app.name === 'main') {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const forbidPlugins = this.getForbidSubAppPlugin();
|
|
310
|
+
plugins.forEach((plugin) => {
|
|
311
|
+
if (forbidPlugins.includes(plugin.name)) {
|
|
312
|
+
plugin.subView = false;
|
|
313
|
+
plugin.enabled = false;
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export default PluginPresets;
|
package/src/preload.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Module } from 'node:module';
|
|
2
|
+
import TachybaseGlobal from '@tachybase/globals';
|
|
3
|
+
import { defineLoader } from '@tachybase/loader';
|
|
4
|
+
|
|
5
|
+
import { DEFAULT_BUILTIN_PLUGINS_PATH, DEFAULT_DEV_PLUGINS_PATH, DEFAULT_REMOTE_PLUGINS_PATH } from './constants';
|
|
6
|
+
|
|
7
|
+
// improve error stack
|
|
8
|
+
Error.stackTraceLimit = process.env.ERROR_STACK_TRACE_LIMIT ? +process.env.ERROR_STACK_TRACE_LIMIT : 10;
|
|
9
|
+
|
|
10
|
+
// 默认 NODE_MODULES_PATH 搜索路径
|
|
11
|
+
if (!process.env.NODE_MODULES_PATH) {
|
|
12
|
+
process.env.NODE_MODULES_PATH = [
|
|
13
|
+
// 开发插件
|
|
14
|
+
DEFAULT_DEV_PLUGINS_PATH,
|
|
15
|
+
// 远程插件
|
|
16
|
+
DEFAULT_REMOTE_PLUGINS_PATH,
|
|
17
|
+
// 内置插件
|
|
18
|
+
DEFAULT_BUILTIN_PLUGINS_PATH,
|
|
19
|
+
].join(',');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 解析 process.env.NODE_MODULES_PATH
|
|
23
|
+
const paths = process.env.NODE_MODULES_PATH.split(',');
|
|
24
|
+
TachybaseGlobal.getInstance().set('PLUGIN_PATHS', paths);
|
|
25
|
+
|
|
26
|
+
declare module 'node:module' {
|
|
27
|
+
// 扩展 NodeJS.Module 静态属性
|
|
28
|
+
export function _load(request: string, parent: NodeModule | null, isMain: boolean): any;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const originalLoad = Module._load;
|
|
32
|
+
const appRoot = __dirname;
|
|
33
|
+
|
|
34
|
+
// 使用加载白名单的机制
|
|
35
|
+
// TODO 考虑服务端校验的版本也和这个保持同步(服务端要求的版本要和这里以及引擎的 package.json 一致)
|
|
36
|
+
const defaultWhitelists = [
|
|
37
|
+
'@koa/cors',
|
|
38
|
+
'@koa/multer',
|
|
39
|
+
'async-mutex',
|
|
40
|
+
'axios',
|
|
41
|
+
'cache-manager',
|
|
42
|
+
'dayjs',
|
|
43
|
+
'dotenv',
|
|
44
|
+
'i18next',
|
|
45
|
+
'jsonwebtoken',
|
|
46
|
+
'koa',
|
|
47
|
+
'koa-bodyparser',
|
|
48
|
+
'lodash',
|
|
49
|
+
'mathjs',
|
|
50
|
+
'multer',
|
|
51
|
+
'mysql2',
|
|
52
|
+
'pg',
|
|
53
|
+
'react',
|
|
54
|
+
'react-dom',
|
|
55
|
+
'sequelize',
|
|
56
|
+
'sqlite3',
|
|
57
|
+
'umzug',
|
|
58
|
+
'winston',
|
|
59
|
+
'winston-daily-rotate-file',
|
|
60
|
+
];
|
|
61
|
+
|
|
62
|
+
const whitelists = new Set(defaultWhitelists);
|
|
63
|
+
|
|
64
|
+
// 允许环境变量设置模块
|
|
65
|
+
// 额外添加的模块会被放在指定目录 NODE_MODULES_PATH 中
|
|
66
|
+
if (process.env.ENGINE_MODULES) {
|
|
67
|
+
process.env.ENGINE_MODULES.split(',').forEach((item: string) => {
|
|
68
|
+
whitelists.add(item);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 加载路径包含两个,一个是引擎的启动目录,另一个是指定的插件目录
|
|
73
|
+
const lookingPaths = [appRoot, ...TachybaseGlobal.getInstance().get('PLUGIN_PATHS')];
|
|
74
|
+
|
|
75
|
+
// 带给子进程加载路径
|
|
76
|
+
TachybaseGlobal.getInstance().set('WORKER_PATHS', lookingPaths);
|
|
77
|
+
TachybaseGlobal.getInstance().set('WORKER_MODULES', [...whitelists]);
|
|
78
|
+
|
|
79
|
+
// 整个加载过程允许报错,保持和默认加载器一样的行为
|
|
80
|
+
Module._load = defineLoader(whitelists, originalLoad, lookingPaths);
|
package/src/prepare.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import process from 'node:process';
|
|
4
|
+
|
|
5
|
+
import yoctoSpinner from '@socketregistry/yocto-spinner/index.cjs';
|
|
6
|
+
import execa from 'execa';
|
|
7
|
+
|
|
8
|
+
import { DEFAULT_BUILTIN_PLUGINS_RELATIVE_PATH, DEFAULT_WEB_PACKAGE_NAME } from './constants';
|
|
9
|
+
import { defaultModules } from './default-modules';
|
|
10
|
+
import { defaultPlugins } from './default-plugins';
|
|
11
|
+
import { downloadTar, initEnvFile } from './utils';
|
|
12
|
+
|
|
13
|
+
export async function prepare({
|
|
14
|
+
name,
|
|
15
|
+
plugins = defaultPlugins,
|
|
16
|
+
init = false,
|
|
17
|
+
}: {
|
|
18
|
+
name?: string;
|
|
19
|
+
plugins: string[];
|
|
20
|
+
init?: boolean;
|
|
21
|
+
}) {
|
|
22
|
+
if (init) {
|
|
23
|
+
if (fs.existsSync(name)) {
|
|
24
|
+
console.log(`project folder ${name} already exists, exit now.`);
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
fs.mkdirSync(name);
|
|
28
|
+
initEnvFile(name);
|
|
29
|
+
} else {
|
|
30
|
+
name = process.cwd();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let npmExist = true;
|
|
34
|
+
// 判断 npm 是否存在
|
|
35
|
+
try {
|
|
36
|
+
await execa('npm', ['--version']);
|
|
37
|
+
} catch {
|
|
38
|
+
npmExist = false;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const prefix = path.join(name, DEFAULT_BUILTIN_PLUGINS_RELATIVE_PATH);
|
|
42
|
+
// 安装前端代码
|
|
43
|
+
console.log('🚀 ~ start download ~ front end files');
|
|
44
|
+
const spinner = yoctoSpinner({ text: `Loading ${DEFAULT_WEB_PACKAGE_NAME}` }).start();
|
|
45
|
+
await downloadTar(DEFAULT_WEB_PACKAGE_NAME, `${prefix}/${DEFAULT_WEB_PACKAGE_NAME}`);
|
|
46
|
+
spinner.success();
|
|
47
|
+
console.log();
|
|
48
|
+
|
|
49
|
+
console.log('🚀 ~ start download ~ required modules');
|
|
50
|
+
// 安装必须得模块
|
|
51
|
+
const moduleNames = defaultModules.map((moduleName) => `@tachybase/module-${moduleName}`);
|
|
52
|
+
let index = 1;
|
|
53
|
+
for (const moduleName of moduleNames) {
|
|
54
|
+
const spinner = yoctoSpinner({ text: `[${index++}/${moduleNames.length}] Loading ${moduleName}` }).start();
|
|
55
|
+
await downloadTar(moduleName, `${prefix}/${moduleName}`);
|
|
56
|
+
if (npmExist) {
|
|
57
|
+
await npmInstall(`${prefix}/${moduleName}`, spinner);
|
|
58
|
+
}
|
|
59
|
+
spinner.success();
|
|
60
|
+
}
|
|
61
|
+
console.log();
|
|
62
|
+
|
|
63
|
+
console.log('🚀 ~ start download ~ plugins');
|
|
64
|
+
// 安装可选的模块,由参数指定
|
|
65
|
+
index = 1;
|
|
66
|
+
const pluginNames = plugins.map((pluginName: string) => `@tachybase/plugin-${pluginName}`);
|
|
67
|
+
for (const pluginName of pluginNames) {
|
|
68
|
+
const spinner = yoctoSpinner({ text: `[${index++}/${pluginNames.length}] Loading ${pluginName}` }).start();
|
|
69
|
+
await downloadTar(pluginName, `${prefix}/${pluginName}`);
|
|
70
|
+
if (npmExist) {
|
|
71
|
+
await npmInstall(`${prefix}/${pluginName}`, spinner);
|
|
72
|
+
}
|
|
73
|
+
spinner.success();
|
|
74
|
+
}
|
|
75
|
+
console.log();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function npmInstall(target: string, spinner: yoctoSpinner.Spinner) {
|
|
79
|
+
// check "dependencies" field exists
|
|
80
|
+
const packageJsonPath = path.join(target, 'package.json');
|
|
81
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
82
|
+
if (!packageJson.dependencies || Object.keys(packageJson.dependencies).length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const originalText = spinner.text;
|
|
86
|
+
spinner.text += ' [installing deps]';
|
|
87
|
+
await execa('npm', ['install', '--omit', 'dev', '--legacy-peer-deps'], {
|
|
88
|
+
stdio: 'inherit',
|
|
89
|
+
cwd: target,
|
|
90
|
+
env: {
|
|
91
|
+
npm_config_loglevel: 'error',
|
|
92
|
+
...process.env,
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
spinner.text = originalText;
|
|
96
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fs, {
|
|
3
|
+
cpSync as _cpSync,
|
|
4
|
+
existsSync as _existsSync,
|
|
5
|
+
writeFileSync as _writeFileSync,
|
|
6
|
+
createWriteStream,
|
|
7
|
+
} from 'node:fs';
|
|
8
|
+
import { mkdir, unlink } from 'node:fs/promises';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import { pipeline } from 'node:stream/promises';
|
|
11
|
+
import TachybaseGlobal from '@tachybase/globals';
|
|
12
|
+
|
|
13
|
+
import { config } from 'dotenv';
|
|
14
|
+
import npmRegistryFetch from 'npm-registry-fetch';
|
|
15
|
+
import * as tar from 'tar';
|
|
16
|
+
|
|
17
|
+
import { DEFAULT_WEB_PACKAGE_NAME } from './constants';
|
|
18
|
+
|
|
19
|
+
export function initEnvFile(name: string) {
|
|
20
|
+
const envPath = resolve(name, '.env');
|
|
21
|
+
if (!fs.existsSync(envPath)) {
|
|
22
|
+
fs.copyFileSync(resolve(__dirname, '../presets/.env.example'), envPath);
|
|
23
|
+
console.log('.env file created.');
|
|
24
|
+
} else {
|
|
25
|
+
console.log('.env file already exists.');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function parseEnv(name: string) {
|
|
30
|
+
if (name === 'DB_UNDERSCORED') {
|
|
31
|
+
if (process.env.DB_UNDERSCORED === 'true') {
|
|
32
|
+
return 'true';
|
|
33
|
+
}
|
|
34
|
+
if (process.env.DB_UNDERSCORED) {
|
|
35
|
+
return 'true';
|
|
36
|
+
}
|
|
37
|
+
return 'false';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function parseEnvironment() {
|
|
42
|
+
const env = {
|
|
43
|
+
APP_ENV: 'development',
|
|
44
|
+
APP_KEY: 'test-jwt-secret',
|
|
45
|
+
APP_PORT: 3000,
|
|
46
|
+
API_BASE_PATH: '/api/',
|
|
47
|
+
DB_DIALECT: 'sqlite',
|
|
48
|
+
DB_STORAGE: 'storage/db/tachybase.sqlite',
|
|
49
|
+
DB_TIMEZONE: '+00:00',
|
|
50
|
+
DB_UNDERSCORED: parseEnv('DB_UNDERSCORED'),
|
|
51
|
+
DEFAULT_STORAGE_TYPE: 'local',
|
|
52
|
+
RUN_MODE: 'engine',
|
|
53
|
+
LOCAL_STORAGE_DEST: 'storage/uploads',
|
|
54
|
+
PLUGIN_STORAGE_PATH: resolve(process.cwd(), 'storage/plugins'),
|
|
55
|
+
MFSU_AD: 'none',
|
|
56
|
+
WS_PATH: '/ws',
|
|
57
|
+
SOCKET_PATH: 'storage/gateway.sock',
|
|
58
|
+
PM2_HOME: resolve(process.cwd(), './storage/.pm2'),
|
|
59
|
+
PLUGIN_PACKAGE_PREFIX: '@tachybase/plugin-,@tachybase/module-',
|
|
60
|
+
SERVER_TSCONFIG_PATH: './tsconfig.server.json',
|
|
61
|
+
PLAYWRIGHT_AUTH_FILE: resolve(process.cwd(), 'storage/playwright/.auth/admin.json'),
|
|
62
|
+
CACHE_DEFAULT_STORE: 'memory',
|
|
63
|
+
CACHE_MEMORY_MAX: 2000,
|
|
64
|
+
PLUGIN_STATICS_PATH: '/static/plugins/',
|
|
65
|
+
LOGGER_BASE_PATH: 'storage/logs',
|
|
66
|
+
APP_SERVER_BASE_URL: '',
|
|
67
|
+
APP_PUBLIC_PATH: '/',
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
config({
|
|
71
|
+
path: resolve(process.cwd(), process.env.APP_ENV_PATH || '.env'),
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
for (const key in env) {
|
|
75
|
+
if (!process.env[key]) {
|
|
76
|
+
process.env[key] = env[key];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!process.env.__env_modified__ && process.env.APP_PUBLIC_PATH) {
|
|
81
|
+
const publicPath = process.env.APP_PUBLIC_PATH.replace(/\/$/g, '');
|
|
82
|
+
const keys = ['API_BASE_PATH', 'WS_PATH', 'PLUGIN_STATICS_PATH'];
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
process.env[key] = publicPath + process.env[key];
|
|
85
|
+
}
|
|
86
|
+
process.env.__env_modified__ = '1';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!process.env.__env_modified__ && process.env.APP_SERVER_BASE_URL && !process.env.API_BASE_URL) {
|
|
90
|
+
process.env.API_BASE_URL = process.env.APP_SERVER_BASE_URL + process.env.API_BASE_PATH;
|
|
91
|
+
process.env.__env_modified__ = '1';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!process.env.SERVE_PATH) {
|
|
95
|
+
const servePath = guessServePath();
|
|
96
|
+
if (servePath) {
|
|
97
|
+
process.env.SERVE_PATH = servePath;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function guessServePath() {
|
|
103
|
+
const distPath = resolve('apps/app-web/dist/index.html');
|
|
104
|
+
const clientPath = resolve('client/index.html');
|
|
105
|
+
|
|
106
|
+
if (fs.existsSync(distPath)) {
|
|
107
|
+
return resolve('apps/app-web/dist');
|
|
108
|
+
} else if (fs.existsSync(clientPath)) {
|
|
109
|
+
return resolve('client');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const pluginPaths = TachybaseGlobal.getInstance().get<string[]>('PLUGIN_PATHS');
|
|
113
|
+
for (const basePath of pluginPaths) {
|
|
114
|
+
if (fs.existsSync(resolve(basePath, DEFAULT_WEB_PACKAGE_NAME, 'dist/index.html'))) {
|
|
115
|
+
return resolve(basePath, DEFAULT_WEB_PACKAGE_NAME, 'dist');
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function getTarballUrl(pkgName, version = 'latest') {
|
|
123
|
+
const info = await npmRegistryFetch.json(`/${pkgName}/${version}`, {
|
|
124
|
+
query: { fullMetadata: true },
|
|
125
|
+
});
|
|
126
|
+
return info.dist.tarball;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export async function downloadTar(packageName: string, target: string) {
|
|
130
|
+
const url = await getTarballUrl(packageName);
|
|
131
|
+
const tarballFile = join(target, '..', `${createHash('md5').update(packageName).digest('hex')}-tarball.gz`);
|
|
132
|
+
await mkdir(dirname(tarballFile), { recursive: true });
|
|
133
|
+
const writer = createWriteStream(tarballFile);
|
|
134
|
+
const response = await fetch(url);
|
|
135
|
+
if (!response.ok || !response.body) {
|
|
136
|
+
throw new Error(`Failed to fetch tarball: ${response.statusText}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 使用 pipeline 将 response.body 写入文件
|
|
140
|
+
await pipeline(response.body as any, writer);
|
|
141
|
+
|
|
142
|
+
await mkdir(target, { recursive: true });
|
|
143
|
+
await tar.x({
|
|
144
|
+
file: tarballFile,
|
|
145
|
+
gzip: true,
|
|
146
|
+
cwd: target,
|
|
147
|
+
strip: 1,
|
|
148
|
+
k: true,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await unlink(tarballFile);
|
|
152
|
+
}
|