@serveryyswys/opm 0.1.2 → 0.1.3

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 CHANGED
@@ -78,17 +78,67 @@ opm help
78
78
 
79
79
  ### 哪些文件会进包
80
80
 
81
+ 规则照 OPM,**只有三类**:
82
+
81
83
  | 类别 | 处理 |
82
84
  | --- | --- |
83
85
  | `manifest.json` | 原样 |
84
- | **JS**(入口 / 子包 / UI / 依赖) | 顺 `require` 链裁剪 + 裸名重写;**不可达的 JS 是死代码,不带** |
85
- | `resources/` `i18n/` `modules/` | **无条件整目录保留**,即使里面是 `.js` |
86
- | 其它非 JS 文件 | 原样带上(运行期数据,静态追不出来) |
87
- | 工程元数据(`package.json`/`tsconfig.json`/`.npmrc`…)与打包产物(`*.tgz`/`*.zip`) | 不带 |
88
-
89
- > 为什么要专门列这条:这三个规范目录**不在 require 链里**。只按 require 收集会把插件的
90
- > logo、多语言文件、WASM 模块**静默丢掉**且不报错。`i18n/*.js` 尤其容易被"不可达 JS 裁剪"
91
- > 误伤 —— 它是被 i18n 机制加载的**数据**,不是 require 出来的模块。
86
+ | `main` / `subpackages[].entry` / UI 模块 | 读入、重写裸名、顺 require 链递归收集 |
87
+ | `manifest.extra_files` 里列出的路径 | 文件直接收;目录**递归**收(跳过 `node_modules` 与点目录,深度上限 10) |
88
+
89
+ **不在 require 闭包里、也没写进 `extra_files` 的文件一律不进包。** 这包括工程里的
90
+ 杂项(草稿、截图、临时配置)—— 归档内容因此是可解释的:闭包 + 入口 + 显式声明。
91
+
92
+ > 为什么改:早先这里是"非 JS 一律原样带上",会把杂项一起打进包,包体不可控
93
+ > 也无法解释。对齐 OPM 的 `extra_files` 之后,"打了什么"完全由声明决定。
94
+
95
+ ### 构建清单 manifest.opmbuild.json(可选)
96
+
97
+ 项目根放一个 `manifest.opmbuild.json`,把**构建期**的决策与运行时 manifest 分开:
98
+
99
+ ```json
100
+ {
101
+ "extra_files": ["resources", "i18n"],
102
+ "packinfo": { "enable": true }
103
+ }
104
+ ```
105
+
106
+ - `extra_files`:额外进包的路径,文件直接收,目录**递归**收(跳过 `node_modules` 与点目录);
107
+ - `packinfo`:是否生成 `PACKINFO`(见下);
108
+ - **优先级高于** `manifest.json` 里的同名字段;文件不存在就用 manifest 里的,都没有就不打额外文件;
109
+ - 不是合法 JSON 会**明确报错**,不会静默忽略。
110
+
111
+ > 为什么单独一个文件:`manifest.json` 是装上设备后给 Operit 读的**运行时**元数据,
112
+ > 而"打哪些文件、要不要 PACKINFO"是**构建期**的事。混在一起会让运行时清单变脏。
113
+
114
+ ### PACKINFO(可选)
115
+
116
+ 在 manifest 里写:
117
+
118
+ ```json
119
+ "packinfo": { "enable": true }
120
+ ```
121
+
122
+ 就会在归档根生成一个无扩展名的 `PACKINFO`(YAML),供市场审核核对:
123
+
124
+ ```yaml
125
+ name: 打包信息
126
+ description: 一句话说明
127
+ id: top.serveryyswys.yourpkg
128
+ version: 1.0.0
129
+ author: "甲, 乙"
130
+ license: Apache-2.0
131
+ dependencies:
132
+ - "@serveryyswys/node.operit"
133
+ - ms
134
+ ```
135
+
136
+ - 字段顺序固定 `name → description → id → version → author → license → dependencies`,
137
+ 有才写、缺则跳过;
138
+ - `dependencies` 从**归档里实际收进的** `node_modules/<pkg>/...` 反推,
139
+ 所以因内建重写带进来的 `node.operit` 也会如实列出;
140
+ - `enable` 不为 `true` 时不生成该文件;
141
+ - 含 `@` / `,` / `:` 等字符的值会按 YAML 规则加双引号。
92
142
 
93
143
  ### 裸名重写的三条规则
94
144
 
package/dist/cli.js CHANGED
@@ -34,7 +34,7 @@ function usage() {
34
34
  '用法:',
35
35
  ' opm init [项目目录] [--no-install] [--overwrite]',
36
36
  ' 把工程准备到能 build:补 scripts.build 与依赖声明、生成 manifest.json 骨架、',
37
- ' TS 工程补 tsconfig.json。默认跑一次 npm install(不指定 registry,走你自己的 npm 源)。',
37
+ ' TS 工程补 tsconfig.json;默认装依赖。',
38
38
  '',
39
39
  ' opm build [项目目录] [--out <名字>] [--dry-run] [--no-install]',
40
40
  ' 计算可达性、重写裸名、拦截不支持的依赖,压成 .toolpkg',
@@ -75,6 +75,15 @@ function parseArgs(argv) {
75
75
  }
76
76
  return { positional, flags };
77
77
  }
78
+ function formatBytes(bytes) {
79
+ if (bytes < 1024) {
80
+ return bytes + ' B';
81
+ }
82
+ if (bytes < 1024 * 1024) {
83
+ return (bytes / 1024).toFixed(1) + ' KB';
84
+ }
85
+ return (bytes / 1024 / 1024).toFixed(1) + ' MB';
86
+ }
78
87
  function main(argv) {
79
88
  const command = argv[0];
80
89
  const { positional, flags } = parseArgs(argv.slice(1));
@@ -102,8 +111,10 @@ function main(argv) {
102
111
  for (const item of result.unchanged) {
103
112
  console.log(' = ' + item + '(已存在,未改动)');
104
113
  }
105
- for (const note of result.notes) {
106
- console.log(' · ' + note);
114
+ if (flags.has('verbose')) {
115
+ for (const note of result.notes) {
116
+ console.log(' · ' + note);
117
+ }
107
118
  }
108
119
  if (!result.ok) {
109
120
  console.error('');
@@ -113,10 +124,11 @@ function main(argv) {
113
124
  }
114
125
  return 1;
115
126
  }
116
- console.log('');
117
- console.log('下一步:');
118
- for (const step of result.nextSteps) {
119
- console.log(' ' + step);
127
+ if (result.nextSteps.length > 0) {
128
+ console.log('');
129
+ for (const step of result.nextSteps) {
130
+ console.log(' ' + step);
131
+ }
120
132
  }
121
133
  return 0;
122
134
  }
@@ -128,60 +140,51 @@ function main(argv) {
128
140
  dryRun: flags.has('dry-run'),
129
141
  install: !flags.has('no-install')
130
142
  });
131
- for (const note of result.notes) {
132
- console.log(' · ' + note);
143
+ if (!result.ok) {
144
+ for (const error of result.errors) {
145
+ console.error('✗ ' + error);
146
+ }
147
+ return 1;
133
148
  }
134
- if (result.diagnostics.length > 0) {
149
+ if (flags.has('verbose')) {
150
+ for (const note of result.notes) {
151
+ console.log('· ' + note);
152
+ }
135
153
  const byKind = new Map();
136
154
  for (const item of result.diagnostics) {
137
155
  byKind.set(item.kind, (byKind.get(item.kind) ?? 0) + 1);
138
156
  }
139
- const summary = Array.from(byKind.entries()).map(([kind, count]) => kind + ' ' + count).join(' / ');
140
- console.log(' 重写 ' + result.diagnostics.length + ' 处(' + summary + ')');
141
- }
142
- if (!result.ok) {
143
- console.error('');
144
- console.error('构建失败(' + result.errors.length + ' 个问题):');
145
- for (const error of result.errors) {
146
- console.error(' ✗ ' + error);
147
- }
148
- return 1;
157
+ const summary = Array.from(byKind.entries()).map(([kind, count]) => kind + ' ' + count).join(', ');
158
+ console.log('· 重写 ' + result.diagnostics.length + ' 处(' + summary + ')');
149
159
  }
150
- console.log('');
151
- console.log('✓ 已打包 ' + result.fileCount + ' 个文件');
152
- console.log(' ' + result.toolpkgPath);
160
+ console.log('✓ ' + result.toolpkgPath + ' (' + result.fileCount + ' 个文件, ' + formatBytes(result.totalBytes) + ')');
153
161
  return 0;
154
162
  }
155
163
  if (command === 'verify') {
156
164
  const projectDir = (0, node_path_1.resolve)(positional[0] === undefined ? process.cwd() : positional[0]);
157
165
  const report = (0, verify_1.verifyProject)({ projectDir, install: !flags.has('no-install') });
158
- console.log('检查项:');
159
- for (const check of report.checks) {
160
- console.log(' ' + (check.ok ? '✓' : '✗') + ' ' + check.name + ' —— ' + check.detail);
161
- }
162
- const stats = report.stats;
163
- console.log('');
164
- console.log('统计:');
165
- console.log(' 归档文件 ' + stats.archiveFiles + ' 个(其中资产 ' + stats.assets + ' 个),共 ' + stats.totalBytes + ' 字节');
166
- console.log(' 重写 内建 ' + stats.builtin + ' / npm 包 ' + stats.package + ' / 相对 ' + stats.relative + ' / 保持原样 ' + stats.untouched);
167
- console.log(' 引入的包 ' + (stats.packages.length === 0 ? '(无)' : stats.packages.join(', ')));
168
- console.log(' node.operit ' + stats.nodeOperitVersion);
169
- for (const note of report.notes) {
170
- console.log(' · ' + note);
166
+ if (flags.has('verbose')) {
167
+ for (const check of report.checks) {
168
+ console.log((check.ok ? '✓ ' : '✗ ') + check.name + ' —— ' + check.detail);
169
+ }
170
+ const stats = report.stats;
171
+ console.log('· 归档 ' + stats.archiveFiles + ' 个文件, ' + formatBytes(stats.totalBytes)
172
+ + ';额外文件 ' + stats.assets + ';依赖 ' + (stats.packages.length === 0 ? '无' : stats.packages.join(', '))
173
+ + ';node.operit ' + stats.nodeOperitVersion);
174
+ for (const note of report.notes) {
175
+ console.log('· ' + note);
176
+ }
171
177
  }
172
178
  for (const warning of report.warnings) {
173
- console.log(' ! ' + warning);
179
+ console.log('! ' + warning);
174
180
  }
175
181
  if (!report.ok) {
176
- console.error('');
177
- console.error('校验未通过(' + report.errors.length + ' 个问题):');
178
182
  for (const error of report.errors) {
179
- console.error(' ✗ ' + error);
183
+ console.error('✗ ' + error);
180
184
  }
181
185
  return 1;
182
186
  }
183
- console.log('');
184
- console.log('✓ 校验通过(未写任何文件)');
187
+ console.log('✓ 校验通过');
185
188
  return 0;
186
189
  }
187
190
  if (command === 'version') {
package/dist/lib/build.js CHANGED
@@ -12,6 +12,7 @@ exports.buildProject = buildProject;
12
12
  * 5. 入口注入 process / Buffer 全局(沙盒没有这两个全局);
13
13
  * 6. 压成 zip,产物放项目根的上一级,名为 <项目目录名>.toolpkg。
14
14
  */
15
+ const packinfo_1 = require("./packinfo");
15
16
  const node_fs_1 = require("node:fs");
16
17
  const node_path_1 = require("node:path");
17
18
  const zip_1 = require("./zip");
@@ -76,7 +77,7 @@ function buildProject(options) {
76
77
  ' 执行 npm install 补齐后重试(本次带了 --no-install,所以没有自动装)。');
77
78
  return { ok: false, toolpkgPath: '', fileCount: 0, files: [], totalBytes: 0, assets: [], diagnostics, errors, notes };
78
79
  }
79
- notes.push('检测到缺少依赖:' + audit.missing.join(', ') + ',正在执行 npm install');
80
+ notes.push('缺少依赖,正在安装:' + audit.missing.join(', '));
80
81
  const installed = (0, dependencies_1.installDependencies)(projectDir);
81
82
  if (!installed.ok) {
82
83
  errors.push('npm install 失败:\n' + installed.output.trim());
@@ -119,6 +120,7 @@ function buildProject(options) {
119
120
  }
120
121
  const locate = (0, typescript_1.createLocator)(projectDir, plan.outDir);
121
122
  const env = { locate, contract, nodeOperitArchiveDir: contract_1.NODE_OPERIT_DIR };
123
+ const buildConfig = readBuildConfig(projectDir, errors);
122
124
  const seeds = collectSeeds(manifest, errors);
123
125
  // 入口要注入 process / Buffer 全局(沙盒没有这两个全局)。
124
126
  // 关键是:**注入必须在可达性遍历之前登记**,把被注入引用的这两个文件
@@ -174,18 +176,16 @@ function buildProject(options) {
174
176
  rewritten.set(entryPath, prelude + rewritten.get(entryPath));
175
177
  notes.push('已向入口 ' + entryPath + ' 注入 process / Buffer 全局');
176
178
  }
177
- // 非 JS 资产原样带上(resources / i18n / modules 等)
178
- const reachableDiskPaths = new Set();
179
- for (const archivePath of rewritten.keys()) {
180
- const disk = locate(archivePath);
181
- if (disk !== null) {
182
- reachableDiskPaths.add(disk);
183
- }
184
- }
185
- const collected = collectAssets(projectDir, plan.outDir, reachableDiskPaths);
179
+ // 额外文件:只收 manifest.extra_files 里显式声明的(照 OPM 的语义)
180
+ const archivePathSet = new Set(rewritten.keys());
181
+ archivePathSet.add('manifest.json');
182
+ const configExtra = Array.isArray(buildConfig.extra_files) ? buildConfig.extra_files : null;
183
+ const manifestExtra = Array.isArray(manifest.extra_files) ? manifest.extra_files : [];
184
+ const extraFiles = configExtra !== null ? configExtra : manifestExtra;
185
+ const collected = collectExtraFiles(projectDir, extraFiles, archivePathSet);
186
186
  errors.push(...collected.errors);
187
187
  if (collected.assets.length > 0) {
188
- notes.push('原样带上 ' + collected.assets.length + ' 个非代码资产(resources / i18n / modules 等)');
188
+ notes.push('按 extra_files 带上 ' + collected.assets.length + ' 个额外文件');
189
189
  }
190
190
  const outName = options.outName === undefined ? baseName(projectDir) : options.outName;
191
191
  const toolpkgPath = (0, node_path_1.join)((0, node_path_1.dirname)(projectDir), outName + '.toolpkg');
@@ -210,6 +210,17 @@ function buildProject(options) {
210
210
  for (const entry of archiveEntries) {
211
211
  totalBytes += entry.content.length;
212
212
  }
213
+ // PACKINFO:仅在 manifest.packinfo.enable === true 时生成
214
+ const configPackinfo = buildConfig.packinfo;
215
+ const packinfoOptions = (configPackinfo !== undefined && configPackinfo !== null
216
+ ? configPackinfo
217
+ : manifest.packinfo);
218
+ const packinfoText = (0, packinfo_1.renderPackInfo)(manifest, packinfoOptions, archiveEntries.map((entry) => entry.path));
219
+ if (packinfoText !== null) {
220
+ entries.push({ path: 'PACKINFO', content: Buffer.from(packinfoText, 'utf8') });
221
+ archiveEntries.push({ path: 'PACKINFO', content: Buffer.from(packinfoText, 'utf8') });
222
+ notes.push('已生成 PACKINFO(' + packinfoText.split('\n').length + ' 行)');
223
+ }
213
224
  const archive = (0, zip_1.createZip)(entries);
214
225
  (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(toolpkgPath), { recursive: true });
215
226
  (0, node_fs_1.writeFileSync)(toolpkgPath, archive);
@@ -226,78 +237,82 @@ function buildProject(options) {
226
237
  };
227
238
  }
228
239
  /**
229
- * 非 JS 资产的收集。
240
+ * 额外文件收集 —— 照 OPM 的 extra_files 语义。
230
241
  *
231
- * 规则:**JS 按可达性裁剪,非 JS 一律原样带上。**
242
+ * OPM(src/build/extrafiles.ts)只收 manifest 显式列出的路径:
243
+ * - 是文件 → 收;
244
+ * - 是目录 → **递归**收集其下文件(跳过 node_modules 与点目录,深度上限 10);
245
+ * - 不在列表里的一律不进归档。
232
246
  *
233
- * 为什么:resources / i18n / modules(WASM)这类文件是运行期数据,
234
- * 静态追 require 是追不到的 —— 早先只按 require 链收集,会把插件的 logo、
235
- * 多语言文件、WASM 模块**静默丢掉**,而且不报错。官方打包脚本本来就是整包 zip,
236
- * 使用者按"东西都在包里"预期;这里只裁掉死代码,不再动数据文件。
237
- */
238
- const ASSET_SKIP_DIRS = new Set(['node_modules', '.git']);
239
- const ASSET_SKIP_FILES = new Set([
240
- // manifest 由构建流程显式加入,不能再被资产收集加一遍(否则 zip 里出现重复条目)
241
- 'manifest.json', 'manifest.hjson',
242
- 'package.json', 'package-lock.json', 'npm-shrinkwrap.json',
243
- 'tsconfig.json', '.npmrc', '.gitignore', '.gitattributes'
244
- ]);
245
- const ARCHIVE_LIKE = /\.(tgz|zip|tar|gz)$/i;
246
- const SCRIPT_LIKE = /\.(js|cjs|mjs|ts|jsx|tsx|map)$/i;
247
- /**
248
- * 这三个目录**无条件整目录保留**,即使里面的文件是 .js。
249
- * 因为 i18n/*.js 是被 i18n 机制加载的**数据**,不是 require 出来的模块 ——
250
- * 按"JS 一律按可达性裁剪"会把多语言文件一起丢掉,正是这次要修的坑。
247
+ * 之前这里写的是"非 JS 一律原样带上",那是错的:它会把工程里的杂项
248
+ * (草稿、截图、临时配置)一起打进包,包体不可控也不可解释。
249
+ * 现在归档内容 = require 闭包 + manifest 声明的入口 + 显式 extra_files,没有别的。
251
250
  */
252
- const ALWAYS_INCLUDE_DIRS = new Set(['resources', 'i18n', 'modules']);
253
- function collectAssets(projectDir, outDir, reachableDiskPaths) {
251
+ const EXTRA_SKIP_DIRS = new Set(['node_modules']);
252
+ const EXTRA_MAX_DEPTH = 10;
253
+ function collectExtraFiles(projectDir, extraFiles, archivePaths) {
254
254
  const assets = [];
255
255
  const errors = [];
256
- const outDirPrefix = outDir === null ? null : outDir.replace(/[\\/]+$/, '') + node_path_1.sep;
257
- const walk = (dir, prefix, insideAlways) => {
256
+ const seen = new Set();
257
+ const addFile = (archivePath) => {
258
+ const normalized = toPosix(archivePath).replace(/^\.\//, '');
259
+ if (normalized === '' || seen.has(normalized) || archivePaths.has(normalized)) {
260
+ return; // 已在 require 闭包里,不重复
261
+ }
262
+ const disk = (0, node_path_1.resolve)(projectDir, normalized);
263
+ if (!(0, node_fs_1.existsSync)(disk) || !(0, node_fs_1.statSync)(disk).isFile()) {
264
+ return;
265
+ }
266
+ seen.add(normalized);
267
+ assets.push({ path: normalized, disk });
268
+ };
269
+ const walkDir = (relDir, depth) => {
270
+ if (depth <= 0) {
271
+ return;
272
+ }
273
+ const absDir = (0, node_path_1.resolve)(projectDir, relDir);
258
274
  let entries;
259
275
  try {
260
- entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
276
+ entries = (0, node_fs_1.readdirSync)(absDir, { withFileTypes: true });
261
277
  }
262
278
  catch (error) {
263
- errors.push('读取目录失败:' + dir + ' —— ' + String(error));
279
+ errors.push('读取目录失败:' + relDir + ' —— ' + String(error));
264
280
  return;
265
281
  }
266
282
  for (const entry of entries) {
267
- const archivePath = prefix === '' ? entry.name : prefix + '/' + entry.name;
268
- const disk = (0, node_path_1.join)(dir, entry.name);
269
- const nextInsideAlways = insideAlways || (prefix === '' && ALWAYS_INCLUDE_DIRS.has(entry.name));
283
+ if (entry.name.startsWith('.')) {
284
+ continue; // 点目录/点文件不进包
285
+ }
286
+ const childRel = relDir === '' ? entry.name : relDir + '/' + entry.name;
270
287
  if (entry.isDirectory()) {
271
- if (ASSET_SKIP_DIRS.has(entry.name)) {
288
+ if (EXTRA_SKIP_DIRS.has(entry.name)) {
272
289
  continue;
273
290
  }
274
- if (outDirPrefix !== null && (disk + node_path_1.sep).startsWith(outDirPrefix)) {
275
- continue; // 编译产物目录:它的 JS 由可达性接管
276
- }
277
- walk(disk, archivePath, nextInsideAlways);
278
- continue;
279
- }
280
- if (prefix === '' && ASSET_SKIP_FILES.has(entry.name)) {
281
- continue;
282
- }
283
- if (!nextInsideAlways && ARCHIVE_LIKE.test(entry.name)) {
284
- continue; // 打包产物不该再进包 —— 但 resources/ 里的 zip 是合法资源,规范明确支持
285
- }
286
- if (reachableDiskPaths.has(disk)) {
287
- continue; // 已经是可达 JS,不重复
288
- }
289
- // 规范目录之外:脚本类文件交给可达性裁剪(不可达的 JS 是死代码),
290
- // 其它一律当数据带上。
291
- if (!nextInsideAlways && SCRIPT_LIKE.test(entry.name)) {
291
+ walkDir(childRel, depth - 1);
292
292
  continue;
293
293
  }
294
- assets.push({ path: archivePath, disk });
294
+ addFile(childRel);
295
295
  }
296
296
  };
297
- walk(projectDir, '', false);
297
+ for (const raw of extraFiles) {
298
+ const rel = toPosix(String(raw)).replace(/^\.\//, '');
299
+ if (rel === '') {
300
+ continue;
301
+ }
302
+ const abs = (0, node_path_1.resolve)(projectDir, rel);
303
+ if (!(0, node_fs_1.existsSync)(abs)) {
304
+ errors.push('extra_files 里声明的路径不存在:' + rel);
305
+ continue;
306
+ }
307
+ if ((0, node_fs_1.statSync)(abs).isDirectory()) {
308
+ walkDir(rel, EXTRA_MAX_DEPTH);
309
+ continue;
310
+ }
311
+ addFile(rel);
312
+ }
298
313
  return { assets, errors };
299
314
  }
300
- /** 归档正文的总字节(未压缩):重写后的 JS + 原样资产。dry-run 也要能报出来。 */
315
+ /** 归档正文的总字节(未压缩):重写后的 JS + 额外文件。dry-run 也要能报出来。 */
301
316
  function measureBytes(files, assets) {
302
317
  let total = 0;
303
318
  for (const code of files.values()) {
@@ -313,6 +328,41 @@ function measureBytes(files, assets) {
313
328
  }
314
329
  return total;
315
330
  }
331
+ /**
332
+ * 构建期清单(可选):项目根的 manifest.opmbuild.json。
333
+ *
334
+ * 为什么不塞进 manifest.json:manifest.json 是**运行时**元数据(装到设备上给 Operit 读的),
335
+ * 而"打哪些额外文件、要不要生成 PACKINFO"是**构建期**决策,混在一起会让运行时清单变脏。
336
+ * 单独一个清单文件,职责清楚,也可以不提交(看团队习惯)。
337
+ *
338
+ * 形状:
339
+ * {
340
+ * "extra_files": ["resources", "i18n/zh-CN.js"],
341
+ * "packinfo": { "enable": true }
342
+ * }
343
+ *
344
+ * 兼容:manifest.json 里同名字段仍然生效(先看构建清单,再看 manifest)。
345
+ */
346
+ const OPM_BUILD_CONFIG = 'manifest.opmbuild.json';
347
+ function readBuildConfig(projectDir, errors) {
348
+ const configPath = (0, node_path_1.join)(projectDir, OPM_BUILD_CONFIG);
349
+ if (!(0, node_fs_1.existsSync)(configPath)) {
350
+ return {};
351
+ }
352
+ let parsed;
353
+ try {
354
+ parsed = JSON.parse((0, node_fs_1.readFileSync)(configPath, 'utf8'));
355
+ }
356
+ catch (error) {
357
+ errors.push(OPM_BUILD_CONFIG + ' 不是合法 JSON:' + String(error));
358
+ return {};
359
+ }
360
+ if (parsed === null || typeof parsed !== 'object') {
361
+ errors.push(OPM_BUILD_CONFIG + ' 的顶层必须是对象');
362
+ return {};
363
+ }
364
+ return parsed;
365
+ }
316
366
  function baseName(value) {
317
367
  const normalized = value.replace(/[\\/]+$/, '');
318
368
  const index = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
package/dist/lib/init.js CHANGED
@@ -177,7 +177,7 @@ function initProject(options) {
177
177
  errors.push('npm install 失败:\n' + installOutput.trim());
178
178
  }
179
179
  else {
180
- notes.push('已执行 npm install(源按你自己的 npm 配置,未指定 registry)');
180
+ notes.push('已执行 npm install');
181
181
  }
182
182
  }
183
183
  else {
@@ -0,0 +1,29 @@
1
+ /**
2
+ * PACKINFO:打包信息核对文件(归档根,文件名无扩展名,内容 YAML)。
3
+ *
4
+ * 语义对齐 OPM 的 src/build/packinfo.ts:
5
+ * - 仅当 manifest.packinfo.enable === true 时生成;否则不写这个文件;
6
+ * - 字段顺序固定 name → description → id → version → author → license → dependencies;
7
+ * - 每个字段「有才写、缺则跳过」;id / version 恒有(manifest 是构建前提);
8
+ * - dependencies 从**归档内实际收进的路径**反推(node_modules/<pkg>/... → 包名),
9
+ * 作用域粒度为 @scope/pkg,去重并保持首次出现顺序;
10
+ * - 不写 extra_files。
11
+ */
12
+ export interface PackInfoOptions {
13
+ /** 开关:仅 true 时生成 */
14
+ enable?: unknown;
15
+ name?: unknown;
16
+ description?: unknown;
17
+ version?: unknown;
18
+ author?: unknown;
19
+ license?: unknown;
20
+ }
21
+ /**
22
+ * 从归档路径集合推导依赖列表。
23
+ *
24
+ * 依据"实际进包的文件"而不是"解析到的裸名":这样 node.operit 这类因内建重写
25
+ * 而带进来的运行时也会被如实列为依赖 —— 它就是依赖。
26
+ */
27
+ export declare function collectDependenciesFromArchive(archivePaths: Iterable<string>): string[];
28
+ /** 生成 PACKINFO 文本;返回 null 表示不生成。 */
29
+ export declare function renderPackInfo(manifest: Record<string, unknown>, options: PackInfoOptions | null | undefined, archivePaths: Iterable<string>): string | null;
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.collectDependenciesFromArchive = collectDependenciesFromArchive;
4
+ exports.renderPackInfo = renderPackInfo;
5
+ /** 取本地化文本:优先 .zh,其次 .en,再次任意非空字符串。 */
6
+ function localized(value) {
7
+ if (typeof value === 'string') {
8
+ return value;
9
+ }
10
+ if (value !== null && typeof value === 'object') {
11
+ const obj = value;
12
+ if (typeof obj.zh === 'string' && obj.zh !== '') {
13
+ return obj.zh;
14
+ }
15
+ if (typeof obj.en === 'string' && obj.en !== '') {
16
+ return obj.en;
17
+ }
18
+ for (const key of Object.keys(obj)) {
19
+ const candidate = obj[key];
20
+ if (typeof candidate === 'string' && candidate !== '') {
21
+ return candidate;
22
+ }
23
+ }
24
+ }
25
+ return '';
26
+ }
27
+ /** YAML 标量:含特殊字符时才加双引号。 */
28
+ function yamlScalar(value) {
29
+ const text = String(value);
30
+ if (/^\s|\s$/.test(text) || /[:#"'\[\]{}&*!|>%@`,]/.test(text) || /\n/.test(text)) {
31
+ return '"' + text.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
32
+ }
33
+ return text;
34
+ }
35
+ /**
36
+ * 从归档路径集合推导依赖列表。
37
+ *
38
+ * 依据"实际进包的文件"而不是"解析到的裸名":这样 node.operit 这类因内建重写
39
+ * 而带进来的运行时也会被如实列为依赖 —— 它就是依赖。
40
+ */
41
+ function collectDependenciesFromArchive(archivePaths) {
42
+ const out = [];
43
+ const seen = new Set();
44
+ const prefix = 'node_modules/';
45
+ for (const raw of archivePaths) {
46
+ const path = String(raw === undefined ? '' : raw).replace(/\\/g, '/');
47
+ if (path.indexOf(prefix) !== 0) {
48
+ continue;
49
+ }
50
+ const segments = path.slice(prefix.length).split('/');
51
+ let name = '';
52
+ if (segments[0].charAt(0) === '@') {
53
+ if (segments.length < 2 || segments[1] === '') {
54
+ continue;
55
+ }
56
+ name = segments[0] + '/' + segments[1];
57
+ }
58
+ else {
59
+ if (segments[0] === '') {
60
+ continue;
61
+ }
62
+ name = segments[0];
63
+ }
64
+ if (seen.has(name)) {
65
+ continue;
66
+ }
67
+ seen.add(name);
68
+ out.push(name);
69
+ }
70
+ return out;
71
+ }
72
+ /** 生成 PACKINFO 文本;返回 null 表示不生成。 */
73
+ function renderPackInfo(manifest, options, archivePaths) {
74
+ if (options === null || options === undefined || options.enable !== true) {
75
+ return null;
76
+ }
77
+ const lines = [];
78
+ const name = String(options.name === undefined ? '' : options.name) ||
79
+ localized(manifest.display_name) || localized(manifest.displayName);
80
+ if (name !== '') {
81
+ lines.push('name: ' + yamlScalar(name));
82
+ }
83
+ const description = String(options.description === undefined ? '' : options.description) || localized(manifest.description);
84
+ if (description !== '') {
85
+ lines.push('description: ' + yamlScalar(description));
86
+ }
87
+ const id = String(manifest.toolpkg_id === undefined ? (manifest.id === undefined ? '' : manifest.id) : manifest.toolpkg_id);
88
+ if (id !== '') {
89
+ lines.push('id: ' + yamlScalar(id));
90
+ }
91
+ const version = String(options.version === undefined ? '' : options.version) || String(manifest.version === undefined ? '' : manifest.version);
92
+ if (version !== '') {
93
+ lines.push('version: ' + yamlScalar(version));
94
+ }
95
+ let author = String(options.author === undefined ? '' : options.author);
96
+ if (author === '') {
97
+ const raw = manifest.author;
98
+ if (Array.isArray(raw)) {
99
+ author = raw.map((item) => String(item)).filter((item) => item !== '').join(', ');
100
+ }
101
+ else if (typeof raw === 'string') {
102
+ author = raw;
103
+ }
104
+ }
105
+ if (author !== '') {
106
+ lines.push('author: ' + yamlScalar(author));
107
+ }
108
+ const license = String(options.license === undefined ? '' : options.license) || String(manifest.license === undefined ? '' : manifest.license);
109
+ if (license !== '') {
110
+ lines.push('license: ' + yamlScalar(license));
111
+ }
112
+ const dependencies = collectDependenciesFromArchive(archivePaths);
113
+ if (dependencies.length > 0) {
114
+ lines.push('dependencies:');
115
+ for (const dependency of dependencies) {
116
+ lines.push(' - ' + yamlScalar(dependency));
117
+ }
118
+ }
119
+ if (lines.length === 0) {
120
+ return null;
121
+ }
122
+ return lines.join('\n') + '\n';
123
+ }
@@ -153,9 +153,7 @@ function rewriteModule(env, archivePath, source) {
153
153
  if (site.dynamic) {
154
154
  // 改不了就必须说,不能默认放过:运行时对裸名静默返回 {},
155
155
  // 症状会是"某个依赖神秘失效",比构建立刻失败难查得多。
156
- errors.push(archivePath + ' 里存在无法静态分析的 require(偏移 ' + site.start + ')。\n' +
157
- ' 打包器无法改写它,而 Operit 运行时对裸名会静默返回 {} —— 依赖会在业务里失效而不报错。\n' +
158
- ' 请改成字符串字面量(require(\'fs\')),或拆成多分支的字面量 require。');
156
+ errors.push(archivePath + ':' + site.start + ' 是无法静态分析的 require —— 运行时必然返回 {}。改用字面量:require(\'fs\')');
159
157
  continue;
160
158
  }
161
159
  const spec = site.spec;
@@ -107,11 +107,11 @@ function verifyProject(options) {
107
107
  : built.errors.length + ' 个问题(见 errors)'
108
108
  });
109
109
  checks.push({
110
- name: '资产保留',
110
+ name: '额外文件(extra_files)',
111
111
  ok: true,
112
112
  detail: built.assets.length === 0
113
- ? '工程没有非代码资产'
114
- : '原样带上 ' + built.assets.length + ' 个资产'
113
+ ? 'manifest 没有声明 extra_files'
114
+ : '按 extra_files 带上 ' + built.assets.length + ' 个文件'
115
115
  });
116
116
  const counted = { builtin: 0, package: 0, relative: 0, untouched: 0 };
117
117
  const packageNames = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@serveryyswys/opm",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "把 Operit 插件工程打成 .toolpkg 的构建器(传统 Node 开发环境,不依赖 Operit 运行时)",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "author": "serveryyswys",