react-native-flux-desktop-packer 0.1.4 → 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 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
- return true;
120
+ branded = true;
120
121
  } catch (e) {
121
122
  warn(`rcedit 注入失败(忽略):${(e && e.message) || e}`);
122
- return false;
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/bundle.js CHANGED
@@ -21,6 +21,8 @@ const EXTERNALS = [
21
21
  'react-native-flux-desktop-linux-*',
22
22
  'react-native-flux-desktop-android-*',
23
23
  'react-native-flux-desktop-freebsd-*',
24
+ // 各原生扩展分支包的裸平台包名(ffi / webview / 未来新增):保持 external,运行期由 bootstrap 钩子重定向到自解压目录
25
+ 'react-native-flux-desktop-*-win32-x64-msvc',
24
26
  // @napi-rs/canvas 的平台预编译包(skia .node 由 bootstrap 经 env 指向解压副本)
25
27
  '@napi-rs/canvas-win32-x64-msvc',
26
28
  '@napi-rs/canvas-win32-x64-gnu',
package/lib/config.js CHANGED
@@ -47,6 +47,16 @@ function loadConfig(projectRoot) {
47
47
  const packMode = packObj.mode === 'sea' ? 'sea' : 'folder';
48
48
  // 资源目录(相对项目根),sea 模式内嵌进 exe、运行时解压到 cwd;folder 模式整目录拷贝
49
49
  const assetsDirRel = typeof packObj.assets === 'string' ? packObj.assets : 'assets';
50
+ // 目标原生库(相对项目根路径数组):随包拷进运行目录,运行期 ffi.open(相对路径) 按 cwd 命中。
51
+ const nativeLibs = Array.isArray(packObj.nativeLibs)
52
+ ? packObj.nativeLibs
53
+ : Array.isArray(app.nativeLibs)
54
+ ? app.nativeLibs
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 : [];
50
60
 
51
61
  const cfg = {
52
62
  projectRoot,
@@ -58,6 +68,9 @@ function loadConfig(projectRoot) {
58
68
  outputRel: output,
59
69
  packMode,
60
70
  assetsDirRel,
71
+ nativeLibs,
72
+ nativePackages,
73
+ skipNative,
61
74
  // 归一化入口为平台分隔(Go 里用 dist\\Gallery.js)
62
75
  mainRel: main.split('/').join(path.sep),
63
76
  // 数据/缓存目录用的应用名(与启动器 sanitize 同规则:空格/斜杠等转 '-')
package/lib/index.js CHANGED
@@ -79,6 +79,17 @@ async function pack(options = {}) {
79
79
  fs.copyFileSync(src, path.join(stage, item));
80
80
  }
81
81
  }
82
+ // 目标原生库(ffi):按相对路径拷进负载根,运行期 ffi.open(相对) 按 cwd 命中。
83
+ for (const rel of (cfg.nativeLibs || [])) {
84
+ const src = path.join(projectRoot, rel);
85
+ if (!fs.existsSync(src)) {
86
+ warn(`nativeLibs 跳过(不存在):${rel}`);
87
+ continue;
88
+ }
89
+ const dst = path.join(stage, rel);
90
+ ensureDir(path.dirname(dst));
91
+ fs.copyFileSync(src, dst);
92
+ }
82
93
  // 捆绑当前 node.exe,但以应用名落盘(napi ABI 稳定,跨大版本可加载)。
83
94
  // 运行时进程改名 → 任务管理器「详细信息」页的名称列不再显示 node.exe(node 不校验自身文件名,改名照常运行)。
84
95
  const runtimeName = exeBase + '.exe';
@@ -98,6 +109,7 @@ async function pack(options = {}) {
98
109
  version: cfg.version,
99
110
  });
100
111
  } catch (e) {
112
+ if (e && e.brandFatal) throw e; // FileDescription 回读不达标 → 硬失败,不静默出货
101
113
  warn(`运行时 exe 品牌注入异常(忽略):${e.message}`);
102
114
  }
103
115
 
package/lib/sea.js CHANGED
@@ -61,29 +61,174 @@ function walkFiles(base, rel = '') {
61
61
 
62
62
  /**
63
63
  * 收集需内嵌为 SEA assets 的原生/数据文件。
64
- * @returns {{rnfd:string,skia:string,icu:string}} 绝对路径
64
+ * 通用发现:node_modules 下所有 react-native-flux-desktop* 包根的 *.win32-x64-msvc.node
65
+ * ——覆盖主包 + 各原生扩展分支包(ffi / webview / 未来新增),不再逐包硬编码。
66
+ * @returns {{natives:{base:string,abs:string}[],rnfdBase:string,skia:string,icu:string}}
65
67
  */
66
68
  function locateNatives(projectRoot) {
67
- const roots = [path.join(projectRoot, 'node_modules')];
68
- const pick = (rel) => {
69
- for (const r of roots) {
70
- const f = findFirst(r, rel);
71
- if (f) return f;
69
+ const nm = path.join(projectRoot, 'node_modules');
70
+ // @napi-rs/canvas 平台包:命名特殊(skia.node + icudtl.dat 同目录),单独处理
71
+ const canvasPkg = path.join(nm, '@napi-rs', 'canvas-win32-x64-msvc');
72
+ const skia = findFirst(canvasPkg, ['skia.win32-x64-msvc.node']);
73
+ const icu = findFirst(canvasPkg, ['icudtl.dat']);
74
+ // 通用扫 flux 系原生 .node(主包与所有扩展分支包一视同仁)
75
+ const natives = [];
76
+ let dirs = [];
77
+ try { dirs = fs.readdirSync(nm); } catch { dirs = []; }
78
+ for (const d of dirs) {
79
+ if (!d.startsWith('react-native-flux-desktop')) continue;
80
+ let files = [];
81
+ try { files = fs.readdirSync(path.join(nm, d)); } catch { continue; }
82
+ for (const f of files) {
83
+ if (/\.win32-x64-msvc\.node$/.test(f)) natives.push({ base: f, abs: path.join(nm, d, f) });
72
84
  }
73
- // 兜底:全局扫 node_modules(同名 .node 可能嵌在 pkg 副本里)
74
- return null;
75
- };
76
- const rnfd = pick(['react-native-flux-desktop', 'react-native-flux-desktop.win32-x64-msvc.node']);
77
- const skia = pick(['@napi-rs', 'canvas-win32-x64-msvc', 'skia.win32-x64-msvc.node']);
78
- const icu = pick(['@napi-rs', 'canvas-win32-x64-msvc', 'icudtl.dat']);
85
+ }
86
+ const rnfdBase = 'react-native-flux-desktop.win32-x64-msvc.node';
79
87
  const miss = [];
80
- if (!rnfd) miss.push('react-native-flux-desktop.win32-x64-msvc.node');
88
+ if (!natives.some((n) => n.base === rnfdBase)) miss.push(rnfdBase);
81
89
  if (!skia) miss.push('skia.win32-x64-msvc.node');
82
90
  if (!icu) miss.push('icudtl.dat');
83
91
  if (miss.length) throw new Error(`未找到原生依赖:${miss.join(', ')}\n(sea 模式需在项目 node_modules 里存在这些 .node/dat)`);
84
- return { rnfd, skia, icu };
92
+ return { natives, rnfdBase, skia, icu };
93
+ }
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;
85
229
  }
86
230
 
231
+
87
232
  // 生成 bootstrap.cjs 源码(SEA main)。manifest 以 JSON 字面量注入。
88
233
  function makeBootstrap(manifest) {
89
234
  return `// 自动生成:react-native-flux-desktop-packer sea bootstrap(请勿手改)
@@ -121,14 +266,21 @@ if (!fs.existsSync(appMarker) || fs.readFileSync(appMarker).toString() !== sig)
121
266
  // 3) @napi-rs/canvas:绝对路径直指解压后的 skia(最高优先级)
122
267
  process.env.NAPI_RS_NATIVE_LIBRARY_PATH = path.join(dir, MANIFEST.skiaRel);
123
268
 
124
- // 4) 自研原生:bare/相对 .node 重定向到 dir(全局钩子,对 createRequire 加载的模块同样生效)
125
- const RNFD = 'react-native-flux-desktop.win32-x64-msvc.node';
126
- const RNFD_ABS = path.join(dir, RNFD);
269
+ // 4) 自研原生(主包 + 各扩展分支包 ffi/webview/…):相对 .node 与 bare 平台包名一律重定向到 dir。
270
+ // 通用规则,新增扩展包无需改此处——只要它的 .node 被 locateNatives 收集进 MANIFEST.natives。
271
+ const NATIVE_LIST = MANIFEST.natives || [];
272
+ const NATIVE_SET = new Set(NATIVE_LIST);
127
273
  const origResolve = Module._resolveFilename;
128
274
  Module._resolveFilename = function (request) {
129
275
  if (typeof request === 'string') {
130
- if (request === 'react-native-flux-desktop-win32-x64-msvc') return RNFD_ABS;
131
- if (request.indexOf(RNFD) >= 0) return RNFD_ABS;
276
+ for (let i = 0; i < NATIVE_LIST.length; i++) {
277
+ if (request.indexOf(NATIVE_LIST[i]) >= 0) return path.join(dir, NATIVE_LIST[i]);
278
+ }
279
+ const m = /^(react-native-flux-desktop(?:-[a-z0-9]+)*?)-win32-x64-msvc$/.exec(request);
280
+ if (m) {
281
+ const file = m[1] + '.win32-x64-msvc.node';
282
+ if (NATIVE_SET.has(file)) return path.join(dir, file);
283
+ }
132
284
  }
133
285
  return origResolve.apply(this, arguments);
134
286
  };
@@ -139,7 +291,18 @@ if (MANIFEST.appDirEnv) process.env.FLUX_APP_DIR = MANIFEST.appDirEnv;
139
291
  try { process.chdir(dir); } catch (e) {}
140
292
 
141
293
  // 6) 用 createRequire 正常加载 app(普通 require,绕开 embedderRequire)
142
- Module.createRequire(path.join(dir, 'boot.js'))(appPath);
294
+ // GUI 子系统无控制台:启动期任何抛错(如可选原生 .node 加载失败)都会静默闪退,
295
+ // 故兜底把堆栈写到 LOCALAPPDATA 下 APPDIR 的 startup-error.log,便于诊断“双击打不开”。
296
+ try {
297
+ Module.createRequire(path.join(dir, 'boot.js'))(appPath);
298
+ } catch (err) {
299
+ try {
300
+ const errLog = path.join(process.env.LOCALAPPDATA || os.tmpdir(), APPDIR, 'startup-error.log');
301
+ fs.mkdirSync(path.dirname(errLog), { recursive: true });
302
+ fs.writeFileSync(errLog, '[' + new Date().toISOString() + '] stamp=' + MANIFEST.stamp + '\\n' + ((err && err.stack) || err) + '\\n', 'utf8');
303
+ } catch (e2) {}
304
+ throw err;
305
+ }
143
306
  `;
144
307
  }
145
308
 
@@ -191,10 +354,16 @@ async function packSea(a) {
191
354
  const seaDir = path.join(work, 'sea');
192
355
  ensureDir(seaDir);
193
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
+
194
363
  // ---- 1) bundle app ----
195
364
  step('[s1/4] esbuild 打包应用为单 bundle');
196
365
  const bundle = path.join(seaDir, 'app.cjs');
197
- const r = await buildAppBundle({ projectRoot, entryAbs, outfile: bundle });
366
+ const r = await buildAppBundle({ projectRoot, entryAbs, outfile: bundle, extraExternals: nativeExternals });
198
367
  ok(`app bundle:${fmtMB(r.size)}(全部 JS 已内联,无 dist 源码树)`);
199
368
 
200
369
  // ---- 2) 收集 assets(原生 + 数据 + 资源树)+ stamp ----
@@ -207,9 +376,51 @@ async function packSea(a) {
207
376
  resources.push({ key, rel });
208
377
  assetsMap[key] = abs;
209
378
  };
210
- addRes('rnfd', 'react-native-flux-desktop.win32-x64-msvc.node', nat.rnfd);
379
+ // 通用内嵌所有 flux 原生 .node(主包 + ffi + webview + 未来扩展),按文件名落盘到 dir 根
380
+ for (const n of nat.natives) addRes('nat' + idx++, n.base, n.abs);
381
+ ok(`flux 原生绑定:${nat.natives.map((n) => n.base).join(', ')}`);
211
382
  addRes('skia', 'skia.win32-x64-msvc.node', nat.skia);
212
383
  addRes('icu', 'icudtl.dat', nat.icu);
384
+ // 目标原生库(app.json.pack.nativeLibs / app.nativeLibs):按相对路径内嵌,运行期 ffi.open(相对) 命中 cwd。
385
+ const nativeLibs = Array.isArray(cfg.nativeLibs) ? cfg.nativeLibs : [];
386
+ let nlCount = 0;
387
+ for (const rel of nativeLibs) {
388
+ const abs = path.join(projectRoot, rel);
389
+ if (!fs.existsSync(abs)) {
390
+ warn(`nativeLibs 跳过(不存在):${rel}`);
391
+ continue;
392
+ }
393
+ addRes('nl' + idx++, rel.split('/').join(path.sep), abs);
394
+ nlCount++;
395
+ }
396
+ if (nlCount) ok(`目标原生库随包:${nlCount} 个`);
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
+ }
213
424
 
214
425
  // app.json:应用运行时从 cwd 读它取 name/description/version/icon(如 App.tsx 里
215
426
  // readFileSync(process.cwd()/app.json))。sea 下 cwd=解压目录,故须把 app.json 也展到根,
@@ -231,7 +442,11 @@ async function packSea(a) {
231
442
 
232
443
  // stamp:内容变化才重解压(含 app.json,改名字/描述也能刷新)
233
444
  const ajSig = fs.existsSync(appJsonAbs) ? (() => { const s = fs.statSync(appJsonAbs); return s.size + ':' + Math.round(s.mtimeMs); })() : 'none';
234
- const sigSrc = [nat.rnfd, nat.skia, nat.icu].map((p) => { const s = fs.statSync(p); return s.size + ':' + Math.round(s.mtimeMs); }).join('|') + '|' + ajSig + '|' + r.size;
445
+ const nativeFiles = nat.natives.map((n) => n.abs)
446
+ .concat([nat.skia, nat.icu])
447
+ .concat(nativeLibs.map((r) => path.join(projectRoot, r)).filter((p) => fs.existsSync(p)))
448
+ .concat(shippedNativeFiles);
449
+ const sigSrc = nativeFiles.map((p) => { const s = fs.statSync(p); return s.size + ':' + Math.round(s.mtimeMs); }).join('|') + '|' + ajSig + '|' + r.size;
235
450
  const stamp = crypto.createHash('md5').update(sigSrc).digest('hex').slice(0, 12);
236
451
 
237
452
  const appDirEnv = cfg.safeAppName || 'Flux';
@@ -241,6 +456,7 @@ async function packSea(a) {
241
456
  appDirEnv,
242
457
  appKey: 'app',
243
458
  skiaRel: 'skia.win32-x64-msvc.node',
459
+ natives: nat.natives.map((n) => n.base),
244
460
  resources,
245
461
  };
246
462
  const bootstrapPath = path.join(seaDir, 'bootstrap.cjs');
@@ -277,6 +493,7 @@ async function packSea(a) {
277
493
  version: cfg.version,
278
494
  });
279
495
  } catch (e) {
496
+ if (e && e.brandFatal) throw e; // FileDescription 回读不达标 → 硬失败,不静默出货
280
497
  warn(`品牌注入跳过(不影响运行):${e.message}`);
281
498
  }
282
499
 
@@ -295,4 +512,4 @@ async function packSea(a) {
295
512
  return finalExe;
296
513
  }
297
514
 
298
- 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.4",
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",