mm_module 1.0.2 → 1.0.4

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/README.md ADDED
@@ -0,0 +1,171 @@
1
+ # mm_module
2
+
3
+ 内核模块管理框架件:基于 npm 实现模块的搜索、安装、卸载与清单编排,并按模块 package.json 声明的 `group` 层级分层装载与声明体检(不注册能力)。对外只暴露一个门面类 `ModuleManager`,宿主用它即可完成"检索远端 → 安装 → 按层级加载 → 更新 → 卸载"的完整模块生命周期管理;模块的自动发现以 npm 关键词 `mm_mod` 为唯一判定依据。
4
+
5
+ ## 项目信息
6
+
7
+ - **当前版本**: 1.0.2
8
+ - **更新日期**: 2026-10-04
9
+ - **Gitee地址**: https://gitee.com/qiuwenwu91/mm_module(package.json 中 repository 指向 github.com/mm-modules,为复制遗留的错误地址)
10
+ - **依赖**: `mm_corejs`(file 依赖,提供 `Manager` 基类与 `Store`);运行环境要求 Node.js >= 18;npm 命令经子进程调用,需宿主可执行 `npm`
11
+
12
+ ## 特点
13
+
14
+ - 单一门面:`index.js` 仅导出 `ModuleManager`,清单规范(lib/spec)与 npm 通道(lib/npm_channel)为内部实现
15
+ - 声明式发现:只把 package.json 带 `mm_mod` 关键词、名称命中前缀(默认 `mm_`)的包纳入清单
16
+ - 分层装载:按 `tool → infra → in_port → in_adapter → channel → service → domain → database → out_adapter → out_port → unknown` 的层级序加载,层内保持传入顺序
17
+ - 生命周期实体自动驱动:模块导出同时具备 `init` 与 `start` 函数时自动补全 init/start,卸载时自动 stop/dispose 并清 require 缓存
18
+ - 失败可回滚:`load` 默认 fail_fast,任一模块失败即逆序回滚本次新加载的模块并抛错
19
+ - 测试友好:npm 执行可经 `config.runner` 注入假执行器,全程离线可演示(见 example/demo.js)
20
+
21
+ ## 安装
22
+
23
+ ```bash
24
+ npm install mm_module
25
+ ```
26
+
27
+ ## 基本使用
28
+
29
+ ```javascript
30
+ const { ModuleManager } = require('mm_module');
31
+
32
+ async function main() {
33
+ const manager = new ModuleManager({ cwd: process.cwd() });
34
+ await manager.init(); // 构建本地清单(读宿主 package.json 依赖声明)
35
+ await manager.start();
36
+
37
+ const specs = await manager.search('pay'); // 远端检索,实际检索词 mm_pay
38
+ console.log(manager.names()); // 本地已装模块名(按层级序)
39
+
40
+ await manager.install('mm_pay_wechat'); // 安装并自动加载
41
+ await manager.load(manager.names()); // 按层级排序批量加载
42
+ await manager.unload('mm_pay_wechat'); // 卸载加载(stop/dispose + 清缓存)
43
+ await manager.uninstall('mm_pay_wechat'); // npm 卸载并移出清单
44
+
45
+ await manager.dispose(); // 释放时自动逆序卸载全部剩余模块
46
+ }
47
+ ```
48
+
49
+ ## API 说明
50
+
51
+ ### 构造与配置
52
+
53
+ `new ModuleManager(config = {}, server)`:`config` 覆盖静态默认配置,`server` 为框架引用(满足 mm_corejs Base 最小契约即可,可为空对象)。
54
+
55
+ | 配置项 | 默认值 | 说明 |
56
+ | --- | --- | --- |
57
+ | `cwd` | `process.cwd()` | 宿主目录(读依赖声明、执行 npm 命令的工作目录) |
58
+ | `prefix` | `'mm_'` | 模块名前缀,检索与查询只命中该前缀开头的模块;空串时不过滤 |
59
+ | `keyword` | `'mm_mod'` | 发现关键词,模块声明合法性的唯一判定 |
60
+ | `limit` | `250` | 远端检索条数上限 |
61
+ | `registry` | `''` | 指定 npm registry,非空时追加 `--registry=` 参数 |
62
+ | `runner` | `null` | 注入的 npm 执行函数 `(args, options) => { stdout }`,用于测试与定制 |
63
+ | `include_untagged` | `false` | 本地清单是否纳入未声明关键词的依赖 |
64
+ | `fail_fast` | `true` | `load` 失败时是否回滚并抛错 |
65
+
66
+ ### 模块清单项(spec)
67
+
68
+ 所有查询方法返回的条目由包描述归一化而来,字段固定为:
69
+
70
+ | 字段 | 类型 | 说明 |
71
+ | --- | --- | --- |
72
+ | `name` / `version` / `description` | `string\|null` | 包名 / 版本 / 描述 |
73
+ | `group` | `string` | 层级;未声明或非法时为 `'unknown'` |
74
+ | `isolation` | `string\|null` | 隔离声明(透传 package.json 的 `isolation` 字段) |
75
+ | `keywords` | `Array` | 关键词列表 |
76
+ | `homepage` | `string\|null` | 主页,缺失时回退 repository 地址 |
77
+ | `installed` | `boolean` | 是否本地已装 |
78
+ | `path` | `string\|null` | 本地包根目录(远端项为 null) |
79
+ | `source` | `'local'\|'remote'` | 清单项来源 |
80
+
81
+ ### 查找
82
+
83
+ | 方法 | 签名 | 返回值 | 说明 |
84
+ | --- | --- | --- | --- |
85
+ | `search` | `async search(keyword = '', options = {})` | `Promise<Array>` | 远端检索(`npm search`),只命中前缀开头且带关键词的模块,标记本地已装项;`options.limit` 覆盖条数上限;结果按层级序排序 |
86
+ | `inspect` | `async inspect(name)` | `Promise<object\|null>` | 查询单个模块,本地清单优先,未装时查远端(`npm view`);非前缀名直接返回 `null` |
87
+ | `list` | `list(filter = {})` | `Array` | 本地已装清单;`filter` 支持 `group / installed / isolation / keyword / text`(text 命中名称、描述、关键词) |
88
+ | `names` | `names(filter = {})` | `Array` | 本地模块名列表,可直接交给 `load` |
89
+ | `summary` | `summary(filter = {})` | `object` | 按层级汇总:`{ total, groups }`,groups 为层级 → 模块名数组 |
90
+
91
+ ### 安装与卸载
92
+
93
+ | 方法 | 签名 | 返回值 | 说明 |
94
+ | --- | --- | --- | --- |
95
+ | `install` | `async install(name, options = {})` | `Promise<object>` | 校验发现关键词后 `npm install --save`,成功后重建本地清单并默认自动加载;`options.force` 跳过关键词校验、`options.version` 指定版本、`options.load: false` 跳过自动加载;已安装时幂等(仅补自动加载) |
96
+ | `uninstall` | `async uninstall(name)` | `Promise<object>` | 已加载先自动卸载,再 `npm uninstall` 并移出本地清单;返回 `{ name, removed }`,未装时 `removed: false` 幂等返回 |
97
+
98
+ ### 更新版本
99
+
100
+ | 方法 | 签名 | 返回值 | 说明 |
101
+ | --- | --- | --- | --- |
102
+ | `update` | `async update(name, options = {})` | `Promise<object\|null>` | 远端版本高于本地时重装并(若原本已加载)重载;无新版返回 `null`;`options.load: false` 更新后不重载。未安装或远端不可用抛 `Error` |
103
+ | `updateAll` | `async updateAll(options = {})` | `Promise<Array>` | 逐个检查本地模块,返回更新过的清单项数组(无远端信息与无新版的跳过) |
104
+
105
+ ### 加载与卸载
106
+
107
+ | 方法 | 签名 | 返回值 | 说明 |
108
+ | --- | --- | --- | --- |
109
+ | `load` | `async load(names, options = {})` | `Promise<object>` | 按层级排序后逐个装载;返回 `{ loaded, order, failed }`;`options.fail_fast` 覆盖实例配置——为真时任一失败即逆序回滚本次新加载模块并抛错,为假时失败记入 `failed` 继续 |
110
+ | `unload` | `async unload(name)` | `Promise<boolean>` | 从受管集合移除并触发 disposer(实体 stop/dispose、清 require 缓存) |
111
+ | `doctor` | `doctor()` | `object` | 静态体检已加载模块:`{ untagged, ungrouped }`(未带关键词 / 未声明合法层级) |
112
+
113
+ 装载约定:入口经 `require.resolve` 定位;导出对象同时具备 `init` 与 `start` 函数时视为生命周期实体,自动驱动至运行态(具备 `getState/isRunning` 的按状态补调用);已装载模块幂等跳过。
114
+
115
+ ### 继承自 mm_corejs Manager 的能力
116
+
117
+ `init()` / `start()` / `stop()` / `dispose()`(dispose 时逆序触发各模块 disposer,即自动全部卸载)、`has(name)`、`get/del/set/keys/size`、`log(level, msg)`、`state`。
118
+
119
+ ## 使用示例
120
+
121
+ ### 场景一:宿主启动时按层级批量装载
122
+
123
+ ```javascript
124
+ const { ModuleManager } = require('mm_module');
125
+
126
+ async function bootstrap() {
127
+ const manager = new ModuleManager({ cwd: __dirname });
128
+ await manager.init();
129
+ await manager.start();
130
+
131
+ // 乱序传入也会按 group 层级排序执行,失败自动回滚
132
+ const result = await manager.load(manager.names());
133
+ console.log('加载顺序:', result.order.join(' -> '));
134
+ console.log('体检:', manager.doctor());
135
+ }
136
+ ```
137
+
138
+ ### 场景二:检索并安装远端模块
139
+
140
+ ```javascript
141
+ const { ModuleManager } = require('mm_module');
142
+
143
+ async function addModule() {
144
+ const manager = new ModuleManager({ cwd: process.cwd(), registry: 'https://registry.npmmirror.com' });
145
+ await manager.init();
146
+
147
+ const specs = await manager.search('pay'); // 检索 mm_pay 前缀模块
148
+ if (specs.length > 0) {
149
+ const spec = await manager.install(specs[0].name); // 安装 + 自动加载
150
+ console.log(`已安装 ${spec.name}@${spec.version}`);
151
+ }
152
+ }
153
+ ```
154
+
155
+ ### 场景三:离线测试(注入假 npm 执行器)
156
+
157
+ ```javascript
158
+ const { ModuleManager } = require('mm_module');
159
+
160
+ const fakeRunner = async (args) => {
161
+ if (args[0] === 'search') {
162
+ return { stdout: JSON.stringify([{ name: 'mm_demo_mod', version: '1.0.0', keywords: ['mm_mod'], group: 'tool' }]) };
163
+ }
164
+ return { stdout: '' };
165
+ };
166
+
167
+ const manager = new ModuleManager({ cwd: process.cwd(), runner: fakeRunner });
168
+ const specs = await manager.search('demo'); // 不触网完成检索
169
+ ```
170
+
171
+ > 完整端到端演示(装配/查找/安装/加载/更新/卸载/体检/释放)见包内 `example/demo.js`,运行:`node example/demo.js`。
@@ -0,0 +1,647 @@
1
+ /**
2
+ * 模块管理器
3
+ * 基于 mm_corejs 的 Manager 基类,统一实现模块的查找、安装、卸载、更新版本、加载与卸载:
4
+ * 受管集合承载已加载模块(dispose 时由基类逆序触发 disposer,即自动全部卸载),
5
+ * Store 承载本地与远端清单,npm 命令经 lib/npm_channel 执行,清单规范见 lib/spec,
6
+ * 宿主无需注入任何装载契约
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+ const { Manager } = require('mm_mod');
12
+ const { Store } = require('mm_corejs');
13
+ const {
14
+ KEYWORD,
15
+ DEFAULT_PREFIX,
16
+ UNKNOWN_GROUP,
17
+ _groupOrder,
18
+ _readSpec,
19
+ _checkSpec,
20
+ _matchSpec,
21
+ _groupSpecs,
22
+ _sortedSpecs,
23
+ _isVersionNewer
24
+ } = require('./spec');
25
+ const { _runNpm, _parseRows, _parseObject } = require('./npm_channel');
26
+
27
+ // 远端检索条数上限默认值
28
+ const DEFAULT_LIMIT = 250;
29
+
30
+ /**
31
+ * 把模块导出识别为生命周期实体:同时具备 init 与 start 函数的对象即实体实例
32
+ * @param {any} exports 模块导出值
33
+ * @returns {object|null} 实体实例或 null
34
+ */
35
+ function _toEntity(exports) {
36
+ if (exports === null || typeof exports !== 'object') return null;
37
+ if (typeof exports.init === 'function' && typeof exports.start === 'function') return exports;
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * 驱动实体进入运行态:具备状态查询的实体按状态补 init/start,
43
+ * 无状态查询的普通实体依次调用 init 与 start
44
+ * @param {object} entity 生命周期实体
45
+ * @returns {Promise<void>} 无返回值
46
+ */
47
+ async function _startEntity(entity) {
48
+ if (typeof entity.getState !== 'function') {
49
+ await entity.init();
50
+ await entity.start();
51
+ return;
52
+ }
53
+ if ((await entity.getState()) === 'created') await entity.init();
54
+ if (!(await entity.isRunning())) await entity.start();
55
+ }
56
+
57
+ /**
58
+ * 停止并释放实体,非实体时跳过
59
+ * @param {object|null} entity 生命周期实体
60
+ * @returns {Promise<void>} 无返回值
61
+ */
62
+ async function _stopEntity(entity) {
63
+ if (!entity) return;
64
+ if (typeof entity.stop === 'function') await entity.stop();
65
+ if (typeof entity.dispose === 'function') await entity.dispose();
66
+ }
67
+
68
+ /**
69
+ * 模块管理器:查找、安装、卸载、更新版本、加载与卸载的唯一门面
70
+ */
71
+ class ModuleManager extends Manager {
72
+ static config = {
73
+ name: 'module',
74
+ version: '1.0.0',
75
+ description: '模块管理器:基于 mm_corejs 的模块查找/安装/卸载/更新/加载编排',
76
+ cwd: '',
77
+ prefix: DEFAULT_PREFIX,
78
+ keyword: KEYWORD,
79
+ limit: DEFAULT_LIMIT,
80
+ registry: '',
81
+ runner: null,
82
+ include_untagged: false,
83
+ fail_fast: true
84
+ };
85
+
86
+ /**
87
+ * 构造函数
88
+ * @param {object} config 实体配置,覆盖静态默认值:cwd / prefix / keyword / limit / registry / runner / include_untagged / fail_fast
89
+ * @param {object} server 框架引用,满足 mm_corejs Base 的最小契约即可(可为空对象)
90
+ */
91
+ constructor(config = {}, server) {
92
+ super(config, server);
93
+ if (typeof this.config.cwd !== 'string' || this.config.cwd.length === 0) {
94
+ this.config.cwd = process.cwd();
95
+ }
96
+ if (typeof this.config.prefix !== 'string') this.config.prefix = DEFAULT_PREFIX;
97
+ /** @type {Store} 本地清单:宿主依赖中声明合规的模块 */
98
+ this._local = new Store({ key: 'name', like: true });
99
+ /** @type {Store} 远端检索缓存 */
100
+ this._remote = new Store({ key: 'name', like: true });
101
+ }
102
+
103
+ // ---------- 生命周期 ----------
104
+ /**
105
+ * 初始化钩子:构建本地清单
106
+ * @returns {Promise<void>} 无返回值
107
+ */
108
+ async onInit() {
109
+ this._rebuildLocal();
110
+ this.log('info', `本地清单已构建,共 ${this._local.count()} 个模块`);
111
+ }
112
+
113
+ // ---------- 查找 ----------
114
+ /**
115
+ * 远端检索模块,只命中配置前缀开头的模块名,并标记本地已装项
116
+ * @param {string} keyword 检索关键词(前缀之后的部分),空串时按前缀列全量
117
+ * @param {object} options 检索选项,支持 limit 覆盖条数上限
118
+ * @returns {Promise<Array>} 模块清单项数组,按层级序排序
119
+ * @throws {TypeError} 参数类型错误时抛出
120
+ */
121
+ async search(keyword = '', options = {}) {
122
+ if (typeof keyword !== 'string') throw new TypeError('检索关键词必须是字符串');
123
+ if (options === null || typeof options !== 'object') throw new TypeError('options 必须是对象');
124
+ const limit = typeof options.limit === 'number' && options.limit > 0 ? options.limit : this.config.limit;
125
+ const args = ['search', this._buildQuery(keyword), '--json', `--searchlimit=${limit}`];
126
+ const rows = _parseRows(await this._execNpm(args));
127
+ const specs = [];
128
+ this._remote.clear();
129
+ for (const row of rows) {
130
+ const spec = this._toSpec(row, true);
131
+ if (spec === null || !this._hasPrefix(spec.name)) continue;
132
+ const local = this._local.getById(spec.name);
133
+ const item = local ? Object.assign({}, spec, { installed: true }) : spec;
134
+ this._remote.add(item);
135
+ specs.push(item);
136
+ }
137
+ return _sortedSpecs(specs);
138
+ }
139
+
140
+ /**
141
+ * 查询单个模块,本地清单优先,未装时查远端;非前缀名直接返回 null
142
+ * @param {string} name 模块名
143
+ * @returns {Promise<object|null>} 模块清单项,不存在时为 null
144
+ * @throws {TypeError} name 不是非空字符串时抛出
145
+ */
146
+ async inspect(name) {
147
+ if (typeof name !== 'string' || name.length === 0) {
148
+ throw new TypeError('模块名必须是非空字符串');
149
+ }
150
+ const local = this._local.getById(name);
151
+ if (local !== undefined) return local;
152
+ if (!this._hasPrefix(name)) return null;
153
+ const remote = await this._view(name);
154
+ if (remote !== null && this._remote.getById(remote.name) === undefined) {
155
+ this._remote.add(remote);
156
+ }
157
+ return remote;
158
+ }
159
+
160
+ /**
161
+ * 本地已装模块清单
162
+ * @param {object} filter 过滤条件,支持 group / installed / isolation / keyword / text
163
+ * @returns {Array} 模块清单项数组,按层级序排序
164
+ * @throws {TypeError} filter 不是对象时抛出
165
+ */
166
+ list(filter = {}) {
167
+ if (filter === null || typeof filter !== 'object') throw new TypeError('filter 必须是对象');
168
+ const specs = this._local.list().filter((spec) => _matchSpec(spec, filter));
169
+ return _sortedSpecs(specs);
170
+ }
171
+
172
+ /**
173
+ * 本地模块名列表,可直接交给 load
174
+ * @param {object} filter 过滤条件
175
+ * @returns {Array} 模块名数组
176
+ */
177
+ names(filter = {}) {
178
+ return this.list(filter).map((spec) => spec.name);
179
+ }
180
+
181
+ /**
182
+ * 按层级汇总本地模块
183
+ * @param {object} filter 过滤条件
184
+ * @returns {object} 含 total 与 groups 的汇总,groups 为层级到模块名数组
185
+ */
186
+ summary(filter = {}) {
187
+ const specs = this.list(filter);
188
+ const buckets = _groupSpecs(specs);
189
+ const groups = {};
190
+ for (const key of Object.keys(buckets)) {
191
+ groups[key] = buckets[key].map((spec) => spec.name);
192
+ }
193
+ return { total: specs.length, groups };
194
+ }
195
+
196
+ // ---------- 安装与卸载 ----------
197
+ /**
198
+ * 安装模块:校验发现关键词后执行 npm install,成功后重建本地清单并默认自动加载
199
+ * @param {string} name 模块名
200
+ * @param {object} options 安装选项
201
+ * @param {boolean} options.force 为真时跳过关键词校验
202
+ * @param {string} options.version 指定安装版本
203
+ * @param {boolean} options.load 为假时跳过安装后自动加载
204
+ * @returns {Promise<object>} 安装后的模块清单项
205
+ * @throws {TypeError} 参数类型错误时抛出
206
+ * @throws {Error} 模块不存在、未声明关键词或自动加载失败时抛出
207
+ */
208
+ async install(name, options = {}) {
209
+ if (typeof name !== 'string' || name.length === 0) {
210
+ throw new TypeError('模块名必须是非空字符串');
211
+ }
212
+ if (options === null || typeof options !== 'object') throw new TypeError('options 必须是对象');
213
+ const local = this._local.getById(name);
214
+ if (local !== undefined) {
215
+ await this._autoLoad(name, options);
216
+ return local;
217
+ }
218
+ const remote = await this._view(name);
219
+ this._assertInstallable(remote, name, options);
220
+ await this._execNpm(['install', this._target(name, options), '--save']);
221
+ this._rebuildLocal();
222
+ const spec = this._local.getById(name) || remote;
223
+ await this._autoLoad(name, options);
224
+ return spec;
225
+ }
226
+
227
+ /**
228
+ * 卸载模块:已加载先自动卸载,再执行 npm uninstall 并移出本地清单;未装时幂等返回
229
+ * @param {string} name 模块名
230
+ * @returns {Promise<object>} 含 name 与 removed 的结果
231
+ * @throws {TypeError} name 不是非空字符串时抛出
232
+ */
233
+ async uninstall(name) {
234
+ if (typeof name !== 'string' || name.length === 0) {
235
+ throw new TypeError('模块名必须是非空字符串');
236
+ }
237
+ if (this._local.getById(name) === undefined) return { name, removed: false };
238
+ if (this.has(name)) await this.del(name);
239
+ await this._execNpm(['uninstall', name]);
240
+ this._local.delById(name);
241
+ return { name, removed: true };
242
+ }
243
+
244
+ // ---------- 更新版本 ----------
245
+ /**
246
+ * 更新模块版本:远端最新版高于本地时重装并(若已加载)重载;无新版返回 null
247
+ * @param {string} name 模块名
248
+ * @param {object} options 更新选项
249
+ * @param {boolean} options.load 为假时更新后不重载已加载的模块
250
+ * @returns {Promise<object|null>} 更新后的模块清单项,无新版时为 null
251
+ * @throws {TypeError} 参数类型错误时抛出
252
+ * @throws {Error} 模块未安装或远端信息不可用时抛出
253
+ */
254
+ async update(name, options = {}) {
255
+ if (typeof name !== 'string' || name.length === 0) {
256
+ throw new TypeError('模块名必须是非空字符串');
257
+ }
258
+ if (options === null || typeof options !== 'object') throw new TypeError('options 必须是对象');
259
+ const local = this._local.getById(name);
260
+ if (local === undefined) throw new Error(`模块 ${name} 未安装,无法更新`);
261
+ const remote = await this._view(name);
262
+ if (remote === null) throw new Error(`模块 ${name} 远端信息不可用,无法更新`);
263
+ return this._applyUpdate(name, local, remote, options);
264
+ }
265
+
266
+ /**
267
+ * 执行更新:远端版本高于本地时重装并(若已加载)重载,无新版返回 null
268
+ * @param {string} name 模块名
269
+ * @param {object} local 本地清单项
270
+ * @param {object} remote 远端清单项
271
+ * @param {object} options 更新选项,load 为假时更新后不重载
272
+ * @returns {Promise<object|null>} 更新后的模块清单项,无新版时为 null
273
+ */
274
+ async _applyUpdate(name, local, remote, options) {
275
+ if (!_isVersionNewer(remote.version, local.version)) return null;
276
+ await this._execNpm(['install', `${name}@${remote.version}`, '--save']);
277
+ const was_loaded = this.has(name);
278
+ if (was_loaded) await this.del(name);
279
+ this._rebuildLocal();
280
+ const spec = this._local.getById(name);
281
+ if (was_loaded && options.load !== false) await this._loadOne(name);
282
+ return spec;
283
+ }
284
+
285
+ /**
286
+ * 更新全部本地模块:逐个查远端,无远端信息与无新版的模块均跳过,返回更新过的清单项
287
+ * @param {object} options 更新选项,load 为假时更新后不重载
288
+ * @returns {Promise<Array>} 更新过的模块清单项数组
289
+ * @throws {TypeError} options 不是对象时抛出
290
+ */
291
+ async updateAll(options = {}) {
292
+ if (options === null || typeof options !== 'object') throw new TypeError('options 必须是对象');
293
+ const updated = [];
294
+ for (const spec of this._local.list()) {
295
+ const remote = await this._view(spec.name);
296
+ if (remote === null) continue;
297
+ const result = await this._applyUpdate(spec.name, spec, remote, options);
298
+ if (result) updated.push(result);
299
+ }
300
+ return updated;
301
+ }
302
+
303
+ // ---------- 加载与卸载 ----------
304
+ /**
305
+ * 加载模块:按声明层级排序后逐个装载
306
+ * fail_fast 为真时任一失败即逆序回滚本次新加载的模块并抛错,否则记入 failed 继续
307
+ * @param {Array} names 模块名列表
308
+ * @param {object} options 加载选项,fail_fast 覆盖实例配置
309
+ * @returns {Promise<object>} 含 loaded(本次新加载) / order / failed 的加载结果
310
+ * @throws {TypeError} 参数类型错误时抛出
311
+ */
312
+ async load(names, options = {}) {
313
+ if (!Array.isArray(names)) throw new TypeError('模块名列表必须是数组');
314
+ if (options === null || typeof options !== 'object') throw new TypeError('options 必须是对象');
315
+ const fail_fast =
316
+ options.fail_fast === undefined
317
+ ? this.config.fail_fast !== false
318
+ : options.fail_fast !== false;
319
+ const order = this._sortNamesByGroup(names);
320
+ const loaded = [];
321
+ const failed = [];
322
+ for (const name of order) {
323
+ try {
324
+ const is_loaded = await this._loadOne(name);
325
+ if (is_loaded) loaded.push(name);
326
+ } catch (error) {
327
+ if (fail_fast) {
328
+ await this._rollback(loaded);
329
+ throw error;
330
+ }
331
+ failed.push({ name, error });
332
+ }
333
+ }
334
+ return { loaded, order, failed };
335
+ }
336
+
337
+ /**
338
+ * 卸载模块:从受管集合移除并触发 disposer(停止实体、清 require 缓存)
339
+ * @param {string} name 模块名
340
+ * @returns {Promise<boolean>} 是否卸载了模块
341
+ * @throws {TypeError} name 不是非空字符串时抛出
342
+ */
343
+ async unload(name) {
344
+ if (typeof name !== 'string' || name.length === 0) {
345
+ throw new TypeError('模块名必须是非空字符串');
346
+ }
347
+ return this.del(name);
348
+ }
349
+
350
+ /**
351
+ * 静态体检:报出已加载模块中未带发现关键词与未声明合法层级的模块
352
+ * @returns {object} 含 untagged 与 ungrouped 的体检报告
353
+ */
354
+ doctor() {
355
+ const untagged = [];
356
+ const ungrouped = [];
357
+ for (const name of this.keys()) {
358
+ const manifest = this._readManifest(name);
359
+ if (manifest === null) continue;
360
+ const info = _checkSpec(manifest);
361
+ if (info.ok !== true) untagged.push(name);
362
+ if (info.group === UNKNOWN_GROUP) ungrouped.push(name);
363
+ }
364
+ return { untagged, ungrouped };
365
+ }
366
+
367
+ // ---------- 私有:装载 ----------
368
+ /**
369
+ * 装载单个模块:require 入口,导出若为生命周期实体则驱动到运行态,
370
+ * 随后登记进受管集合并挂 disposer(停止实体、清 require 缓存)
371
+ * @param {string} name 模块名
372
+ * @returns {Promise<boolean>} 本次新装载返回 true,已装载幂等跳过返回 false
373
+ * @throws {Error} 模块未安装、不可解析或实体启动失败时抛出
374
+ */
375
+ async _loadOne(name) {
376
+ if (this.has(name)) return false;
377
+ const spec = this._inspectLocal(name);
378
+ if (spec === null) throw new Error(`模块 ${name} 未安装或声明不可解析`);
379
+ const entry = this._locate(name);
380
+ if (entry === null) throw new Error(`模块 ${name} 入口无法解析`);
381
+ const exports = require(entry);
382
+ const entity = _toEntity(exports);
383
+ if (entity) await _startEntity(entity);
384
+ const dispose = async () => {
385
+ await _stopEntity(entity);
386
+ delete require.cache[entry];
387
+ };
388
+ this.set(name, { spec, exports, entity }, { dispose });
389
+ this.log('info', `模块已加载:${name}`);
390
+ return true;
391
+ }
392
+
393
+ /**
394
+ * 逆序卸载本次新加载的模块,卸载失败不掩盖原始错误
395
+ * @param {Array} loaded 本次新加载的模块名
396
+ * @returns {Promise<void>} 无返回值
397
+ */
398
+ async _rollback(loaded) {
399
+ for (let i = loaded.length - 1; i >= 0; i--) {
400
+ try {
401
+ await this.del(loaded[i]);
402
+ } catch {
403
+ // 回滚失败忽略
404
+ }
405
+ }
406
+ }
407
+
408
+ /**
409
+ * 按层级序稳定排序模块名,层内保持传入顺序
410
+ * @param {Array} names 模块名列表
411
+ * @returns {Array} 层级顺序下的模块名
412
+ */
413
+ _sortNamesByGroup(names) {
414
+ const decorated = names.map((name, index) => {
415
+ const manifest = this._readManifest(name);
416
+ const group = manifest === null ? UNKNOWN_GROUP : manifest.group;
417
+ return { name, index, order: _groupOrder(group) };
418
+ });
419
+ decorated.sort((a, b) => (a.order !== b.order ? a.order - b.order : a.index - b.index));
420
+ return decorated.map((item) => item.name);
421
+ }
422
+
423
+ /**
424
+ * 安装后自动加载,显式关闭时跳过,失败时抛带上下文的错误
425
+ * @param {string} name 模块名
426
+ * @param {object} options 安装选项
427
+ * @returns {Promise<void>} 无返回值
428
+ * @throws {Error} 自动加载失败时抛出
429
+ */
430
+ async _autoLoad(name, options) {
431
+ if (options.load === false) return;
432
+ try {
433
+ await this.load([name], {});
434
+ } catch (error) {
435
+ throw new Error(`模块 ${name} 已安装,但自动加载失败:${error.message}`, { cause: error });
436
+ }
437
+ }
438
+
439
+ // ---------- 私有:清单 ----------
440
+ /**
441
+ * 重建本地清单:读宿主依赖声明,逐包定位并校验后灌入 _local
442
+ * @returns {void}
443
+ */
444
+ _rebuildLocal() {
445
+ this._local.clear();
446
+ const host = this._readHost();
447
+ const prod = Object.keys(host.dependencies);
448
+ const dev = Object.keys(host.devDependencies);
449
+ const names = prod.concat(dev.filter((item) => prod.indexOf(item) < 0));
450
+ for (const name of names) {
451
+ const spec = this._inspectLocal(name);
452
+ if (spec !== null) this._local.add(spec);
453
+ }
454
+ }
455
+
456
+ /**
457
+ * 单个模块的本地清单项,未安装或声明不合规时为 null
458
+ * @param {string} name 模块名
459
+ * @returns {object|null} 模块清单项或 null
460
+ */
461
+ _inspectLocal(name) {
462
+ const manifest = this._readManifest(name);
463
+ if (manifest === null) return null;
464
+ const keywords = Array.isArray(manifest.keywords) ? manifest.keywords : [];
465
+ if (keywords.indexOf(this.config.keyword) < 0 && this.config.include_untagged !== true) {
466
+ return null;
467
+ }
468
+ return _readSpec(manifest, { installed: true, path: this._root(name), source: 'local' });
469
+ }
470
+
471
+ /**
472
+ * 读取宿主 package.json 的依赖声明
473
+ * @returns {object} 含 dependencies 与 devDependencies 的对象
474
+ */
475
+ _readHost() {
476
+ const host = this._readJson(path.join(this.config.cwd, 'package.json'));
477
+ if (host === null) return { dependencies: {}, devDependencies: {} };
478
+ const deps = host.dependencies;
479
+ const dev = host.devDependencies;
480
+ return {
481
+ dependencies: deps && typeof deps === 'object' ? deps : {},
482
+ devDependencies: dev && typeof dev === 'object' ? dev : {}
483
+ };
484
+ }
485
+
486
+ /**
487
+ * 读取 JSON 文件,读取或解析失败时返回 null
488
+ * @param {string} file 文件绝对路径
489
+ * @returns {object|null} 解析结果或 null
490
+ */
491
+ _readJson(file) {
492
+ try {
493
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
494
+ } catch {
495
+ return null;
496
+ }
497
+ }
498
+
499
+ /**
500
+ * 用 require.resolve 定位模块入口
501
+ * @param {string} name 模块名
502
+ * @returns {string|null} 入口文件路径或 null
503
+ */
504
+ _locate(name) {
505
+ try {
506
+ return require.resolve(name, { paths: [this.config.cwd] });
507
+ } catch {
508
+ return null;
509
+ }
510
+ }
511
+
512
+ /**
513
+ * 由入口向上查找最近的包根目录
514
+ * @param {string} name 模块名
515
+ * @returns {string|null} 包根目录或 null
516
+ */
517
+ _root(name) {
518
+ const entry = this._locate(name);
519
+ if (entry === null) return null;
520
+ const top = path.parse(entry).root;
521
+ let dir = path.dirname(entry);
522
+ while (true) {
523
+ if (fs.existsSync(path.join(dir, 'package.json'))) return dir;
524
+ if (dir === top) return null;
525
+ dir = path.dirname(dir);
526
+ }
527
+ }
528
+
529
+ /**
530
+ * 由入口向上查找包描述文件,包名不符视为无声明
531
+ * @param {string} name 模块名
532
+ * @returns {object|null} 包描述文件或 null
533
+ */
534
+ _readManifest(name) {
535
+ const root = this._root(name);
536
+ if (root === null) return null;
537
+ const manifest = this._readJson(path.join(root, 'package.json'));
538
+ if (manifest === null || manifest.name !== name) return null;
539
+ return manifest;
540
+ }
541
+
542
+ // ---------- 私有:命令与转换 ----------
543
+ /**
544
+ * 执行 npm 命令,透传实例配置的 cwd / registry / runner
545
+ * @param {Array} args 命令参数数组
546
+ * @returns {Promise<string>} 标准输出文本
547
+ */
548
+ async _execNpm(args) {
549
+ return _runNpm(args, {
550
+ cwd: this.config.cwd,
551
+ registry: this.config.registry,
552
+ runner: this.config.runner
553
+ });
554
+ }
555
+
556
+ /**
557
+ * 查询远端单模块详情,命令失败或不存在时返回 null
558
+ * @param {string} name 模块名
559
+ * @returns {Promise<object|null>} 模块清单项或 null
560
+ */
561
+ async _view(name) {
562
+ try {
563
+ const row = _parseObject(await this._execNpm(['view', name, '--json']));
564
+ return this._toSpec(row, false);
565
+ } catch {
566
+ return null;
567
+ }
568
+ }
569
+
570
+ /**
571
+ * 把 npm 记录转换为模块清单项
572
+ * @param {object} row npm 返回的单条记录
573
+ * @param {boolean} require_keyword 是否强制要求命中发现关键词
574
+ * @returns {object|null} 模块清单项,不合规时为 null
575
+ */
576
+ _toSpec(row, require_keyword) {
577
+ if (row === null || typeof row !== 'object') return null;
578
+ if (typeof row.name !== 'string' || row.name.length === 0) return null;
579
+ const keywords = Array.isArray(row.keywords) ? row.keywords : [];
580
+ if (require_keyword === true && keywords.indexOf(this.config.keyword) < 0) return null;
581
+ const links = row.links && typeof row.links === 'object' ? row.links : {};
582
+ const manifest = {
583
+ name: row.name,
584
+ version: row.version,
585
+ description: row.description,
586
+ group: row.group,
587
+ isolation: row.isolation,
588
+ keywords,
589
+ homepage: row.homepage || links.homepage,
590
+ repository: row.repository || links.repository
591
+ };
592
+ return _readSpec(manifest, { installed: false, path: null, source: 'remote' });
593
+ }
594
+
595
+ /**
596
+ * 组装检索词:关键词为空时仅用前缀,已带前缀时原样,否则前缀拼接
597
+ * @param {string} keyword 检索关键词
598
+ * @returns {string} 实际检索词
599
+ */
600
+ _buildQuery(keyword) {
601
+ const prefix = this.config.prefix;
602
+ if (keyword.length === 0) return prefix;
603
+ if (prefix.length > 0 && keyword.indexOf(prefix) === 0) return keyword;
604
+ return prefix + keyword;
605
+ }
606
+
607
+ /**
608
+ * 模块名是否命中配置前缀,前缀为空时恒为真
609
+ * @param {string} name 模块名
610
+ * @returns {boolean} 命中返回 true
611
+ */
612
+ _hasPrefix(name) {
613
+ const prefix = this.config.prefix;
614
+ if (prefix.length === 0) return true;
615
+ return typeof name === 'string' && name.indexOf(prefix) === 0;
616
+ }
617
+
618
+ /**
619
+ * 组装安装目标,带版本时拼成 name@version
620
+ * @param {string} name 模块名
621
+ * @param {object} options 安装选项
622
+ * @returns {string} 安装目标
623
+ */
624
+ _target(name, options) {
625
+ return typeof options.version === 'string' && options.version.length > 0
626
+ ? `${name}@${options.version}`
627
+ : name;
628
+ }
629
+
630
+ /**
631
+ * 校验远端模块是否可安装,不通过时抛错
632
+ * @param {object|null} remote 远端清单项
633
+ * @param {string} name 模块名
634
+ * @param {object} options 安装选项
635
+ * @returns {void}
636
+ * @throws {Error} 模块不存在或未声明关键词时抛出
637
+ */
638
+ _assertInstallable(remote, name, options) {
639
+ if (options.force === true) return;
640
+ if (remote === null) throw new Error(`模块 ${name} 不存在或无法访问`);
641
+ if (remote.keywords.indexOf(this.config.keyword) < 0) {
642
+ throw new Error(`模块 ${name} 未声明 ${this.config.keyword} 关键词,拒绝安装`);
643
+ }
644
+ }
645
+ }
646
+
647
+ module.exports = ModuleManager;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * npm 命令通道
3
+ * 统一执行 npm 子进程命令(Windows 经 cmd.exe 调起),支持 runner 注入以适配测试与定制场景;
4
+ * 并提供 npm search / view 的 JSON 输出解析,全部为无实例状态的纯过程
5
+ */
6
+
7
+ const { execFile } = require('child_process');
8
+
9
+ // npm 默认可执行文件
10
+ const DEFAULT_BIN = 'npm';
11
+
12
+ // 子进程超时毫秒
13
+ const DEFAULT_TIMEOUT = 120000;
14
+
15
+ // 子进程输出上限字节
16
+ const MAX_BUFFER = 10485760;
17
+
18
+ /**
19
+ * 组装 npm 执行计划,Windows 下经 cmd.exe 调起
20
+ * @param {Array} args 命令参数数组
21
+ * @returns {object} 含 file / args / verbatim 的执行计划
22
+ */
23
+ function _npmPlan(args) {
24
+ if (process.platform === 'win32') {
25
+ const head = ['/d', '/s', '/c', DEFAULT_BIN];
26
+ return { file: 'cmd.exe', args: head.concat(args), verbatim: true };
27
+ }
28
+ return { file: DEFAULT_BIN, args, verbatim: false };
29
+ }
30
+
31
+ /**
32
+ * 用 execFile 执行 npm 命令,退出码非 0 时抛错
33
+ * @param {Array} args 命令参数数组
34
+ * @param {object} options 执行选项,含 cwd
35
+ * @returns {Promise<string>} 标准输出文本
36
+ */
37
+ function _execNpmFile(args, options) {
38
+ const plan = _npmPlan(args);
39
+ const exec_options = {
40
+ cwd: typeof options.cwd === 'string' ? options.cwd : process.cwd(),
41
+ timeout: DEFAULT_TIMEOUT,
42
+ maxBuffer: MAX_BUFFER,
43
+ windowsVerbatimArguments: plan.verbatim,
44
+ windowsHide: true
45
+ };
46
+ return new Promise((resolve, reject) => {
47
+ execFile(plan.file, plan.args, exec_options, (error, stdout, stderr) => {
48
+ if (error) {
49
+ reject(new Error(`执行 npm 命令失败:${stderr || error.message}`));
50
+ return;
51
+ }
52
+ resolve(String(stdout));
53
+ });
54
+ });
55
+ }
56
+
57
+ /**
58
+ * npm 命令通道:runner 为函数时注入执行(测试与定制场景),否则走 execFile
59
+ * @param {Array} args 命令参数数组
60
+ * @param {object} options 执行选项,含 cwd / registry / runner
61
+ * @returns {Promise<string>} 标准输出文本
62
+ * @throws {TypeError} args 不是数组时抛出
63
+ */
64
+ async function _runNpm(args, options = {}) {
65
+ if (!Array.isArray(args)) {
66
+ throw new TypeError('命令参数必须是数组');
67
+ }
68
+ const full = args.slice();
69
+ if (typeof options.registry === 'string' && options.registry.length > 0) {
70
+ full.push(`--registry=${options.registry}`);
71
+ }
72
+ if (typeof options.runner === 'function') {
73
+ const result = (await options.runner(full, { cwd: options.cwd })) || {};
74
+ return typeof result.stdout === 'string' ? result.stdout : '';
75
+ }
76
+ return _execNpmFile(full, options);
77
+ }
78
+
79
+ /**
80
+ * 解析 npm search 的 JSON 输出
81
+ * @param {string} text 命令输出
82
+ * @returns {Array} 记录数组,无法解析时为空数组
83
+ */
84
+ function _parseRows(text) {
85
+ const raw = String(text);
86
+ const start = raw.indexOf('[');
87
+ const end = raw.lastIndexOf(']');
88
+ const body = start >= 0 && end > start ? raw.slice(start, end + 1) : raw;
89
+ try {
90
+ const rows = JSON.parse(body);
91
+ return Array.isArray(rows) ? rows : [];
92
+ } catch {
93
+ return [];
94
+ }
95
+ }
96
+
97
+ /**
98
+ * 解析 npm view 的 JSON 输出
99
+ * @param {string} text 命令输出
100
+ * @returns {object|null} 记录对象,无法解析时为 null
101
+ */
102
+ function _parseObject(text) {
103
+ const raw = String(text);
104
+ const start = raw.indexOf('{');
105
+ const end = raw.lastIndexOf('}');
106
+ if (start < 0 || end <= start) return null;
107
+ try {
108
+ return JSON.parse(raw.slice(start, end + 1));
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ module.exports = {
115
+ DEFAULT_BIN,
116
+ DEFAULT_TIMEOUT,
117
+ MAX_BUFFER,
118
+ _npmPlan,
119
+ _execNpmFile,
120
+ _runNpm,
121
+ _parseRows,
122
+ _parseObject
123
+ };
package/lib/spec.js ADDED
@@ -0,0 +1,231 @@
1
+ /**
2
+ * 模块清单规范
3
+ * 领域协议常量(发现关键词、模块前缀、合法层级)与包描述/清单项的纯函数:
4
+ * 归一化、合法性校验、条件过滤、层级分组、层级排序与版本号比较,不依赖任何实例状态
5
+ */
6
+
7
+ // 模块发现关键词:npm 检索命中依据,也是模块声明合法性的唯一判定
8
+ const KEYWORD = 'mm_mod';
9
+
10
+ // 模块名前缀默认值,可经 config.prefix 配置;检索只命中该前缀开头的模块
11
+ const DEFAULT_PREFIX = 'mm_';
12
+
13
+ // 合法层级,值域顺序即加载顺序
14
+ const GROUPS = [
15
+ 'tool',
16
+ 'infra',
17
+ 'in_port',
18
+ 'in_adapter',
19
+ 'channel',
20
+ 'service',
21
+ 'domain',
22
+ 'database',
23
+ 'out_adapter',
24
+ 'out_port'
25
+ ];
26
+
27
+ // 未声明或声明非法层级时归入的层级,加载顺序排在全部合法层级之后
28
+ const UNKNOWN_GROUP = 'unknown';
29
+
30
+ /**
31
+ * 把任意值归一化为数组,非数组一律补空数组
32
+ * @param {any} value 待归一化的值
33
+ * @returns {Array} 归一化后的数组
34
+ */
35
+ function _toArray(value) {
36
+ return Array.isArray(value) ? value : [];
37
+ }
38
+
39
+ /**
40
+ * 把任意值归一化为字符串,空串与非字符串一律补 null
41
+ * @param {any} value 待归一化的值
42
+ * @returns {string|null} 字符串或 null
43
+ */
44
+ function _toText(value) {
45
+ return typeof value === 'string' && value.length > 0 ? value : null;
46
+ }
47
+
48
+ /**
49
+ * 从 repository 字段提取仓库地址
50
+ * @param {string|object} repo repository 字段值
51
+ * @returns {string|null} 仓库地址或 null
52
+ */
53
+ function _repoUrl(repo) {
54
+ if (typeof repo === 'string') return repo;
55
+ if (repo && typeof repo.url === 'string') return repo.url;
56
+ return null;
57
+ }
58
+
59
+ /**
60
+ * 判定模块层级,未声明或声明非法时归入 unknown
61
+ * @param {object} manifest 包描述文件
62
+ * @returns {string} 层级名
63
+ */
64
+ function _groupOf(manifest) {
65
+ return GROUPS.indexOf(manifest.group) >= 0 ? manifest.group : UNKNOWN_GROUP;
66
+ }
67
+
68
+ /**
69
+ * 解出层级的加载序号,未声明或声明非法时排在最后
70
+ * @param {string} group 层级名
71
+ * @returns {number} 层级序号,从 0 起
72
+ */
73
+ function _groupOrder(group) {
74
+ const idx = GROUPS.indexOf(group);
75
+ return idx < 0 ? GROUPS.length : idx;
76
+ }
77
+
78
+ /**
79
+ * 文本是否命中模块的名称、描述或关键词
80
+ * @param {object} spec 模块清单项
81
+ * @param {string} text 检索文本
82
+ * @returns {boolean} 命中返回 true
83
+ */
84
+ function _matchText(spec, text) {
85
+ const hay = [_toText(spec.name), _toText(spec.description)]
86
+ .concat(_toArray(spec.keywords))
87
+ .filter((item) => typeof item === 'string')
88
+ .join(' ')
89
+ .toLowerCase();
90
+ return hay.indexOf(String(text).toLowerCase()) >= 0;
91
+ }
92
+
93
+ /**
94
+ * 把包描述文件归一化为模块清单项
95
+ * @param {object} manifest 包描述文件
96
+ * @param {object} extra 覆盖字段,支持 installed / path / source
97
+ * @returns {object} 模块清单项
98
+ * @throws {TypeError} manifest 不是对象时抛出
99
+ */
100
+ function _readSpec(manifest, extra = {}) {
101
+ if (manifest === null || typeof manifest !== 'object') {
102
+ throw new TypeError('manifest 必须是对象');
103
+ }
104
+ return {
105
+ name: _toText(manifest.name),
106
+ version: _toText(manifest.version),
107
+ description: _toText(manifest.description),
108
+ group: _groupOf(manifest),
109
+ isolation: _toText(manifest.isolation),
110
+ keywords: _toArray(manifest.keywords),
111
+ homepage: _toText(manifest.homepage) || _toText(_repoUrl(manifest.repository)),
112
+ installed: extra.installed === true,
113
+ path: _toText(extra.path),
114
+ source: extra.source === 'remote' ? 'remote' : 'local'
115
+ };
116
+ }
117
+
118
+ /**
119
+ * 校验模块是否按要求声明了发现关键词
120
+ * @param {object} manifest 包描述文件
121
+ * @returns {object} 校验结果,含 ok / missing / group
122
+ * @throws {TypeError} manifest 不是对象时抛出
123
+ */
124
+ function _checkSpec(manifest) {
125
+ if (manifest === null || typeof manifest !== 'object') {
126
+ throw new TypeError('manifest 必须是对象');
127
+ }
128
+ const keywords = _toArray(manifest.keywords);
129
+ const ok = keywords.indexOf(KEYWORD) >= 0;
130
+ return {
131
+ ok,
132
+ missing: ok ? [] : [KEYWORD],
133
+ group: _groupOf(manifest)
134
+ };
135
+ }
136
+
137
+ /**
138
+ * 按条件过滤模块清单项,条件为 undefined 时不参与
139
+ * @param {object} spec 模块清单项
140
+ * @param {object} filter 过滤条件,支持 group / installed / isolation / keyword / text
141
+ * @returns {boolean} 命中返回 true
142
+ * @throws {TypeError} spec 或 filter 类型错误时抛出
143
+ */
144
+ function _matchSpec(spec, filter = {}) {
145
+ if (spec === null || typeof spec !== 'object') {
146
+ throw new TypeError('spec 必须是对象');
147
+ }
148
+ if (filter === null || typeof filter !== 'object') {
149
+ throw new TypeError('filter 必须是对象');
150
+ }
151
+ if (filter.group !== undefined && spec.group !== filter.group) return false;
152
+ if (filter.installed !== undefined && spec.installed !== filter.installed) return false;
153
+ if (filter.isolation !== undefined && spec.isolation !== filter.isolation) return false;
154
+ if (filter.keyword !== undefined && _toArray(spec.keywords).indexOf(filter.keyword) < 0) {
155
+ return false;
156
+ }
157
+ if (filter.text !== undefined && !_matchText(spec, filter.text)) return false;
158
+ return true;
159
+ }
160
+
161
+ /**
162
+ * 按层级分组模块清单项
163
+ * @param {Array} specs 模块清单项数组
164
+ * @returns {object} 层级到清单项数组的映射
165
+ * @throws {TypeError} specs 不是数组时抛出
166
+ */
167
+ function _groupSpecs(specs) {
168
+ if (!Array.isArray(specs)) {
169
+ throw new TypeError('specs 必须是数组');
170
+ }
171
+ const groups = {};
172
+ for (const key of GROUPS) groups[key] = [];
173
+ groups[UNKNOWN_GROUP] = [];
174
+ for (const spec of specs) {
175
+ if (spec === null || typeof spec !== 'object') continue;
176
+ const key = GROUPS.indexOf(spec.group) >= 0 ? spec.group : UNKNOWN_GROUP;
177
+ groups[key].push(spec);
178
+ }
179
+ return groups;
180
+ }
181
+
182
+ /**
183
+ * 按层级序与模块名排序
184
+ * @param {Array} specs 模块清单项数组
185
+ * @returns {Array} 排序后的新数组
186
+ */
187
+ function _sortedSpecs(specs) {
188
+ return specs.slice().sort((a, b) => {
189
+ const a_order = _groupOrder(a.group);
190
+ const b_order = _groupOrder(b.group);
191
+ if (a_order !== b_order) return a_order - b_order;
192
+ return String(a.name).localeCompare(String(b.name));
193
+ });
194
+ }
195
+
196
+ /**
197
+ * 版本号逐段数值比较,candidate 大于 current 时返回 true
198
+ * @param {string} candidate 候选版本号
199
+ * @param {string} current 当前版本号
200
+ * @returns {boolean} 候选是否更新
201
+ */
202
+ function _isVersionNewer(candidate, current) {
203
+ const a = String(candidate || '').split('.');
204
+ const b = String(current || '').split('.');
205
+ const len = Math.max(a.length, b.length);
206
+ for (let i = 0; i < len; i++) {
207
+ const a_num = parseInt(a[i], 10) || 0;
208
+ const b_num = parseInt(b[i], 10) || 0;
209
+ if (a_num !== b_num) return a_num > b_num;
210
+ }
211
+ return false;
212
+ }
213
+
214
+ module.exports = {
215
+ KEYWORD,
216
+ DEFAULT_PREFIX,
217
+ GROUPS,
218
+ UNKNOWN_GROUP,
219
+ _toArray,
220
+ _toText,
221
+ _repoUrl,
222
+ _groupOf,
223
+ _groupOrder,
224
+ _matchText,
225
+ _readSpec,
226
+ _checkSpec,
227
+ _matchSpec,
228
+ _groupSpecs,
229
+ _sortedSpecs,
230
+ _isVersionNewer
231
+ };
package/package.json CHANGED
@@ -1,53 +1,58 @@
1
1
  {
2
- "name": "mm_module",
3
- "version": "1.0.2",
4
- "description": "内核模块管理框架件:基于 npm 实现模块的搜索、安装、卸载与清单编排,并按声明的层级分层装载与声明体检(不注册能力)",
5
- "main": "./index.js",
6
- "exports": {
7
- ".": "./index.js",
8
- "./package.json": "./package.json"
9
- },
10
- "files": [
11
- "index.js",
12
- "src/"
13
- ],
14
- "scripts": {
15
- "test": "jest --passWithNoTests --maxWorkers=1 2>&1",
16
- "lint": "eslint src/ test/ index.js --fix",
17
- "check": "eslint src/ test/ index.js",
18
- "format": "prettier --write \"src/**/*.js\" \"test/**/*.js\" \"*.js\" \"*.json\"",
19
- "demo": "node example/demo.js"
20
- },
21
- "keywords": [
22
- "mm_mod",
23
- "mm_kernel",
24
- "module",
25
- "npm",
26
- "install",
27
- "uninstall",
28
- "manager",
29
- "catalog"
30
- ],
31
- "engines": {
32
- "node": ">=18"
33
- },
34
- "dependencies": {
35
- "mm_corejs": "file:../mm_corejs"
36
- },
37
- "devDependencies": {
38
- "@eslint/js": "^10.0.1",
39
- "eslint": "^10.9.1",
40
- "jest": "^30.4.2",
41
- "prettier": "^3.9.8"
42
- },
43
- "repository": {
44
- "type": "git",
45
- "url": "git+https://github.com/mm-modules/mm_module.git"
46
- },
47
- "homepage": "https://github.com/mm-modules/mm_module#readme",
48
- "bugs": {
49
- "url": "https://github.com/mm-modules/mm_module/issues"
50
- },
51
- "author": "qww",
52
- "license": "MIT"
2
+ "name": "mm_module",
3
+ "version": "1.0.4",
4
+ "description": "内核模块管理框架件:基于 npm 实现模块的搜索、安装、卸载与清单编排,并按声明的层级分层装载与声明体检(不注册能力)",
5
+ "main": "./index.js",
6
+ "exports": {
7
+ ".": "./index.js",
8
+ "./package.json": "./package.json"
9
+ },
10
+ "files": [
11
+ "index.js",
12
+ "lib/"
13
+ ],
14
+ "scripts": {
15
+ "test": "jest --passWithNoTests --maxWorkers=1 2>&1",
16
+ "lint": "eslint lib/ test/ index.js --fix",
17
+ "check": "eslint lib/ test/ index.js",
18
+ "format": "prettier --write \"lib/**/*.js\" \"test/**/*.js\" \"*.js\" \"*.json\"",
19
+ "demo": "node example/demo.js"
20
+ },
21
+ "keywords": [
22
+ "mm_mod",
23
+ "mm_infra",
24
+ "mm_runtime",
25
+ "mm_kernel",
26
+ "module",
27
+ "npm",
28
+ "install",
29
+ "uninstall",
30
+ "manager",
31
+ "catalog"
32
+ ],
33
+ "layer": "crosscut",
34
+ "group": "runtime",
35
+ "engines": {
36
+ "node": ">=18"
37
+ },
38
+ "dependencies": {
39
+ "mm_corejs": "^1.1.4",
40
+ "mm_mod": "^1.0.1"
41
+ },
42
+ "devDependencies": {
43
+ "@eslint/js": "^10.0.1",
44
+ "eslint": "^10.9.1",
45
+ "jest": "^30.4.2",
46
+ "prettier": "^3.9.8"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "git+https://gitee.com/qiuwenwu91/mm_module.git"
51
+ },
52
+ "homepage": "https://github.com/mm-modules/mm_module#readme",
53
+ "bugs": {
54
+ "url": "https://github.com/mm-modules/mm_module/issues"
55
+ },
56
+ "author": "qww",
57
+ "license": "MIT"
53
58
  }