@serveryyswys/opm 0.0.0-stage → 0.1.0

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.
@@ -0,0 +1,319 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildProject = buildProject;
4
+ /**
5
+ * opm build:把 Operit 插件工程打成 .toolpkg。
6
+ *
7
+ * 流程(与 node.operit 的契约配套):
8
+ * 1. 读 manifest.json,取可达种子(main + subpackages[].entry);
9
+ * 2. 顺 require 链递归收集被引用文件;
10
+ * 3. 裸名重写(内建 → node.operit 子路径;npm 包 → 包入口;lodash/uuid/axios 原样);
11
+ * 4. 契约拦截:planned / unsupported 直接构建失败,不把问题带到运行期;
12
+ * 5. 入口注入 process / Buffer 全局(沙盒没有这两个全局);
13
+ * 6. 压成 zip,产物放项目根的上一级,名为 <项目目录名>.toolpkg。
14
+ */
15
+ const node_fs_1 = require("node:fs");
16
+ const node_path_1 = require("node:path");
17
+ const zip_1 = require("./zip");
18
+ const contract_1 = require("./contract");
19
+ const rewrite_1 = require("./rewrite");
20
+ const typescript_1 = require("./typescript");
21
+ const dependencies_1 = require("./dependencies");
22
+ function toPosix(value) {
23
+ return value.replace(/\\/g, '/').replace(/^\.\//, '');
24
+ }
25
+ function readManifest(projectDir, errors) {
26
+ const manifestPath = (0, node_path_1.join)(projectDir, 'manifest.json');
27
+ if (!(0, node_fs_1.existsSync)(manifestPath)) {
28
+ errors.push('找不到 ' + manifestPath + ':.toolpkg 必须有 manifest.json(至少要声明 main)');
29
+ return null;
30
+ }
31
+ try {
32
+ return JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
33
+ }
34
+ catch (error) {
35
+ errors.push('manifest.json 不是合法 JSON:' + String(error));
36
+ return null;
37
+ }
38
+ }
39
+ /** 可达种子:main 与所有子包入口。 */
40
+ function collectSeeds(manifest, errors) {
41
+ const seeds = [];
42
+ if (typeof manifest.main === 'string' && manifest.main.length > 0) {
43
+ seeds.push(toPosix(manifest.main));
44
+ }
45
+ else {
46
+ errors.push('manifest.json 缺少 main(字符串),没有入口就无从计算可达性');
47
+ }
48
+ if (Array.isArray(manifest.subpackages)) {
49
+ for (const item of manifest.subpackages) {
50
+ if (item === null || typeof item !== 'object')
51
+ continue;
52
+ const entry = item.entry;
53
+ if (typeof entry === 'string' && entry.length > 0) {
54
+ seeds.push(toPosix(entry));
55
+ }
56
+ }
57
+ }
58
+ return seeds;
59
+ }
60
+ function buildProject(options) {
61
+ const projectDir = (0, node_path_1.resolve)(options.projectDir);
62
+ const errors = [];
63
+ const notes = [];
64
+ const diagnostics = [];
65
+ const manifest = readManifest(projectDir, errors);
66
+ if (manifest === null) {
67
+ return { ok: false, toolpkgPath: '', fileCount: 0, files: [], totalBytes: 0, assets: [], diagnostics, errors, notes };
68
+ }
69
+ // 依赖补齐:重写器是在 node_modules 里找包的,依赖没装齐就会以"解析不到"收场,
70
+ // 那个报错对使用者没有指导性,而一条 npm install 就能解决。所以先补齐再解析。
71
+ // 不指定 registry —— 包从哪来由使用者自己的 npm 配置决定。
72
+ const audit = (0, dependencies_1.auditDependencies)(projectDir);
73
+ if (audit.missing.length > 0) {
74
+ if (options.install === false) {
75
+ errors.push('缺少依赖:' + audit.missing.join(', ') + '\n' +
76
+ ' 执行 npm install 补齐后重试(本次带了 --no-install,所以没有自动装)。');
77
+ return { ok: false, toolpkgPath: '', fileCount: 0, files: [], totalBytes: 0, assets: [], diagnostics, errors, notes };
78
+ }
79
+ notes.push('检测到缺少依赖:' + audit.missing.join(', ') + ',正在执行 npm install');
80
+ const installed = (0, dependencies_1.installDependencies)(projectDir);
81
+ if (!installed.ok) {
82
+ errors.push('npm install 失败:\n' + installed.output.trim());
83
+ return { ok: false, toolpkgPath: '', fileCount: 0, files: [], totalBytes: 0, assets: [], diagnostics, errors, notes };
84
+ }
85
+ const afterInstall = (0, dependencies_1.auditDependencies)(projectDir);
86
+ if (afterInstall.missing.length > 0) {
87
+ errors.push('npm install 执行完了,但这些依赖仍然不在 node_modules 里:' + afterInstall.missing.join(', ') + '\n' +
88
+ ' 检查 package.json 的拼写,或你的 npm 源里有没有这些包。');
89
+ return { ok: false, toolpkgPath: '', fileCount: 0, files: [], totalBytes: 0, assets: [], diagnostics, errors, notes };
90
+ }
91
+ notes.push('依赖已补齐');
92
+ }
93
+ let nodeOperitDir;
94
+ try {
95
+ nodeOperitDir = (0, contract_1.findNodeOperitDir)(projectDir);
96
+ }
97
+ catch (error) {
98
+ errors.push(String(error instanceof Error ? error.message : error));
99
+ return { ok: false, toolpkgPath: '', fileCount: 0, files: [], totalBytes: 0, assets: [], diagnostics, errors, notes };
100
+ }
101
+ const contract = (0, contract_1.loadContract)(nodeOperitDir);
102
+ // TS 模式:先编译,再按 outDir 建立「归档路径 -> 磁盘文件」映射。
103
+ // 归档内路径始终是 manifest 视角,编译产物落在 outDir 由映射负责对上。
104
+ const plan = (0, typescript_1.planTypeScript)(projectDir);
105
+ if (plan.isTypeScript) {
106
+ notes.push('TS 模式,使用 ' + plan.tsconfigPath);
107
+ const compiled = (0, typescript_1.compileTypeScript)(projectDir, plan.tsconfigPath);
108
+ if (!compiled.ok) {
109
+ errors.push('tsc 编译失败:\n' + compiled.output.trim());
110
+ return { ok: false, toolpkgPath: '', fileCount: 0, files: [], totalBytes: 0, assets: [], diagnostics, errors, notes };
111
+ }
112
+ if (compiled.output.trim().length > 0) {
113
+ notes.push('tsc 输出:\n ' + compiled.output.trim().split('\n').join('\n '));
114
+ }
115
+ notes.push('编译产物目录:' + (plan.outDir === null ? '(与源码同目录)' : plan.outDir));
116
+ }
117
+ else {
118
+ notes.push('JS 模式(无 tsconfig.json),以项目根为根打包');
119
+ }
120
+ const locate = (0, typescript_1.createLocator)(projectDir, plan.outDir);
121
+ const env = { locate, contract, nodeOperitArchiveDir: contract_1.NODE_OPERIT_DIR };
122
+ const seeds = collectSeeds(manifest, errors);
123
+ const visited = new Set();
124
+ const rewritten = new Map();
125
+ const queue = [];
126
+ for (const seed of seeds) {
127
+ if (locate(seed) === null) {
128
+ errors.push('入口不存在:' + seed + '(manifest 里写的是相对项目根的路径)');
129
+ continue;
130
+ }
131
+ if (!visited.has(seed)) {
132
+ visited.add(seed);
133
+ queue.push(seed);
134
+ }
135
+ }
136
+ while (queue.length > 0) {
137
+ const archivePath = queue.shift();
138
+ const diskPath = locate(archivePath);
139
+ if (diskPath === null) {
140
+ errors.push('引用了不存在的文件:' + archivePath);
141
+ continue;
142
+ }
143
+ const source = (0, node_fs_1.readFileSync)(diskPath, 'utf8');
144
+ const outcome = (0, rewrite_1.rewriteModule)(env, archivePath, source);
145
+ diagnostics.push(...outcome.diagnostics);
146
+ errors.push(...outcome.errors);
147
+ rewritten.set(archivePath, outcome.code);
148
+ for (const discovered of outcome.discovered) {
149
+ if (!visited.has(discovered)) {
150
+ visited.add(discovered);
151
+ queue.push(discovered);
152
+ }
153
+ }
154
+ }
155
+ // 入口注入运行时全局
156
+ const entryPath = typeof manifest.main === 'string' ? toPosix(manifest.main) : '';
157
+ if (entryPath !== '' && rewritten.has(entryPath)) {
158
+ const joined = toPosix(joinRelative((0, node_path_1.dirname)(entryPath), contract_1.NODE_OPERIT_DIR));
159
+ const relative = joined.startsWith('.') ? joined : './' + joined;
160
+ const prelude = '/* opm: Operit 沙盒没有 process / Buffer 全局,入口先挂上(见 node.operit 的 BUILTINS.json) */\n' +
161
+ "globalThis.process = require('" + relative + "/dist/process.js');\n" +
162
+ "globalThis.Buffer = require('" + relative + "/dist/buffer.js').Buffer;\n";
163
+ rewritten.set(entryPath, prelude + rewritten.get(entryPath));
164
+ notes.push('已向入口 ' + entryPath + ' 注入 process / Buffer 全局');
165
+ }
166
+ // 非 JS 资产原样带上(resources / i18n / modules 等)
167
+ const reachableDiskPaths = new Set();
168
+ for (const archivePath of rewritten.keys()) {
169
+ const disk = locate(archivePath);
170
+ if (disk !== null) {
171
+ reachableDiskPaths.add(disk);
172
+ }
173
+ }
174
+ const collected = collectAssets(projectDir, plan.outDir, reachableDiskPaths);
175
+ errors.push(...collected.errors);
176
+ if (collected.assets.length > 0) {
177
+ notes.push('原样带上 ' + collected.assets.length + ' 个非代码资产(resources / i18n / modules 等)');
178
+ }
179
+ const outName = options.outName === undefined ? baseName(projectDir) : options.outName;
180
+ const toolpkgPath = (0, node_path_1.join)((0, node_path_1.dirname)(projectDir), outName + '.toolpkg');
181
+ if (errors.length > 0) {
182
+ return { ok: false, toolpkgPath, fileCount: rewritten.size, files: [...rewritten.keys()], totalBytes: 0, assets: collected.assets.map((asset) => asset.path), diagnostics, errors, notes };
183
+ }
184
+ if (options.dryRun === true) {
185
+ notes.push('dry-run:未写产物');
186
+ return { ok: true, toolpkgPath, fileCount: rewritten.size, files: [...rewritten.keys()], totalBytes: measureBytes(rewritten, collected.assets), assets: collected.assets.map((asset) => asset.path), diagnostics, errors, notes };
187
+ }
188
+ const entries = [
189
+ { path: 'manifest.json', content: (0, node_fs_1.readFileSync)((0, node_path_1.join)(projectDir, 'manifest.json')) }
190
+ ];
191
+ for (const [archivePath, code] of rewritten) {
192
+ entries.push({ path: archivePath, content: Buffer.from(code, 'utf8') });
193
+ }
194
+ for (const asset of collected.assets) {
195
+ entries.push({ path: asset.path, content: (0, node_fs_1.readFileSync)(asset.disk) });
196
+ }
197
+ const archiveEntries = entries.slice().sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
198
+ let totalBytes = 0;
199
+ for (const entry of archiveEntries) {
200
+ totalBytes += entry.content.length;
201
+ }
202
+ const archive = (0, zip_1.createZip)(entries);
203
+ (0, node_fs_1.mkdirSync)((0, node_path_1.dirname)(toolpkgPath), { recursive: true });
204
+ (0, node_fs_1.writeFileSync)(toolpkgPath, archive);
205
+ return {
206
+ ok: true,
207
+ toolpkgPath,
208
+ fileCount: entries.length,
209
+ files: archiveEntries.map((entry) => entry.path),
210
+ totalBytes,
211
+ assets: collected.assets.map((asset) => asset.path),
212
+ diagnostics,
213
+ errors,
214
+ notes
215
+ };
216
+ }
217
+ /**
218
+ * 非 JS 资产的收集。
219
+ *
220
+ * 规则:**JS 按可达性裁剪,非 JS 一律原样带上。**
221
+ *
222
+ * 为什么:resources / i18n / modules(WASM)这类文件是运行期数据,
223
+ * 静态追 require 是追不到的 —— 早先只按 require 链收集,会把插件的 logo、
224
+ * 多语言文件、WASM 模块**静默丢掉**,而且不报错。官方打包脚本本来就是整包 zip,
225
+ * 使用者按"东西都在包里"预期;这里只裁掉死代码,不再动数据文件。
226
+ */
227
+ const ASSET_SKIP_DIRS = new Set(['node_modules', '.git']);
228
+ const ASSET_SKIP_FILES = new Set([
229
+ // manifest 由构建流程显式加入,不能再被资产收集加一遍(否则 zip 里出现重复条目)
230
+ 'manifest.json', 'manifest.hjson',
231
+ 'package.json', 'package-lock.json', 'npm-shrinkwrap.json',
232
+ 'tsconfig.json', '.npmrc', '.gitignore', '.gitattributes'
233
+ ]);
234
+ const ARCHIVE_LIKE = /\.(tgz|zip|tar|gz)$/i;
235
+ const SCRIPT_LIKE = /\.(js|cjs|mjs|ts|jsx|tsx|map)$/i;
236
+ /**
237
+ * 这三个目录**无条件整目录保留**,即使里面的文件是 .js。
238
+ * 因为 i18n/*.js 是被 i18n 机制加载的**数据**,不是 require 出来的模块 ——
239
+ * 按"JS 一律按可达性裁剪"会把多语言文件一起丢掉,正是这次要修的坑。
240
+ */
241
+ const ALWAYS_INCLUDE_DIRS = new Set(['resources', 'i18n', 'modules']);
242
+ function collectAssets(projectDir, outDir, reachableDiskPaths) {
243
+ const assets = [];
244
+ const errors = [];
245
+ const outDirPrefix = outDir === null ? null : outDir.replace(/[\\/]+$/, '') + node_path_1.sep;
246
+ const walk = (dir, prefix, insideAlways) => {
247
+ let entries;
248
+ try {
249
+ entries = (0, node_fs_1.readdirSync)(dir, { withFileTypes: true });
250
+ }
251
+ catch (error) {
252
+ errors.push('读取目录失败:' + dir + ' —— ' + String(error));
253
+ return;
254
+ }
255
+ for (const entry of entries) {
256
+ const archivePath = prefix === '' ? entry.name : prefix + '/' + entry.name;
257
+ const disk = (0, node_path_1.join)(dir, entry.name);
258
+ const nextInsideAlways = insideAlways || (prefix === '' && ALWAYS_INCLUDE_DIRS.has(entry.name));
259
+ if (entry.isDirectory()) {
260
+ if (ASSET_SKIP_DIRS.has(entry.name)) {
261
+ continue;
262
+ }
263
+ if (outDirPrefix !== null && (disk + node_path_1.sep).startsWith(outDirPrefix)) {
264
+ continue; // 编译产物目录:它的 JS 由可达性接管
265
+ }
266
+ walk(disk, archivePath, nextInsideAlways);
267
+ continue;
268
+ }
269
+ if (prefix === '' && ASSET_SKIP_FILES.has(entry.name)) {
270
+ continue;
271
+ }
272
+ if (!nextInsideAlways && ARCHIVE_LIKE.test(entry.name)) {
273
+ continue; // 打包产物不该再进包 —— 但 resources/ 里的 zip 是合法资源,规范明确支持
274
+ }
275
+ if (reachableDiskPaths.has(disk)) {
276
+ continue; // 已经是可达 JS,不重复
277
+ }
278
+ // 规范目录之外:脚本类文件交给可达性裁剪(不可达的 JS 是死代码),
279
+ // 其它一律当数据带上。
280
+ if (!nextInsideAlways && SCRIPT_LIKE.test(entry.name)) {
281
+ continue;
282
+ }
283
+ assets.push({ path: archivePath, disk });
284
+ }
285
+ };
286
+ walk(projectDir, '', false);
287
+ return { assets, errors };
288
+ }
289
+ /** 归档正文的总字节(未压缩):重写后的 JS + 原样资产。dry-run 也要能报出来。 */
290
+ function measureBytes(files, assets) {
291
+ let total = 0;
292
+ for (const code of files.values()) {
293
+ total += Buffer.byteLength(code, 'utf8');
294
+ }
295
+ for (const asset of assets) {
296
+ try {
297
+ total += (0, node_fs_1.statSync)(asset.disk).size;
298
+ }
299
+ catch {
300
+ // 读不到大小就不计它,不影响其余结论
301
+ }
302
+ }
303
+ return total;
304
+ }
305
+ function baseName(value) {
306
+ const normalized = value.replace(/[\\/]+$/, '');
307
+ const index = Math.max(normalized.lastIndexOf('/'), normalized.lastIndexOf('\\'));
308
+ return index < 0 ? normalized : normalized.slice(index + 1);
309
+ }
310
+ function joinRelative(fromDir, target) {
311
+ const fromParts = fromDir === '' || fromDir === '.' ? [] : fromDir.split('/');
312
+ const targetParts = target.split('/');
313
+ let common = 0;
314
+ while (common < fromParts.length && common < targetParts.length && fromParts[common] === targetParts[common]) {
315
+ common += 1;
316
+ }
317
+ const up = new Array(fromParts.length - common).fill('..');
318
+ return up.concat(targetParts.slice(common)).join('/');
319
+ }
@@ -0,0 +1,23 @@
1
+ export declare const NODE_OPERIT_PACKAGE = "@serveryyswys/node.operit";
2
+ export declare const NODE_OPERIT_DIR = "node_modules/@serveryyswys/node.operit";
3
+ /** 运行时已内建、重写时必须保持原样的裸名。 */
4
+ export declare const RUNTIME_BUILTINS: string[];
5
+ export type BuiltinStatus = 'ready' | 'partial' | 'planned' | 'unsupported';
6
+ export interface BuiltinEntry {
7
+ subpath: string;
8
+ status: BuiltinStatus;
9
+ note?: string;
10
+ }
11
+ export interface Contract {
12
+ package: string;
13
+ distDir: string;
14
+ subpathTemplate: string;
15
+ installDir: string;
16
+ builtins: Record<string, BuiltinEntry>;
17
+ }
18
+ export declare class ContractError extends Error {
19
+ constructor(message: string);
20
+ }
21
+ /** 找到项目里安装的 node.operit 包目录;找不到就给出可操作的提示。 */
22
+ export declare function findNodeOperitDir(projectDir: string): string;
23
+ export declare function loadContract(nodeOperitDir: string): Contract;
@@ -0,0 +1,60 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ContractError = exports.RUNTIME_BUILTINS = exports.NODE_OPERIT_DIR = exports.NODE_OPERIT_PACKAGE = void 0;
4
+ exports.findNodeOperitDir = findNodeOperitDir;
5
+ exports.loadContract = loadContract;
6
+ /**
7
+ * BUILTINS.json 契约表 —— 重写规则的唯一真源。
8
+ *
9
+ * 表在 node.operit 包里(node_modules/@serveryyswys/node.operit/BUILTINS.json),
10
+ * 不是本包自带的副本:那份副本一定会过期,而过期的契约会把构建带偏。
11
+ */
12
+ const node_fs_1 = require("node:fs");
13
+ const node_path_1 = require("node:path");
14
+ exports.NODE_OPERIT_PACKAGE = '@serveryyswys/node.operit';
15
+ exports.NODE_OPERIT_DIR = 'node_modules/@serveryyswys/node.operit';
16
+ /** 运行时已内建、重写时必须保持原样的裸名。 */
17
+ exports.RUNTIME_BUILTINS = ['lodash', 'uuid', 'axios'];
18
+ class ContractError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = 'ContractError';
22
+ }
23
+ }
24
+ exports.ContractError = ContractError;
25
+ /** 找到项目里安装的 node.operit 包目录;找不到就给出可操作的提示。 */
26
+ function findNodeOperitDir(projectDir) {
27
+ const candidates = [
28
+ (0, node_path_1.join)(projectDir, exports.NODE_OPERIT_DIR),
29
+ // pnpm / 提升安装时可能落在更上层
30
+ (0, node_path_1.join)(projectDir, '..', exports.NODE_OPERIT_DIR)
31
+ ];
32
+ for (const candidate of candidates) {
33
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(candidate, 'BUILTINS.json'))) {
34
+ return candidate;
35
+ }
36
+ }
37
+ throw new ContractError('找不到 ' + exports.NODE_OPERIT_PACKAGE + '(在 ' + projectDir + ' 下找过 ' + exports.NODE_OPERIT_DIR + ')。\n' +
38
+ '它是重写规则的来源,必须先安装:npm install -D ' + exports.NODE_OPERIT_PACKAGE);
39
+ }
40
+ function loadContract(nodeOperitDir) {
41
+ const contractPath = (0, node_path_1.join)(nodeOperitDir, 'BUILTINS.json');
42
+ let raw;
43
+ try {
44
+ raw = JSON.parse((0, node_fs_1.readFileSync)(contractPath, 'utf8'));
45
+ }
46
+ catch (error) {
47
+ throw new ContractError('读取契约表失败:' + contractPath + '\n' + String(error));
48
+ }
49
+ const record = raw;
50
+ if (record.builtins === undefined || typeof record.builtins !== 'object') {
51
+ throw new ContractError('契约表缺少 builtins 段:' + contractPath);
52
+ }
53
+ return {
54
+ package: record.package === undefined ? exports.NODE_OPERIT_PACKAGE : record.package,
55
+ distDir: record.distDir === undefined ? 'dist' : record.distDir,
56
+ subpathTemplate: record.subpathTemplate === undefined ? '<distDir>/<subpath>.js' : record.subpathTemplate,
57
+ installDir: record.installDir === undefined ? exports.NODE_OPERIT_DIR : record.installDir,
58
+ builtins: record.builtins
59
+ };
60
+ }
@@ -0,0 +1,24 @@
1
+ export interface DependencyAudit {
2
+ /** package.json 里声明了、但 node_modules 里没有的包 */
3
+ missing: string[];
4
+ /** 是否连 package.json 都没有 */
5
+ hasManifest: boolean;
6
+ }
7
+ /**
8
+ * 审计依赖。
9
+ *
10
+ * 只查 dependencies,不查 devDependencies —— 进包的是运行期依赖,
11
+ * 开发依赖被 require 到属于打包错误,应当让重写器如实报错,而不是默默装进来。
12
+ * 例外是 node.operit:它是**构建期**的契约来源,无论写在哪一段都必须存在。
13
+ */
14
+ export declare function auditDependencies(projectDir: string): DependencyAudit;
15
+ /**
16
+ * 跑 npm install。
17
+ *
18
+ * 输出重定向到临时文件而不是管道:与调 tsc 同一个理由(受限环境不允许管道,
19
+ * 而这里必须拿到 npm 的日志才能报错)。
20
+ */
21
+ export declare function installDependencies(projectDir: string): {
22
+ ok: boolean;
23
+ output: string;
24
+ };
@@ -0,0 +1,105 @@
1
+ 'use strict';
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.auditDependencies = auditDependencies;
4
+ exports.installDependencies = installDependencies;
5
+ /**
6
+ * 依赖补齐。
7
+ *
8
+ * 为什么 build 要管这件事:重写器是在 node_modules 里找包的,依赖没装齐就会直接
9
+ * "既不是 Node 内建、也不在 node_modules 里" —— 那个报错对使用者没有指导性,
10
+ * 而且明明一条 npm install 就能解决。所以 build 先补齐再解析。
11
+ *
12
+ * **不指定 registry**:包从哪来由使用者自己的 npm 配置决定(私有源的用户自然走私源)。
13
+ */
14
+ const node_fs_1 = require("node:fs");
15
+ const node_child_process_1 = require("node:child_process");
16
+ const node_path_1 = require("node:path");
17
+ const node_os_1 = require("node:os");
18
+ const contract_1 = require("./contract");
19
+ function readManifest(projectDir) {
20
+ const manifestPath = (0, node_path_1.join)(projectDir, 'package.json');
21
+ if (!(0, node_fs_1.existsSync)(manifestPath)) {
22
+ return null;
23
+ }
24
+ try {
25
+ return JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
26
+ }
27
+ catch {
28
+ return null;
29
+ }
30
+ }
31
+ /** 判断一个包在不在 node_modules 里(看它自己的 package.json)。 */
32
+ function isInstalled(projectDir, name) {
33
+ return (0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, 'node_modules', name, 'package.json'));
34
+ }
35
+ /**
36
+ * 审计依赖。
37
+ *
38
+ * 只查 dependencies,不查 devDependencies —— 进包的是运行期依赖,
39
+ * 开发依赖被 require 到属于打包错误,应当让重写器如实报错,而不是默默装进来。
40
+ * 例外是 node.operit:它是**构建期**的契约来源,无论写在哪一段都必须存在。
41
+ */
42
+ function auditDependencies(projectDir) {
43
+ const manifest = readManifest(projectDir);
44
+ if (manifest === null) {
45
+ // 没有 package.json 也可能是合法的 toolpkg 工程(纯 Operit 工程),
46
+ // 所以这里仍然要如实检查 node.operit 装没装,不能一律判缺。
47
+ return {
48
+ missing: isInstalled(projectDir, contract_1.NODE_OPERIT_PACKAGE) ? [] : [contract_1.NODE_OPERIT_PACKAGE],
49
+ hasManifest: false
50
+ };
51
+ }
52
+ const names = new Set([contract_1.NODE_OPERIT_PACKAGE]);
53
+ const dependencies = manifest.dependencies;
54
+ if (dependencies !== null && typeof dependencies === 'object') {
55
+ for (const name of Object.keys(dependencies)) {
56
+ names.add(name);
57
+ }
58
+ }
59
+ const missing = [];
60
+ for (const name of names) {
61
+ if (!isInstalled(projectDir, name)) {
62
+ missing.push(name);
63
+ }
64
+ }
65
+ missing.sort();
66
+ return { missing, hasManifest: true };
67
+ }
68
+ /**
69
+ * 跑 npm install。
70
+ *
71
+ * 输出重定向到临时文件而不是管道:与调 tsc 同一个理由(受限环境不允许管道,
72
+ * 而这里必须拿到 npm 的日志才能报错)。
73
+ */
74
+ function installDependencies(projectDir) {
75
+ const logPath = (0, node_path_1.join)((0, node_os_1.tmpdir)(), 'opm-npm-' + process.pid + '-' + Date.now() + '.log');
76
+ const logFd = (0, node_fs_1.openSync)(logPath, 'w');
77
+ let result;
78
+ try {
79
+ result = (0, node_child_process_1.spawnSync)('npm', ['install'], {
80
+ cwd: projectDir,
81
+ shell: true,
82
+ stdio: ['ignore', logFd, logFd]
83
+ });
84
+ }
85
+ finally {
86
+ (0, node_fs_1.closeSync)(logFd);
87
+ }
88
+ let output = '';
89
+ try {
90
+ output = (0, node_fs_1.readFileSync)(logPath, 'utf8');
91
+ }
92
+ catch {
93
+ output = '';
94
+ }
95
+ try {
96
+ (0, node_fs_1.unlinkSync)(logPath);
97
+ }
98
+ catch {
99
+ // 清不掉临时日志不影响结论
100
+ }
101
+ if (result.error !== undefined && result.error !== null) {
102
+ return { ok: false, output: output + '\n无法启动 npm:' + result.error.message };
103
+ }
104
+ return { ok: result.status === 0, output };
105
+ }
@@ -0,0 +1,19 @@
1
+ export declare const OPM_PACKAGE = "@serveryyswys/opm";
2
+ export interface InitOptions {
3
+ projectDir: string;
4
+ /** 是否跑 npm install(默认 true) */
5
+ install?: boolean;
6
+ /** 覆盖已存在的 package.json 字段(默认 false:只补不改) */
7
+ overwrite?: boolean;
8
+ }
9
+ export interface InitResult {
10
+ ok: boolean;
11
+ created: string[];
12
+ updated: string[];
13
+ unchanged: string[];
14
+ notes: string[];
15
+ errors: string[];
16
+ /** 建议使用者接着执行的命令 */
17
+ nextSteps: string[];
18
+ }
19
+ export declare function initProject(options: InitOptions): InitResult;