@chidchanun/bcp 0.3.1 → 0.3.2
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/docs/api-freeze-snapshot.json +13 -2
- package/docs/api-manifest.json +22 -14
- package/docs/docs-web-manifest.json +6 -4
- package/docs/module-system-v2.md +386 -0
- package/docs/platform-manifest.json +19 -4
- package/docs/releases/0.3.2.md +125 -0
- package/package.json +7 -1
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/application.mjs +434 -30
- package/packages/client/src/modules.mjs +392 -0
- package/packages/client/src/modules.ts +17 -0
- package/packages/server/src/application.ts +58 -9
- package/packages/server/src/modules.ts +682 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// packages/server/src/modules.ts
|
|
2
|
+
var MODULE_V2_KIND = "bcp-module-v2";
|
|
3
|
+
var ModuleDependencyError = class extends Error {
|
|
4
|
+
constructor(message) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.name = "ModuleDependencyError";
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
var ModuleLifecycleError = class extends Error {
|
|
10
|
+
module;
|
|
11
|
+
phase;
|
|
12
|
+
cause;
|
|
13
|
+
constructor(moduleName, phase, cause) {
|
|
14
|
+
super(
|
|
15
|
+
`BCP Modules: ${phase} failed for module "${moduleName}": ${formatError(cause)}`
|
|
16
|
+
);
|
|
17
|
+
this.name = "ModuleLifecycleError";
|
|
18
|
+
this.module = moduleName;
|
|
19
|
+
this.phase = phase;
|
|
20
|
+
this.cause = cause;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
function defineModule(input) {
|
|
24
|
+
const definition = {
|
|
25
|
+
...input,
|
|
26
|
+
kind: MODULE_V2_KIND
|
|
27
|
+
};
|
|
28
|
+
validateModuleDefinition(definition);
|
|
29
|
+
return definition;
|
|
30
|
+
}
|
|
31
|
+
function isModuleDefinition(value) {
|
|
32
|
+
return Boolean(
|
|
33
|
+
value && typeof value === "object" && value.kind === MODULE_V2_KIND
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
function composeModules(roots = []) {
|
|
37
|
+
const ordered = resolveModuleOrder(roots);
|
|
38
|
+
const providers = [];
|
|
39
|
+
const plugins = [];
|
|
40
|
+
const services = [];
|
|
41
|
+
const resources = [];
|
|
42
|
+
const serviceKeys = /* @__PURE__ */ new Set();
|
|
43
|
+
for (const module of ordered) {
|
|
44
|
+
providers.push(
|
|
45
|
+
...module.providers ?? []
|
|
46
|
+
);
|
|
47
|
+
plugins.push(
|
|
48
|
+
...module.plugins ?? []
|
|
49
|
+
);
|
|
50
|
+
resources.push(
|
|
51
|
+
...module.resources ?? []
|
|
52
|
+
);
|
|
53
|
+
for (const entry of module.services ?? []) {
|
|
54
|
+
const [key] = entry;
|
|
55
|
+
if (serviceKeys.has(key)) {
|
|
56
|
+
throw new ModuleDependencyError(
|
|
57
|
+
`BCP Modules: duplicate shared service ${formatServiceKey(key)} while composing module "${module.name}".`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
serviceKeys.add(key);
|
|
61
|
+
services.push(entry);
|
|
62
|
+
}
|
|
63
|
+
validateModuleExports(
|
|
64
|
+
module
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
modules: ordered,
|
|
69
|
+
providers,
|
|
70
|
+
plugins,
|
|
71
|
+
services,
|
|
72
|
+
resources,
|
|
73
|
+
records() {
|
|
74
|
+
return ordered.map(
|
|
75
|
+
(module) => ({
|
|
76
|
+
name: module.name,
|
|
77
|
+
...module.version ? {
|
|
78
|
+
version: module.version
|
|
79
|
+
} : {},
|
|
80
|
+
imports: (module.imports ?? []).map(
|
|
81
|
+
(dependency) => dependency.name
|
|
82
|
+
),
|
|
83
|
+
providers: (module.providers ?? []).map(
|
|
84
|
+
(provider) => provider.token.description
|
|
85
|
+
),
|
|
86
|
+
exports: (module.exports ?? []).map(
|
|
87
|
+
(token) => token.description
|
|
88
|
+
),
|
|
89
|
+
plugins: (module.plugins ?? []).map(
|
|
90
|
+
(plugin) => plugin.name
|
|
91
|
+
),
|
|
92
|
+
resources: (module.resources ?? []).map(
|
|
93
|
+
(resource) => resource.name
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
},
|
|
98
|
+
exportedTokens(moduleName) {
|
|
99
|
+
const normalized = normalizeName(
|
|
100
|
+
moduleName,
|
|
101
|
+
"module name"
|
|
102
|
+
);
|
|
103
|
+
const module = ordered.find(
|
|
104
|
+
(entry) => entry.name === normalized
|
|
105
|
+
);
|
|
106
|
+
if (!module) {
|
|
107
|
+
throw new ModuleDependencyError(
|
|
108
|
+
`BCP Modules: module "${normalized}" is not part of this composition.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return [
|
|
112
|
+
...module.exports ?? []
|
|
113
|
+
];
|
|
114
|
+
},
|
|
115
|
+
createLifecycleResources(context) {
|
|
116
|
+
return ordered.flatMap(
|
|
117
|
+
(module) => [
|
|
118
|
+
...module.resources ?? [],
|
|
119
|
+
createModuleLifecycleResource(
|
|
120
|
+
module,
|
|
121
|
+
context
|
|
122
|
+
)
|
|
123
|
+
]
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
function resolveModuleOrder(roots) {
|
|
129
|
+
const byName = /* @__PURE__ */ new Map();
|
|
130
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
131
|
+
const visited = /* @__PURE__ */ new Set();
|
|
132
|
+
const order = [];
|
|
133
|
+
const visit = (module, path) => {
|
|
134
|
+
validateModuleDefinition(module);
|
|
135
|
+
const name = module.name;
|
|
136
|
+
const existing = byName.get(name);
|
|
137
|
+
if (existing && existing !== module) {
|
|
138
|
+
throw new ModuleDependencyError(
|
|
139
|
+
`BCP Modules: duplicate module name "${name}" refers to different definitions.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
byName.set(name, module);
|
|
143
|
+
if (visited.has(name)) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (visiting.has(name)) {
|
|
147
|
+
throw new ModuleDependencyError(
|
|
148
|
+
`BCP Modules: circular module dependency detected: ${[
|
|
149
|
+
...path,
|
|
150
|
+
name
|
|
151
|
+
].join(" -> ")}.`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
visiting.add(name);
|
|
155
|
+
for (const dependency of module.imports ?? []) {
|
|
156
|
+
visit(
|
|
157
|
+
dependency,
|
|
158
|
+
[
|
|
159
|
+
...path,
|
|
160
|
+
name
|
|
161
|
+
]
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
visiting.delete(name);
|
|
165
|
+
visited.add(name);
|
|
166
|
+
order.push(module);
|
|
167
|
+
};
|
|
168
|
+
for (const root of roots) {
|
|
169
|
+
visit(root, []);
|
|
170
|
+
}
|
|
171
|
+
return order;
|
|
172
|
+
}
|
|
173
|
+
function createModuleLifecycleResource(module, shared) {
|
|
174
|
+
const config = parseModuleConfig(
|
|
175
|
+
module.schema,
|
|
176
|
+
module.config
|
|
177
|
+
);
|
|
178
|
+
const context = {
|
|
179
|
+
name: module.name,
|
|
180
|
+
config,
|
|
181
|
+
container: shared.container,
|
|
182
|
+
services: shared.services,
|
|
183
|
+
hooks: shared.hooks
|
|
184
|
+
};
|
|
185
|
+
let setupComplete = false;
|
|
186
|
+
let disposed = false;
|
|
187
|
+
return {
|
|
188
|
+
name: `bcp:module:${module.name}`,
|
|
189
|
+
async start() {
|
|
190
|
+
try {
|
|
191
|
+
if (!setupComplete) {
|
|
192
|
+
await runModuleHook(
|
|
193
|
+
module,
|
|
194
|
+
"setup",
|
|
195
|
+
module.setup,
|
|
196
|
+
context
|
|
197
|
+
);
|
|
198
|
+
setupComplete = true;
|
|
199
|
+
}
|
|
200
|
+
await runModuleHook(
|
|
201
|
+
module,
|
|
202
|
+
"start",
|
|
203
|
+
module.start,
|
|
204
|
+
context
|
|
205
|
+
);
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (!disposed) {
|
|
208
|
+
disposed = true;
|
|
209
|
+
try {
|
|
210
|
+
await runModuleHook(
|
|
211
|
+
module,
|
|
212
|
+
"dispose",
|
|
213
|
+
module.dispose,
|
|
214
|
+
context
|
|
215
|
+
);
|
|
216
|
+
} catch (disposeError) {
|
|
217
|
+
throw new AggregateError(
|
|
218
|
+
[
|
|
219
|
+
error,
|
|
220
|
+
disposeError
|
|
221
|
+
],
|
|
222
|
+
`BCP Modules: startup and cleanup failed for module "${module.name}".`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
ready() {
|
|
230
|
+
return true;
|
|
231
|
+
},
|
|
232
|
+
async stop() {
|
|
233
|
+
const errors = [];
|
|
234
|
+
try {
|
|
235
|
+
await runModuleHook(
|
|
236
|
+
module,
|
|
237
|
+
"stop",
|
|
238
|
+
module.stop,
|
|
239
|
+
context
|
|
240
|
+
);
|
|
241
|
+
} catch (error) {
|
|
242
|
+
errors.push(error);
|
|
243
|
+
}
|
|
244
|
+
if (!disposed) {
|
|
245
|
+
disposed = true;
|
|
246
|
+
try {
|
|
247
|
+
await runModuleHook(
|
|
248
|
+
module,
|
|
249
|
+
"dispose",
|
|
250
|
+
module.dispose,
|
|
251
|
+
context
|
|
252
|
+
);
|
|
253
|
+
} catch (error) {
|
|
254
|
+
errors.push(error);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
if (errors.length > 0) {
|
|
258
|
+
throw new AggregateError(
|
|
259
|
+
errors,
|
|
260
|
+
`BCP Modules: shutdown failed for module "${module.name}".`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
diagnostics() {
|
|
265
|
+
return {
|
|
266
|
+
name: module.name,
|
|
267
|
+
...module.version ? {
|
|
268
|
+
version: module.version
|
|
269
|
+
} : {},
|
|
270
|
+
imports: (module.imports ?? []).map(
|
|
271
|
+
(dependency) => dependency.name
|
|
272
|
+
),
|
|
273
|
+
exports: (module.exports ?? []).map(
|
|
274
|
+
(token) => token.description
|
|
275
|
+
)
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
async function runModuleHook(module, phase, hook, context) {
|
|
281
|
+
if (!hook) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
try {
|
|
285
|
+
await hook(context);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
throw new ModuleLifecycleError(
|
|
288
|
+
module.name,
|
|
289
|
+
phase,
|
|
290
|
+
error
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
function validateModuleDefinition(module) {
|
|
295
|
+
if (!module || typeof module !== "object") {
|
|
296
|
+
throw new TypeError(
|
|
297
|
+
"BCP Modules: module must be an object."
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
if (module.kind !== MODULE_V2_KIND) {
|
|
301
|
+
throw new TypeError(
|
|
302
|
+
`BCP Modules: module kind must be "${MODULE_V2_KIND}". Use defineModule().`
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
normalizeName(
|
|
306
|
+
module.name,
|
|
307
|
+
"module name"
|
|
308
|
+
);
|
|
309
|
+
for (const [label, value] of [
|
|
310
|
+
["imports", module.imports],
|
|
311
|
+
["providers", module.providers],
|
|
312
|
+
["exports", module.exports],
|
|
313
|
+
["plugins", module.plugins],
|
|
314
|
+
["resources", module.resources]
|
|
315
|
+
]) {
|
|
316
|
+
if (value !== void 0 && !Array.isArray(value)) {
|
|
317
|
+
throw new TypeError(
|
|
318
|
+
`BCP Modules: ${label} must be an array.`
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
for (const [phase, hook] of [
|
|
323
|
+
["setup", module.setup],
|
|
324
|
+
["start", module.start],
|
|
325
|
+
["stop", module.stop],
|
|
326
|
+
["dispose", module.dispose]
|
|
327
|
+
]) {
|
|
328
|
+
if (hook !== void 0 && typeof hook !== "function") {
|
|
329
|
+
throw new TypeError(
|
|
330
|
+
`BCP Modules: ${phase} must be a function.`
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
function validateModuleExports(module) {
|
|
336
|
+
const available = /* @__PURE__ */ new Set();
|
|
337
|
+
for (const provider of module.providers ?? []) {
|
|
338
|
+
available.add(
|
|
339
|
+
provider.token.id
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
for (const dependency of module.imports ?? []) {
|
|
343
|
+
for (const token of dependency.exports ?? []) {
|
|
344
|
+
available.add(token.id);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
for (const token of module.exports ?? []) {
|
|
348
|
+
if (!available.has(token.id)) {
|
|
349
|
+
throw new ModuleDependencyError(
|
|
350
|
+
`BCP Modules: module "${module.name}" exports "${token.description}" but does not provide or import it.`
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
function parseModuleConfig(parser, value) {
|
|
356
|
+
if (!parser) {
|
|
357
|
+
return value;
|
|
358
|
+
}
|
|
359
|
+
if (typeof parser === "function") {
|
|
360
|
+
return parser(value);
|
|
361
|
+
}
|
|
362
|
+
return parser.parse(value);
|
|
363
|
+
}
|
|
364
|
+
function normalizeName(value, label) {
|
|
365
|
+
if (typeof value !== "string") {
|
|
366
|
+
throw new TypeError(
|
|
367
|
+
`BCP Modules: ${label} must be a string.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
const normalized = value.trim();
|
|
371
|
+
if (!normalized) {
|
|
372
|
+
throw new TypeError(
|
|
373
|
+
`BCP Modules: ${label} cannot be empty.`
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
return normalized;
|
|
377
|
+
}
|
|
378
|
+
function formatServiceKey(key) {
|
|
379
|
+
return typeof key === "symbol" ? key.description ? `Symbol(${key.description})` : key.toString() : key;
|
|
380
|
+
}
|
|
381
|
+
function formatError(value) {
|
|
382
|
+
return value instanceof Error ? value.message : String(value);
|
|
383
|
+
}
|
|
384
|
+
export {
|
|
385
|
+
MODULE_V2_KIND,
|
|
386
|
+
ModuleDependencyError,
|
|
387
|
+
ModuleLifecycleError,
|
|
388
|
+
composeModules,
|
|
389
|
+
defineModule,
|
|
390
|
+
isModuleDefinition,
|
|
391
|
+
resolveModuleOrder
|
|
392
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
MODULE_V2_KIND,
|
|
3
|
+
ModuleDependencyError,
|
|
4
|
+
ModuleLifecycleError,
|
|
5
|
+
composeModules,
|
|
6
|
+
defineModule,
|
|
7
|
+
isModuleDefinition,
|
|
8
|
+
resolveModuleOrder,
|
|
9
|
+
|
|
10
|
+
type ModuleComposition,
|
|
11
|
+
type ModuleConfigParser,
|
|
12
|
+
type ModuleConfigSchema,
|
|
13
|
+
type ModuleContext,
|
|
14
|
+
type ModuleDefinition,
|
|
15
|
+
type ModuleInput,
|
|
16
|
+
type ModuleRecord,
|
|
17
|
+
} from "../../server/src/modules.js";
|
|
@@ -20,6 +20,14 @@ import {
|
|
|
20
20
|
type DeploymentSignalOptions,
|
|
21
21
|
} from "./deployment.js";
|
|
22
22
|
|
|
23
|
+
import {
|
|
24
|
+
composeModules,
|
|
25
|
+
isModuleDefinition,
|
|
26
|
+
|
|
27
|
+
type ModuleComposition,
|
|
28
|
+
type ModuleDefinition,
|
|
29
|
+
} from "./modules.js";
|
|
30
|
+
|
|
23
31
|
import {
|
|
24
32
|
createPluginHost,
|
|
25
33
|
|
|
@@ -62,6 +70,7 @@ export interface ApplicationContext<TConfig = unknown> {
|
|
|
62
70
|
readonly version?: string;
|
|
63
71
|
readonly config: TConfig;
|
|
64
72
|
readonly container: ServiceContainer;
|
|
73
|
+
readonly modules: ModuleComposition;
|
|
65
74
|
readonly services: PluginServiceRegistry;
|
|
66
75
|
readonly hooks: PluginHookBus;
|
|
67
76
|
readonly plugins: PluginHost;
|
|
@@ -75,9 +84,12 @@ export interface ApplicationDefinition<TConfig = unknown> {
|
|
|
75
84
|
version?: string;
|
|
76
85
|
config?: unknown;
|
|
77
86
|
schema?: ApplicationConfigParser<TConfig>;
|
|
78
|
-
providers?: readonly ServiceProvider<
|
|
87
|
+
providers?: readonly ServiceProvider<any>[];
|
|
79
88
|
plugins?: readonly PluginDefinition<any>[];
|
|
80
|
-
modules?: readonly
|
|
89
|
+
modules?: readonly (
|
|
90
|
+
| PluginModule
|
|
91
|
+
| ModuleDefinition<any>
|
|
92
|
+
)[];
|
|
81
93
|
pluginConfigs?: Record<string, unknown>;
|
|
82
94
|
services?: Iterable<
|
|
83
95
|
readonly [PluginServiceKey, unknown]
|
|
@@ -108,6 +120,7 @@ export interface Application<TConfig = unknown> {
|
|
|
108
120
|
readonly config: TConfig;
|
|
109
121
|
readonly context: ApplicationContext<TConfig>;
|
|
110
122
|
readonly container: ServiceContainer;
|
|
123
|
+
readonly modules: ModuleComposition;
|
|
111
124
|
readonly services: PluginServiceRegistry;
|
|
112
125
|
readonly hooks: PluginHookBus;
|
|
113
126
|
readonly plugins: PluginHost;
|
|
@@ -201,23 +214,42 @@ export function createApp<TConfig = unknown>(
|
|
|
201
214
|
definition.schema,
|
|
202
215
|
definition.config
|
|
203
216
|
);
|
|
217
|
+
const moduleInputs =
|
|
218
|
+
definition.modules ?? [];
|
|
219
|
+
const modules =
|
|
220
|
+
composeModules(
|
|
221
|
+
moduleInputs.filter(
|
|
222
|
+
isModuleDefinition
|
|
223
|
+
)
|
|
224
|
+
);
|
|
225
|
+
const legacyModules =
|
|
226
|
+
moduleInputs.filter(
|
|
227
|
+
module =>
|
|
228
|
+
!isModuleDefinition(module)
|
|
229
|
+
) as PluginModule[];
|
|
204
230
|
const container =
|
|
205
231
|
createServiceContainer({
|
|
206
232
|
name:
|
|
207
233
|
`${name}:container`,
|
|
208
|
-
providers:
|
|
209
|
-
|
|
234
|
+
providers: [
|
|
235
|
+
...modules.providers,
|
|
236
|
+
...(definition.providers ?? []),
|
|
237
|
+
],
|
|
210
238
|
});
|
|
211
239
|
const plugins =
|
|
212
240
|
createPluginHost({
|
|
213
|
-
plugins:
|
|
214
|
-
|
|
241
|
+
plugins: [
|
|
242
|
+
...modules.plugins,
|
|
243
|
+
...(definition.plugins ?? []),
|
|
244
|
+
],
|
|
215
245
|
modules:
|
|
216
|
-
|
|
246
|
+
legacyModules,
|
|
217
247
|
configs:
|
|
218
248
|
definition.pluginConfigs,
|
|
219
|
-
services:
|
|
220
|
-
|
|
249
|
+
services: [
|
|
250
|
+
...modules.services,
|
|
251
|
+
...(definition.services ?? []),
|
|
252
|
+
],
|
|
221
253
|
});
|
|
222
254
|
const deployment =
|
|
223
255
|
createDeploymentRuntime({
|
|
@@ -258,6 +290,7 @@ export function createApp<TConfig = unknown>(
|
|
|
258
290
|
: {}),
|
|
259
291
|
config,
|
|
260
292
|
container,
|
|
293
|
+
modules,
|
|
261
294
|
services:
|
|
262
295
|
plugins.services,
|
|
263
296
|
hooks:
|
|
@@ -333,6 +366,19 @@ export function createApp<TConfig = unknown>(
|
|
|
333
366
|
},
|
|
334
367
|
});
|
|
335
368
|
|
|
369
|
+
for (
|
|
370
|
+
const resource
|
|
371
|
+
of modules.createLifecycleResources({
|
|
372
|
+
container,
|
|
373
|
+
services:
|
|
374
|
+
plugins.services,
|
|
375
|
+
hooks:
|
|
376
|
+
plugins.hooks,
|
|
377
|
+
})
|
|
378
|
+
) {
|
|
379
|
+
deployment.addResource(resource);
|
|
380
|
+
}
|
|
381
|
+
|
|
336
382
|
for (
|
|
337
383
|
const resource
|
|
338
384
|
of definition.resources ?? []
|
|
@@ -351,6 +397,7 @@ export function createApp<TConfig = unknown>(
|
|
|
351
397
|
config,
|
|
352
398
|
context,
|
|
353
399
|
container,
|
|
400
|
+
modules,
|
|
354
401
|
services:
|
|
355
402
|
plugins.services,
|
|
356
403
|
hooks:
|
|
@@ -717,6 +764,8 @@ export function createApp<TConfig = unknown>(
|
|
|
717
764
|
node =>
|
|
718
765
|
node.description
|
|
719
766
|
),
|
|
767
|
+
modules:
|
|
768
|
+
modules.records(),
|
|
720
769
|
pluginCount:
|
|
721
770
|
plugins.plugins().length,
|
|
722
771
|
};
|