react-native-flux-desktop-packer 0.1.5 → 0.1.7
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/lib/brand.js +36 -3
- package/lib/config.js +6 -0
- package/lib/index.js +1 -0
- package/lib/sea.js +175 -3
- package/package.json +1 -1
package/lib/brand.js
CHANGED
|
@@ -111,15 +111,16 @@ async function brandRuntime(exePath, { projectRoot, iconPngPaths, name, descript
|
|
|
111
111
|
args.push('--set-file-version', v, '--set-product-version', v);
|
|
112
112
|
}
|
|
113
113
|
if (ico) args.push('--set-icon', ico);
|
|
114
|
+
let branded = false;
|
|
114
115
|
try {
|
|
115
116
|
const r = cp.spawnSync(bin, args, { windowsHide: true, encoding: 'utf8', timeout: 60000 });
|
|
116
117
|
if (r.error) throw r.error;
|
|
117
118
|
if (r.status !== 0) throw new Error(String(r.stderr || r.stdout || '').trim() || 'exit ' + r.status);
|
|
118
119
|
ok(`exe 已注入品牌:${path.basename(exePath)}${ico ? ' + 图标' : ''}`);
|
|
119
|
-
|
|
120
|
+
branded = true;
|
|
120
121
|
} catch (e) {
|
|
121
122
|
warn(`rcedit 注入失败(忽略):${(e && e.message) || e}`);
|
|
122
|
-
|
|
123
|
+
branded = false;
|
|
123
124
|
} finally {
|
|
124
125
|
if (ico) {
|
|
125
126
|
try {
|
|
@@ -129,6 +130,38 @@ async function brandRuntime(exePath, { projectRoot, iconPngPaths, name, descript
|
|
|
129
130
|
}
|
|
130
131
|
}
|
|
131
132
|
}
|
|
133
|
+
if (!branded) return false;
|
|
134
|
+
// 回读硬校验:FileDescription 决定任务栏/Alt-Tab 分组名。历史上曾因「打包用的是陈旧 file: 拷贝、
|
|
135
|
+
// 该行被注释」而反复静默翻车(rcedit 跑了、ProductName 改了,唯独 FileDescription 仍是 Node 默认)。
|
|
136
|
+
// 故注入成功后回读确认:若仍为 node.exe 默认名 → 抛 brandFatal,让打包直接失败而非静默出货。
|
|
137
|
+
const got = readFileDescription(exePath);
|
|
138
|
+
if (got === null) {
|
|
139
|
+
warn('未能回读 FileDescription 校验(非 Windows 或 PowerShell 不可用)——请人工确认任务栏分组名已品牌化');
|
|
140
|
+
} else if (got === '' || /node\.?js javascript runtime/i.test(got)) {
|
|
141
|
+
const err = new Error(
|
|
142
|
+
`FileDescription 注入未生效:回读为「${got || '(空)'}」,任务栏/进程名仍会显示 Node.js 默认。` +
|
|
143
|
+
`(若本地打包反复出现此错,多半是消费方 node_modules 里的 packer 为陈旧 file: 拷贝,需同步 lib/)\nrcedit args=${JSON.stringify(args)}`
|
|
144
|
+
);
|
|
145
|
+
err.brandFatal = true;
|
|
146
|
+
throw err;
|
|
147
|
+
} else {
|
|
148
|
+
ok(`FileDescription 回读校验通过:${got}`);
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** 读回 exe 内嵌的 FileDescription(Windows 经 PowerShell 的 VersionInfo);无法读取返回 null。 */
|
|
154
|
+
function readFileDescription(exePath) {
|
|
155
|
+
if (process.platform !== 'win32') return null;
|
|
156
|
+
try {
|
|
157
|
+
const lit = "'" + String(exePath).replace(/'/g, "''") + "'"; // PowerShell 单引号字面量转义
|
|
158
|
+
const cmd = `(Get-Item -LiteralPath ${lit}).VersionInfo.FileDescription`;
|
|
159
|
+
const r = cp.spawnSync('powershell', ['-NoProfile', '-Command', cmd], { windowsHide: true, encoding: 'utf8', timeout: 15000 });
|
|
160
|
+
if (r.error || r.status !== 0) return null;
|
|
161
|
+
return String(r.stdout || '').trim();
|
|
162
|
+
} catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
132
165
|
}
|
|
133
166
|
|
|
134
|
-
module.exports = { brandRuntime, buildIcoFromPngs, resolveRcedit, resolveRceditBin };
|
|
167
|
+
module.exports = { brandRuntime, buildIcoFromPngs, resolveRcedit, resolveRceditBin, readFileDescription };
|
package/lib/config.js
CHANGED
|
@@ -53,6 +53,10 @@ function loadConfig(projectRoot) {
|
|
|
53
53
|
: Array.isArray(app.nativeLibs)
|
|
54
54
|
? app.nativeLibs
|
|
55
55
|
: [];
|
|
56
|
+
// 第三方原生包(sea 模式):nativePackages 强制纳入整包落盘;skipNative 排除自动扫描命中的包。
|
|
57
|
+
// 二者均可缺省——sea 会自动扫描 node_modules 下含 .node 的第三方包,这里仅作兑底。
|
|
58
|
+
const nativePackages = Array.isArray(packObj.nativePackages) ? packObj.nativePackages : [];
|
|
59
|
+
const skipNative = Array.isArray(packObj.skipNative) ? packObj.skipNative : [];
|
|
56
60
|
|
|
57
61
|
const cfg = {
|
|
58
62
|
projectRoot,
|
|
@@ -65,6 +69,8 @@ function loadConfig(projectRoot) {
|
|
|
65
69
|
packMode,
|
|
66
70
|
assetsDirRel,
|
|
67
71
|
nativeLibs,
|
|
72
|
+
nativePackages,
|
|
73
|
+
skipNative,
|
|
68
74
|
// 归一化入口为平台分隔(Go 里用 dist\\Gallery.js)
|
|
69
75
|
mainRel: main.split('/').join(path.sep),
|
|
70
76
|
// 数据/缓存目录用的应用名(与启动器 sanitize 同规则:空格/斜杠等转 '-')
|
package/lib/index.js
CHANGED
package/lib/sea.js
CHANGED
|
@@ -92,6 +92,143 @@ function locateNatives(projectRoot) {
|
|
|
92
92
|
return { natives, rnfdBase, skia, icu };
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
// 判定一个包目录内(递归)是否存在 .node。不递归进嵌套 node_modules(其依赖作为顶层包单独处理)。
|
|
96
|
+
function pkgHasNodeFile(dir, depth = 0) {
|
|
97
|
+
if (depth > 8) return false;
|
|
98
|
+
let items;
|
|
99
|
+
try {
|
|
100
|
+
items = fs.readdirSync(dir, { withFileTypes: true });
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
for (const it of items) {
|
|
105
|
+
if (it.name === 'node_modules') continue;
|
|
106
|
+
const a = path.join(dir, it.name);
|
|
107
|
+
if (it.isDirectory()) {
|
|
108
|
+
if (pkgHasNodeFile(a, depth + 1)) return true;
|
|
109
|
+
} else if (/\.node$/.test(it.name)) {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 列出 node_modules 下所有包(含 @scope/*),返回 [{name, absDir}]。
|
|
117
|
+
function listInstalledPackages(nm) {
|
|
118
|
+
const out = [];
|
|
119
|
+
let top;
|
|
120
|
+
try {
|
|
121
|
+
top = fs.readdirSync(nm, { withFileTypes: true });
|
|
122
|
+
} catch {
|
|
123
|
+
return out;
|
|
124
|
+
}
|
|
125
|
+
for (const it of top) {
|
|
126
|
+
if (!it.isDirectory() || it.name.startsWith('.')) continue;
|
|
127
|
+
const abs = path.join(nm, it.name);
|
|
128
|
+
if (it.name.startsWith('@')) {
|
|
129
|
+
let subs;
|
|
130
|
+
try {
|
|
131
|
+
subs = fs.readdirSync(abs, { withFileTypes: true });
|
|
132
|
+
} catch {
|
|
133
|
+
subs = [];
|
|
134
|
+
}
|
|
135
|
+
for (const s of subs) {
|
|
136
|
+
if (s.isDirectory() && fs.existsSync(path.join(abs, s.name, 'package.json'))) {
|
|
137
|
+
out.push({ name: it.name + '/' + s.name, absDir: path.join(abs, s.name) });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else if (fs.existsSync(path.join(abs, 'package.json'))) {
|
|
141
|
+
out.push({ name: it.name, absDir: abs });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 收集需在 sea 下「整包原样落盘」的第三方原生包(node-pty / 未来 mysql/redis/better-sqlite3…)。
|
|
149
|
+
* 自动扫描 node_modules 下含 .node 的包 + cfg.nativePackages 强制纳入,剔 cfg.skipNative;
|
|
150
|
+
* 已特殊处理的 rnfd 系与 @napi-rs/canvas 不走本通道。再沿 runtime dependencies 求闭包,
|
|
151
|
+
* 确保被外置包的纯 JS 依赖也在磁盘上可解析。
|
|
152
|
+
* @returns {{name:string, absDir:string, native:boolean}[]} native=true 者需标为 esbuild external
|
|
153
|
+
*/
|
|
154
|
+
function collectNativePackages(projectRoot, cfg) {
|
|
155
|
+
const nm = path.join(projectRoot, 'node_modules');
|
|
156
|
+
const extra = (cfg && Array.isArray(cfg.nativePackages) && cfg.nativePackages) || [];
|
|
157
|
+
const skip = new Set((cfg && Array.isArray(cfg.skipNative) && cfg.skipNative) || []);
|
|
158
|
+
const isSpecial = (name) =>
|
|
159
|
+
name.startsWith('react-native-flux-desktop') || name === '@napi-rs/canvas' || name.startsWith('@napi-rs/canvas-');
|
|
160
|
+
// @types/* 为纯 TypeScript 声明包:运行时无 require,闭包不该把它拉进来(且个别 .d.ts 内嵌 SEA fuse 串会污染 blob)
|
|
161
|
+
const isTypeOnly = (name) => name.startsWith('@types/');
|
|
162
|
+
|
|
163
|
+
const all = listInstalledPackages(nm);
|
|
164
|
+
const byName = new Map(all.map((p) => [p.name, p]));
|
|
165
|
+
|
|
166
|
+
// 1) 命中的原生包(自动扫描 或 显式纳入)
|
|
167
|
+
const nativeNames = new Set();
|
|
168
|
+
for (const p of all) {
|
|
169
|
+
if (isSpecial(p.name) || isTypeOnly(p.name) || skip.has(p.name)) continue;
|
|
170
|
+
if (extra.includes(p.name) || pkgHasNodeFile(p.absDir)) nativeNames.add(p.name);
|
|
171
|
+
}
|
|
172
|
+
// 显式纳入但 node_modules 里找不到的 → 告警(配置错误早暴露)
|
|
173
|
+
for (const e of extra) {
|
|
174
|
+
if (!byName.has(e)) warn(`nativePackages 声明的包不在 node_modules:${e}(已忽略)`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 2) 沿 runtime deps 求闭包(仅纳入顶层能解析到的)
|
|
178
|
+
const closure = new Map(); // name -> {name, absDir, native}
|
|
179
|
+
const queue = [];
|
|
180
|
+
for (const n of nativeNames) {
|
|
181
|
+
const p = byName.get(n);
|
|
182
|
+
if (p) {
|
|
183
|
+
closure.set(n, { name: n, absDir: p.absDir, native: true });
|
|
184
|
+
queue.push(n);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
while (queue.length) {
|
|
188
|
+
const name = queue.shift();
|
|
189
|
+
const p = byName.get(name);
|
|
190
|
+
if (!p) continue;
|
|
191
|
+
let deps = [];
|
|
192
|
+
try {
|
|
193
|
+
deps = Object.keys(JSON.parse(fs.readFileSync(path.join(p.absDir, 'package.json'), 'utf8')).dependencies || {});
|
|
194
|
+
} catch {
|
|
195
|
+
deps = [];
|
|
196
|
+
}
|
|
197
|
+
for (const d of deps) {
|
|
198
|
+
if (isSpecial(d) || isTypeOnly(d) || skip.has(d) || closure.has(d)) continue;
|
|
199
|
+
const dp = byName.get(d);
|
|
200
|
+
if (dp) {
|
|
201
|
+
closure.set(d, { name: d, absDir: dp.absDir, native: false });
|
|
202
|
+
queue.push(d);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return [...closure.values()];
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** 第三方原生包落盘时的逐文件裁剪:只留 win32-x64 二进制 + 运行时 JS,剔除其它平台与非运行时文件。 */
|
|
210
|
+
function nativeAssetFilter(relIn) {
|
|
211
|
+
const rel = '/' + relIn.split(path.sep).join('/');
|
|
212
|
+
const base = rel.split('/').pop();
|
|
213
|
+
// 声明文件运行时无用(纯类型),一律剔除——既瘦身,又避免个别 .d.ts 内嵌串(如 @types/node/sea.d.ts 的 SEA fuse)污染 blob
|
|
214
|
+
if (/\.d\.ts$/.test(base)) return false;
|
|
215
|
+
// 其它平台原生产物(win32-x64 之外)一律丢弃
|
|
216
|
+
const pm = /\/prebuilds\/([^/]+)\//.exec(rel);
|
|
217
|
+
if (pm && pm[1] !== 'win32-x64') return false;
|
|
218
|
+
if (/\/prebuilds\/(darwin|linux|freebsd|android|sunos|aix|win32-arm64|win32-ia32)\b/.test(rel)) return false;
|
|
219
|
+
if (base && /-(darwin|linux|freebsd|android)-(x64|arm64|ia32|arm-eabi|arm64-v8a)/.test(base)) return false;
|
|
220
|
+
if (base && /-win32-(arm64|ia32)/.test(base)) return false;
|
|
221
|
+
// 明确的构建/非运行时目录(不含 src:某些包 main 指向 src)
|
|
222
|
+
if (/\/(deps|test|tests|examples|example|\.github|typings)\//.test(rel)) return false;
|
|
223
|
+
if (/\/third_party\//.test(rel)) return false;
|
|
224
|
+
// 非运行时扩展
|
|
225
|
+
if (/\.(pdb|map|cc|cpp|c|h|hpp|ts|tsx|md|yml|yaml|gyp|gypi|editorconfig)$/.test(base)) return false;
|
|
226
|
+
if (/\.(test|spec)\.js$/.test(base)) return false;
|
|
227
|
+
if (base === 'binding.gyp') return false;
|
|
228
|
+
return true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
|
|
95
232
|
// 生成 bootstrap.cjs 源码(SEA main)。manifest 以 JSON 字面量注入。
|
|
96
233
|
function makeBootstrap(manifest) {
|
|
97
234
|
return `// 自动生成:react-native-flux-desktop-packer sea bootstrap(请勿手改)
|
|
@@ -217,10 +354,16 @@ async function packSea(a) {
|
|
|
217
354
|
const seaDir = path.join(work, 'sea');
|
|
218
355
|
ensureDir(seaDir);
|
|
219
356
|
|
|
357
|
+
// 第三方原生包(node-pty 等):须在 bundle 前算出,以便把它们的 JS 标为 external(不内联),
|
|
358
|
+
// 并在下面作为整包 assets 落盘。collectNativePackages 只依赖 node_modules,与 bundle 无耦合。
|
|
359
|
+
const nativePkgs = collectNativePackages(projectRoot, cfg);
|
|
360
|
+
const nativeExternals = nativePkgs.filter((p) => p.native).map((p) => p.name);
|
|
361
|
+
if (nativeExternals.length) ok(`第三方原生包(external + 整包落盘):${nativeExternals.join(', ')}`);
|
|
362
|
+
|
|
220
363
|
// ---- 1) bundle app ----
|
|
221
364
|
step('[s1/4] esbuild 打包应用为单 bundle');
|
|
222
365
|
const bundle = path.join(seaDir, 'app.cjs');
|
|
223
|
-
const r = await buildAppBundle({ projectRoot, entryAbs, outfile: bundle });
|
|
366
|
+
const r = await buildAppBundle({ projectRoot, entryAbs, outfile: bundle, extraExternals: nativeExternals });
|
|
224
367
|
ok(`app bundle:${fmtMB(r.size)}(全部 JS 已内联,无 dist 源码树)`);
|
|
225
368
|
|
|
226
369
|
// ---- 2) 收集 assets(原生 + 数据 + 资源树)+ stamp ----
|
|
@@ -252,6 +395,33 @@ async function packSea(a) {
|
|
|
252
395
|
}
|
|
253
396
|
if (nlCount) ok(`目标原生库随包:${nlCount} 个`);
|
|
254
397
|
|
|
398
|
+
// 第三方原生包整包落盘:把闭包内每个包目录(经平台/非运行时裁剪)以 node_modules/<name>/... 相对路径
|
|
399
|
+
// 内嵌为 assets,运行期解压到 <run-dir>/node_modules/ → createRequire 天然解析,其相对 .node 与兄弟 DLL 自洽。
|
|
400
|
+
const shippedNativeFiles = [];
|
|
401
|
+
const shippedNodeByPkg = new Map();
|
|
402
|
+
for (const p of nativePkgs) {
|
|
403
|
+
let count = 0;
|
|
404
|
+
let nodeCount = 0;
|
|
405
|
+
for (const rel of walkFiles(p.absDir)) {
|
|
406
|
+
const relFull = path.join('node_modules', p.name, rel);
|
|
407
|
+
if (!nativeAssetFilter(relFull)) continue;
|
|
408
|
+
const absFile = path.join(p.absDir, rel);
|
|
409
|
+
addRes('np' + idx++, relFull, absFile);
|
|
410
|
+
shippedNativeFiles.push(absFile);
|
|
411
|
+
count++;
|
|
412
|
+
if (/\.node$/.test(rel)) nodeCount++;
|
|
413
|
+
}
|
|
414
|
+
shippedNodeByPkg.set(p.name, nodeCount);
|
|
415
|
+
ok(`第三方原生包落盘:node_modules/${p.name}(${count} 文件${p.native ? '' : ',JS 依赖'})`);
|
|
416
|
+
}
|
|
417
|
+
// 验证守卫:仅当包源码内确有 .node(真原生包)却被裁剪到无一个 .node 落盘时,才告警(防「外置了却没落盘」)。
|
|
418
|
+
// 白名单强制外置的纯 JS 包(sequelize/mysql2 等本就无 .node)属正常,不误报。
|
|
419
|
+
for (const p of nativePkgs) {
|
|
420
|
+
if (p.native && !shippedNodeByPkg.get(p.name) && pkgHasNodeFile(p.absDir)) {
|
|
421
|
+
warn(`第三方原生包 ${p.name} 已标 external,但裁剪后无 .node 落盘——运行期可能加载失败(检查其 .node 是否放在被裁剪的目录)`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
255
425
|
// app.json:应用运行时从 cwd 读它取 name/description/version/icon(如 App.tsx 里
|
|
256
426
|
// readFileSync(process.cwd()/app.json))。sea 下 cwd=解压目录,故须把 app.json 也展到根,
|
|
257
427
|
// 否则读不到会回退成默认 name:'App'(窗口标题/托盘 tooltip 都变 'App')。
|
|
@@ -274,7 +444,8 @@ async function packSea(a) {
|
|
|
274
444
|
const ajSig = fs.existsSync(appJsonAbs) ? (() => { const s = fs.statSync(appJsonAbs); return s.size + ':' + Math.round(s.mtimeMs); })() : 'none';
|
|
275
445
|
const nativeFiles = nat.natives.map((n) => n.abs)
|
|
276
446
|
.concat([nat.skia, nat.icu])
|
|
277
|
-
.concat(nativeLibs.map((r) => path.join(projectRoot, r)).filter((p) => fs.existsSync(p)))
|
|
447
|
+
.concat(nativeLibs.map((r) => path.join(projectRoot, r)).filter((p) => fs.existsSync(p)))
|
|
448
|
+
.concat(shippedNativeFiles);
|
|
278
449
|
const sigSrc = nativeFiles.map((p) => { const s = fs.statSync(p); return s.size + ':' + Math.round(s.mtimeMs); }).join('|') + '|' + ajSig + '|' + r.size;
|
|
279
450
|
const stamp = crypto.createHash('md5').update(sigSrc).digest('hex').slice(0, 12);
|
|
280
451
|
|
|
@@ -322,6 +493,7 @@ async function packSea(a) {
|
|
|
322
493
|
version: cfg.version,
|
|
323
494
|
});
|
|
324
495
|
} catch (e) {
|
|
496
|
+
if (e && e.brandFatal) throw e; // FileDescription 回读不达标 → 硬失败,不静默出货
|
|
325
497
|
warn(`品牌注入跳过(不影响运行):${e.message}`);
|
|
326
498
|
}
|
|
327
499
|
|
|
@@ -340,4 +512,4 @@ async function packSea(a) {
|
|
|
340
512
|
return finalExe;
|
|
341
513
|
}
|
|
342
514
|
|
|
343
|
-
module.exports = { packSea, makeBootstrap, locateNatives, walkFiles };
|
|
515
|
+
module.exports = { packSea, makeBootstrap, locateNatives, walkFiles, collectNativePackages, nativeAssetFilter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-flux-desktop-packer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "Package a react-native-flux-desktop app into a self-contained Windows .exe (Go launcher + go:embed payload). Install as a devDependency, add `output`/`main` to app.json, then run `flux-pack`.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "keyon",
|