@serveryyswys/opm 0.1.1 → 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 +58 -8
- package/dist/cli.js +45 -42
- package/dist/lib/build.js +112 -62
- package/dist/lib/init.js +1 -1
- package/dist/lib/js-requires.d.ts +9 -2
- package/dist/lib/js-requires.js +18 -1
- package/dist/lib/packinfo.d.ts +29 -0
- package/dist/lib/packinfo.js +123 -0
- package/dist/lib/rewrite.js +6 -0
- package/dist/lib/verify.js +3 -3
- package/package.json +1 -1
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
|
-
|
|
|
85
|
-
| `
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
>
|
|
91
|
-
>
|
|
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
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
132
|
-
|
|
143
|
+
if (!result.ok) {
|
|
144
|
+
for (const error of result.errors) {
|
|
145
|
+
console.error('✗ ' + error);
|
|
146
|
+
}
|
|
147
|
+
return 1;
|
|
133
148
|
}
|
|
134
|
-
if (
|
|
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('
|
|
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
-
//
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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('
|
|
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
|
-
*
|
|
240
|
+
* 额外文件收集 —— 照 OPM 的 extra_files 语义。
|
|
230
241
|
*
|
|
231
|
-
*
|
|
242
|
+
* OPM(src/build/extrafiles.ts)只收 manifest 显式列出的路径:
|
|
243
|
+
* - 是文件 → 收;
|
|
244
|
+
* - 是目录 → **递归**收集其下文件(跳过 node_modules 与点目录,深度上限 10);
|
|
245
|
+
* - 不在列表里的一律不进归档。
|
|
232
246
|
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
*
|
|
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
|
|
253
|
-
|
|
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
|
|
257
|
-
const
|
|
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)(
|
|
276
|
+
entries = (0, node_fs_1.readdirSync)(absDir, { withFileTypes: true });
|
|
261
277
|
}
|
|
262
278
|
catch (error) {
|
|
263
|
-
errors.push('读取目录失败:' +
|
|
279
|
+
errors.push('读取目录失败:' + relDir + ' —— ' + String(error));
|
|
264
280
|
return;
|
|
265
281
|
}
|
|
266
282
|
for (const entry of entries) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
283
|
+
if (entry.name.startsWith('.')) {
|
|
284
|
+
continue; // 点目录/点文件不进包
|
|
285
|
+
}
|
|
286
|
+
const childRel = relDir === '' ? entry.name : relDir + '/' + entry.name;
|
|
270
287
|
if (entry.isDirectory()) {
|
|
271
|
-
if (
|
|
288
|
+
if (EXTRA_SKIP_DIRS.has(entry.name)) {
|
|
272
289
|
continue;
|
|
273
290
|
}
|
|
274
|
-
|
|
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
|
-
|
|
294
|
+
addFile(childRel);
|
|
295
295
|
}
|
|
296
296
|
};
|
|
297
|
-
|
|
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 +
|
|
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
|
@@ -8,8 +8,15 @@
|
|
|
8
8
|
* 以及 "// require('x')" 这种情况。
|
|
9
9
|
*/
|
|
10
10
|
export interface RequireSite {
|
|
11
|
-
/** require
|
|
12
|
-
spec: string;
|
|
11
|
+
/** require 的实参(已去引号、已解转义);**动态形式为 null** */
|
|
12
|
+
spec: string | null;
|
|
13
|
+
/**
|
|
14
|
+
* 无法静态分析(变量 / 模板串 / 字符串拼接 / 三元…)。
|
|
15
|
+
*
|
|
16
|
+
* 这类 require 打包器改不了,而 Operit 运行时对裸名**静默返回 {}** ——
|
|
17
|
+
* 于是依赖在业务里神秘失效。所以必须显式报错,不能放过。
|
|
18
|
+
*/
|
|
19
|
+
dynamic: boolean;
|
|
13
20
|
/** spec 在源码里的起止下标(不含引号),替换时用 */
|
|
14
21
|
start: number;
|
|
15
22
|
end: number;
|
package/dist/lib/js-requires.js
CHANGED
|
@@ -97,12 +97,29 @@ function findRequires(source) {
|
|
|
97
97
|
cursor += 1;
|
|
98
98
|
}
|
|
99
99
|
if (cursor < length) {
|
|
100
|
-
|
|
100
|
+
// 字面量后面若还跟着运算('a' + x / 'a' ? ...),实参其实是表达式。
|
|
101
|
+
// 以前这里会把它当成 'a' 去重写 —— 截断比忽略更糟,必须区分。
|
|
102
|
+
let after = cursor + 1;
|
|
103
|
+
while (after < length && /\s/.test(source[after])) {
|
|
104
|
+
after += 1;
|
|
105
|
+
}
|
|
106
|
+
const trailing = source[after];
|
|
107
|
+
const isExpression = trailing === '+' || trailing === '?' || trailing === '|' || trailing === '&';
|
|
108
|
+
sites.push({
|
|
109
|
+
spec: isExpression ? null : value,
|
|
110
|
+
dynamic: isExpression,
|
|
111
|
+
start: start,
|
|
112
|
+
end: cursor
|
|
113
|
+
});
|
|
101
114
|
index = cursor + 1;
|
|
102
115
|
previousMeaningful = quote;
|
|
103
116
|
continue;
|
|
104
117
|
}
|
|
105
118
|
}
|
|
119
|
+
else {
|
|
120
|
+
// 实参不是字符串字面量:变量 / 模板串 / 三元 / 函数调用…全部无法静态改写
|
|
121
|
+
sites.push({ spec: null, dynamic: true, start: cursor, end: cursor });
|
|
122
|
+
}
|
|
106
123
|
}
|
|
107
124
|
}
|
|
108
125
|
}
|
|
@@ -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
|
+
}
|
package/dist/lib/rewrite.js
CHANGED
|
@@ -150,6 +150,12 @@ function rewriteModule(env, archivePath, source) {
|
|
|
150
150
|
let output = source;
|
|
151
151
|
for (let index = sites.length - 1; index >= 0; index -= 1) {
|
|
152
152
|
const site = sites[index];
|
|
153
|
+
if (site.dynamic) {
|
|
154
|
+
// 改不了就必须说,不能默认放过:运行时对裸名静默返回 {},
|
|
155
|
+
// 症状会是"某个依赖神秘失效",比构建立刻失败难查得多。
|
|
156
|
+
errors.push(archivePath + ':' + site.start + ' 是无法静态分析的 require —— 运行时必然返回 {}。改用字面量:require(\'fs\')');
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
153
159
|
const spec = site.spec;
|
|
154
160
|
if (contract_1.RUNTIME_BUILTINS.includes(spec)) {
|
|
155
161
|
diagnostics.push({ file: archivePath, spec, kind: 'untouched', target: spec });
|
package/dist/lib/verify.js
CHANGED
|
@@ -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
|
-
: '
|
|
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();
|