crossbind 0.0.1 → 2.0.0-beta.51

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.
Files changed (95) hide show
  1. package/AGENTS.md +92 -0
  2. package/CHANGELOG.md +156 -0
  3. package/LICENSE +21 -0
  4. package/README.md +133 -0
  5. package/package.json +52 -4
  6. package/src/actions/buildBinTools.js +0 -0
  7. package/src/actions/buildCargo.js +88 -0
  8. package/src/actions/buildDependencies.js +157 -0
  9. package/src/actions/buildExternal.js +57 -0
  10. package/src/actions/buildJs.js +93 -0
  11. package/src/actions/buildLib.js +115 -0
  12. package/src/actions/buildTypes.js +33 -0
  13. package/src/actions/buildWasiCommand.js +101 -0
  14. package/src/actions/buildWasm.js +322 -0
  15. package/src/actions/createInterface.js +172 -0
  16. package/src/actions/createLib.js +249 -0
  17. package/src/actions/createXCFramework.js +82 -0
  18. package/src/actions/extensions.js +7 -0
  19. package/src/actions/getAllBridges.js +20 -0
  20. package/src/actions/getCmakeParameters.js +66 -0
  21. package/src/actions/getData.js +46 -0
  22. package/src/actions/getDependLibs.js +25 -0
  23. package/src/actions/isSourceNewer.js +35 -0
  24. package/src/actions/licenses.js +156 -0
  25. package/src/actions/run.js +307 -0
  26. package/src/actions/target.js +54 -0
  27. package/src/assets/cmake/CMakeLists.txt +61 -0
  28. package/src/assets/cmake/dist.cmake +57 -0
  29. package/src/assets/cmake/ios.toolchain.cmake +1176 -0
  30. package/src/assets/cpp-runtime/browser.cpp +12 -0
  31. package/src/assets/cpp-runtime/commonBridges.cpp +17 -0
  32. package/src/assets/cpp-runtime/crossbindEmptySource.cpp +9 -0
  33. package/src/assets/cpp-runtime/node.cpp +19 -0
  34. package/src/assets/js-runtime/adapters/exception-decode.js +79 -0
  35. package/src/assets/js-runtime/adapters/fs-browser.js +173 -0
  36. package/src/assets/js-runtime/adapters/fs-node.js +28 -0
  37. package/src/assets/js-runtime/adapters/path-fs.js +6 -0
  38. package/src/assets/js-runtime/adapters/path-url.js +8 -0
  39. package/src/assets/js-runtime/adapters/vector-coercion.js +209 -0
  40. package/src/assets/js-runtime/adapters/worker-comlink.js +309 -0
  41. package/src/assets/js-runtime/browser.js +16 -0
  42. package/src/assets/js-runtime/core.js +193 -0
  43. package/src/assets/js-runtime/edge.js +18 -0
  44. package/src/assets/js-runtime/node.js +20 -0
  45. package/src/assets/packaging/crossbind-package.podspec +17 -0
  46. package/src/assets/wasi-runtime/stubs.c +84 -0
  47. package/src/bin.js +416 -0
  48. package/src/index.js +19 -0
  49. package/src/integration/getCrossbindScript.js +180 -0
  50. package/src/integration/getDependFilePath.js +132 -0
  51. package/src/runtime/wasiRun.mjs +118 -0
  52. package/src/state/calculateDependencyParameters.js +97 -0
  53. package/src/state/index.js +128 -0
  54. package/src/state/loadConfig.js +215 -0
  55. package/src/utils/appRustCrates.js +106 -0
  56. package/src/utils/binTools.js +76 -0
  57. package/src/utils/bridgeAsyncGuard.js +37 -0
  58. package/src/utils/cargoTarget.js +15 -0
  59. package/src/utils/cppDts.js +201 -0
  60. package/src/utils/cppFieldBindings.js +23 -0
  61. package/src/utils/dependencyRebuild.js +99 -0
  62. package/src/utils/dirLock.js +71 -0
  63. package/src/utils/downloadAndExtractFile.js +102 -0
  64. package/src/utils/familyManifest.js +17 -0
  65. package/src/utils/findFiles.js +5 -0
  66. package/src/utils/fixPackageName.js +7 -0
  67. package/src/utils/flattenConfigForTable.js +75 -0
  68. package/src/utils/getAbsolutePath.js +14 -0
  69. package/src/utils/getCMakeListsFilePath.js +17 -0
  70. package/src/utils/getOsUserAndGroupId.js +11 -0
  71. package/src/utils/getParentPath.js +12 -0
  72. package/src/utils/hash.js +33 -0
  73. package/src/utils/inputStamp.js +28 -0
  74. package/src/utils/licenseReport.js +94 -0
  75. package/src/utils/linkLayout.js +34 -0
  76. package/src/utils/loadJs.js +31 -0
  77. package/src/utils/loadJson.js +16 -0
  78. package/src/utils/logger.js +110 -0
  79. package/src/utils/ociArtifact.js +124 -0
  80. package/src/utils/overrideDependency.js +99 -0
  81. package/src/utils/provenance.js +54 -0
  82. package/src/utils/pullDockerImage.js +105 -0
  83. package/src/utils/replaceBasePathForDocker.js +18 -0
  84. package/src/utils/resolveEmbindRust.js +66 -0
  85. package/src/utils/runCargo.js +228 -0
  86. package/src/utils/rustBridgeGen.js +1013 -0
  87. package/src/utils/rustMt.js +76 -0
  88. package/src/utils/rustSysroot.js +171 -0
  89. package/src/utils/rustSysrootPin.js +11 -0
  90. package/src/utils/systemKeys.js +29 -0
  91. package/src/utils/targets.js +109 -0
  92. package/src/utils/wasiToolchain.js +47 -0
  93. package/src/utils/writeIfChanged.js +8 -0
  94. package/src/utils/writeJson.js +9 -0
  95. package/vitest.config.js +46 -0
@@ -0,0 +1,322 @@
1
+ import fs from 'node:fs';
2
+ import { fileURLToPath } from 'node:url';
3
+ import replace from 'replace';
4
+ import run from './run.js';
5
+ import getDependLibs from './getDependLibs.js';
6
+ import getData from './getData.js';
7
+ import buildJs from './buildJs.js';
8
+ import triggerExtensions from './extensions.js';
9
+ import state from '../state/index.js';
10
+ import resolveEmbindRustRoot from '../utils/resolveEmbindRust.js';
11
+ import logger from '../utils/logger.js';
12
+ import { getContentHash, getFilesFingerprint } from '../utils/hash.js';
13
+ import { buildLinkLibArgs } from '../utils/linkLayout.js';
14
+ import buildAppRustCrates from '../utils/appRustCrates.js';
15
+
16
+ export default async function buildWasm(target, options = {}) {
17
+ const isProd = target.buildType === 'release';
18
+ const buildType = isProd ? 'Release' : 'Debug';
19
+
20
+ // Caller can opt out of the final emcc link entirely (e.g. when the
21
+ // package is consumed only as a static library by downstream builds).
22
+ if (state.config.export.bundle === false) {
23
+ logger.info(`[${target.path}] wasm+js skipped (export.bundle = false)`);
24
+ return false;
25
+ }
26
+ // Cargo packages only stage their staticlib; the consuming app links and bundles it.
27
+ if (state.config.export.type === 'cargo') {
28
+ logger.info(`[${target.path}] wasm+js skipped (cargo package - staticlib only)`);
29
+ return false;
30
+ }
31
+
32
+ // buildLib's cache is keyed on paths.output/prebuilt; after a cache clean the
33
+ // build-dir copy can be gone while the output artifact is still valid — link it then.
34
+ const sourceLibCandidates = [
35
+ `${state.config.paths.build}/Source-${buildType}/${target.path}/lib${state.config.general.name}.a`,
36
+ `${state.config.paths.output}/prebuilt/${target.path}/lib/lib${state.config.general.name}.a`,
37
+ ];
38
+ // App-local Rust surfaces (imported .rs files) arrive as ONE super staticlib; it is the
39
+ // single fully-loaded Rust archive of the link (see the libstd rule below).
40
+ const appRustLibs = buildAppRustCrates(target, state.config.paths.cache, state.config.cargoDependencies ?? {});
41
+ const libs = [
42
+ ...getDependLibs(target),
43
+ ...appRustLibs,
44
+ sourceLibCandidates.find((lib) => fs.existsSync(lib)) ?? sourceLibCandidates[0],
45
+ `${state.config.paths.build}/Bridge-${buildType}/${target.path}/lib${state.config.general.name}.a`,
46
+ ];
47
+
48
+ // By default only the Bridge archive is --whole-archive'd (see
49
+ // linkLayout.js) so wasm-ld dead-code-eliminates everything unreferenced.
50
+ // Two escape hatches, both `export.wholeArchive: true`: on the APP it
51
+ // restores the legacy layout (every archive wholesale); on a LIBRARY's
52
+ // own config it keeps that library's archives whole in every consumer
53
+ // link (for members that self-register from static initializers).
54
+ const wholeArchiveAll = state.config.export.wholeArchive === true;
55
+ const wholeArchiveNames = new Set();
56
+ // Rust libstd rule: every Rust staticlib bundles its own libstd, so at most ONE Rust archive
57
+ // may be fully loaded (the app super staticlib). Generated cargo bridges are linked lazily
58
+ // with their keep symbol pinned (-u pulls just the registration object); manual-bindings
59
+ // crates have no keep symbol and fall back to whole-archive - safe only while they are the
60
+ // single loaded Rust archive.
61
+ const rustKeepFlags = [];
62
+ if (appRustLibs.length > 0) wholeArchiveNames.add('crossbind_app_super');
63
+ state.config.dependencyParameters.getCmakeDepends(target).forEach((dep) => {
64
+ if (dep.export.wholeArchive === true) {
65
+ (dep.export.libName || []).forEach((name) => wholeArchiveNames.add(name));
66
+ }
67
+ if (dep.export.type === 'cargo') {
68
+ const crateLibRs = `${dep.paths.project}/${dep.export.crate ?? 'crate'}/src/lib.rs`.replace('/./', '/');
69
+ const isManual = fs.existsSync(crateLibRs) && fs.readFileSync(crateLibRs, 'utf8').includes('bindings!');
70
+ (dep.export.libName || []).forEach((name) => {
71
+ // wasm-ld's -u does NOT pull archive members (it just leaves an import);
72
+ // --export forces the symbol to resolve, dragging the registration object in.
73
+ if (isManual) wholeArchiveNames.add(name);
74
+ else rustKeepFlags.push(`-Wl,--export=crossbind_keep_${name}`);
75
+ });
76
+ }
77
+ });
78
+ // The whole-archived app super-staticlib and a lazily-pulled package bridge object each
79
+ // carry rustc's allocator/panic shims (codegen-units=1 places them in the same object as
80
+ // the pulled registrations). They are bit-identical toolchain synthetics; Mach-O dedups
81
+ // them as weak defs, wasm-ld sees strong duplicates - allow them for this combination only.
82
+ if (appRustLibs.length > 0 && rustKeepFlags.length > 0) {
83
+ rustKeepFlags.push('-Wl,--allow-multiple-definition');
84
+ }
85
+ const linkLibs = buildLinkLibArgs(libs, { wholeArchiveAll, wholeArchiveNames });
86
+
87
+ // Any Rust archive in the link needs the web adapter TU: it provides the flat
88
+ // crossbind_embind_* C-ABI (typeid getters + passthroughs to emscripten's embind).
89
+ const hasRust = rustKeepFlags.length > 0 || appRustLibs.length > 0
90
+ || state.config.dependencyParameters.getCmakeDepends(target).some((dep) => dep.export.type === 'cargo');
91
+ // The adapter ships in @crossbind/core-embind-rust (declared by the consumer, resolved here).
92
+ const rustSources = hasRust ? [`${resolveEmbindRustRoot()}/adapters/web.cpp`] : [];
93
+
94
+ const binary = getData('binary', target);
95
+ const emccFlags = [...(binary?.emccFlags || []), ...rustKeepFlags];
96
+
97
+ triggerExtensions('buildWasm', 'beforeBuild', [emccFlags]);
98
+
99
+ if (target.runtime === 'mt' && !emccFlags.includes('-pthread')) {
100
+ emccFlags.push('-pthread');
101
+ emccFlags.push('-sPTHREAD_POOL_SIZE=Math.min(navigator.hardwareConcurrency || 1, 2)');
102
+ emccFlags.push('-sPTHREAD_POOL_SIZE_STRICT=2');
103
+ }
104
+
105
+ if (target.platform === 'wasm') {
106
+ emccFlags.push('-msimd128');
107
+ }
108
+
109
+ if (target.arch === 'wasm64') {
110
+ emccFlags.push('-sMEMORY64=1');
111
+ }
112
+
113
+ if (state.config.excludedDependencies?.length && !emccFlags.includes('-sERROR_ON_UNDEFINED_SYMBOLS=0')) {
114
+ emccFlags.push('-sERROR_ON_UNDEFINED_SYMBOLS=0');
115
+ }
116
+
117
+ // Link inputs (flags, lib set, preloaded data) are invisible to a pure
118
+ // artifact-existence cache: a config emccFlags change used to keep
119
+ // serving the old wasm until a manual .crossbind clear. Fingerprint them next
120
+ // to the artifact and treat a mismatch as a cache miss. Lib entries carry
121
+ // size+mtime, so a rebuilt dependency archive (same path, new content)
122
+ // forces the relink too.
123
+ const linkFingerprintFile = `${state.config.paths.build}/${target.jsName}.fingerprint`;
124
+ // The artifact also bundles the JS runtime (buildJs) and links the C runtime
125
+ // entries from cpp-runtime/ - inputs a flag/lib fingerprint cannot see, so a
126
+ // runtime edit used to keep serving the previously linked output.
127
+ const runtimeAssetDirs = [
128
+ `${state.config.paths.cli}/assets/js-runtime`,
129
+ `${state.config.paths.cli}/assets/cpp-runtime`,
130
+ ];
131
+ const runtimeAssets = runtimeAssetDirs.flatMap((dir) => (fs.existsSync(dir)
132
+ ? fs.readdirSync(dir, { recursive: true })
133
+ .map((file) => `${dir}/${file}`)
134
+ .filter((file) => fs.statSync(file).isFile())
135
+ : []));
136
+ const linkFingerprint = getContentHash(JSON.stringify({
137
+ // Bump when the link-arg layout itself changes (e.g. the move to
138
+ // bridge-only --whole-archive), so cached artifacts from the old
139
+ // layout cannot satisfy the new one. The effective whole-archive
140
+ // set is part of the layout: flipping a config flag must relink.
141
+ linkLayout: 'v2-bridge-only-whole-archive',
142
+ // The emcc arg tail is hardcoded per environment in this file, invisible to the
143
+ // emccFlags entry below - hashing the builder itself makes any inline-arg edit
144
+ // (a new -s flag, an EXPORTED_RUNTIME_METHODS change) a guaranteed cache miss.
145
+ builder: getFilesFingerprint([fileURLToPath(import.meta.url)]),
146
+ wholeArchiveAll,
147
+ wholeArchiveNames: [...wholeArchiveNames].sort(),
148
+ emccFlags,
149
+ libs: libs.map((lib) => {
150
+ const stat = fs.existsSync(lib) ? fs.statSync(lib) : null;
151
+ return { lib, size: stat ? stat.size : null, mtimeMs: stat ? stat.mtimeMs : null };
152
+ }),
153
+ data: getData('data', target),
154
+ runtime: getFilesFingerprint(runtimeAssets),
155
+ rustSources: getFilesFingerprint(rustSources),
156
+ }));
157
+ const linkChanged = !fs.existsSync(linkFingerprintFile)
158
+ || fs.readFileSync(linkFingerprintFile, { encoding: 'utf8' }) !== linkFingerprint;
159
+
160
+ if (!options.force && !linkChanged && fs.existsSync(`${state.config.paths.build}/${target.jsName}`) && fs.existsSync(`${state.config.paths.build}/${target.wasmName}`)) {
161
+ logger.cachedStep(target, 'wasm+js');
162
+ return false;
163
+ }
164
+
165
+ if (target.runtimeEnv === 'browser') {
166
+ logger.startStep(target, 'wasm');
167
+ const t0 = performance.now();
168
+
169
+ triggerExtensions('buildWasm', 'beforeBuildBrowser', [emccFlags]);
170
+
171
+ const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
172
+ run('emcc', [
173
+ '-lembind',
174
+ ...emccFlags,
175
+ // '-lwebsocket.js', '-sPROXY_POSIX_SOCKETS', '-sWEBSOCKET_DEBUG=1', '-sJSPI', '-g', '-sWASMFS',
176
+ '-sWASM_BIGINT=1', '-s', 'FORCE_FILESYSTEM=1',
177
+ '-sEXPORT_NAME=Module2', // '-pthread', '-sPTHREAD_POOL_SIZE=5',
178
+ ...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/browser.cpp`,
179
+ ...(isProd ? ['-O3'] : []),
180
+ '-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
181
+ '-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'FORCE_FILESYSTEM=1',
182
+ '-s', 'ALLOW_MEMORY_GROWTH=1',
183
+ // emsdk 6 defaults GROWABLE_ARRAYBUFFERS=1; Firefox/WebKit TextDecoder rejects views
184
+ // over resizable ArrayBuffers, breaking every string crossing the wasm boundary.
185
+ '-s', 'GROWABLE_ARRAYBUFFERS=0',
186
+ '-s', 'WASMFS',
187
+ '-s', 'ENVIRONMENT=web,webview,worker',
188
+ // getExceptionMessage must be listed here too: the helpers flag alone defines it
189
+ // in the glue but does not attach it to the MODULARIZE instance.
190
+ '-s', 'EXPORTED_RUNTIME_METHODS=["FS", "ENV", "getExceptionMessage"]',
191
+ '-fwasm-exceptions',
192
+ // Exports getExceptionMessage so the runtime can decode raw WebAssembly.Exception
193
+ // values back into JS Errors carrying the C++ what() text.
194
+ '-sEXPORT_EXCEPTION_HANDLING_HELPERS',
195
+ '-o', `${state.config.paths.build}/${target.rawJsName}`,
196
+ ...data,
197
+ ], null, target);
198
+ const t1 = performance.now();
199
+ logger.doneStep(target, 'wasm');
200
+ logger.startStep(target, 'js');
201
+ // The pthread bootstrap spawns workers from _scriptName, captured from
202
+ // document.currentScript at LOAD time - undefined when the bridge is
203
+ // loaded as a module script, so workers would fetch "/undefined" and
204
+ // direct-mode mt init hangs. Inject Module.crossbindMainScript (set by the
205
+ // browser adapter from paths.worker/paths.js; a crossbind-specific key, as
206
+ // emscripten's own mainScriptUrlOrBlob trips emsdk 6 debug assertions
207
+ // on st targets) at SPAWN time instead. The previous spaced
208
+ // 'var _scriptName = ' rewrite no longer matched the minified glue and
209
+ // silently no-opped; the guard below turns any future format drift
210
+ // into a visible error instead.
211
+ replace({
212
+ regex: 'pthreadMainJs=_scriptName',
213
+ replacement: 'pthreadMainJs=Module["crossbindMainScript"]||_scriptName',
214
+ paths: [`${state.config.paths.build}/${target.rawJsName}`],
215
+ recursive: false,
216
+ silent: true,
217
+ });
218
+ const glue = fs.readFileSync(`${state.config.paths.build}/${target.rawJsName}`, 'utf8');
219
+ if (glue.includes('pthreadMainJs') && !glue.includes('Module["crossbindMainScript"]||_scriptName')) {
220
+ logger.error('pthread spawn rewrite missed (emscripten glue format changed?): mt builds may fetch /undefined workers');
221
+ }
222
+ /* replace({
223
+ regex: 'val === 10',
224
+ replacement: 'false',
225
+ paths: [`${state.config.paths.build}/${state.config.general.name}.js`],
226
+ recursive: false,
227
+ silent: true,
228
+ }); */
229
+ await buildJs(target);
230
+ // fs.rmSync(`${state.config.paths.build}/${state.config.general.name}.js`);
231
+ // fs.copyFileSync(`${state.config.paths.build}/${state.config.general.name}.browser.js`, `${state.config.paths.build}/${state.config.general.name}.js`);
232
+ // fs.renameSync(`${state.config.paths.build}/${state.config.general.name}.js`, `${state.config.paths.build}/${state.config.general.name}.worker.browser.js`);
233
+ logger.doneStep(target, 'js');
234
+ }
235
+
236
+ if (target.runtimeEnv === 'edge') {
237
+ logger.startStep(target, 'wasm');
238
+ const t0 = performance.now();
239
+
240
+ triggerExtensions('buildWasm', 'beforeBuildEdge', [emccFlags]);
241
+
242
+ const data = Object.entries(getData('data', target)).map(([key, value]) => ['--preload-file', `${key.replaceAll('@', '@@')}@/crossbind/${value}`]).flat();
243
+ run('emcc', [
244
+ '-lembind',
245
+ ...emccFlags,
246
+ '-sWASM_BIGINT=1',
247
+ '-sEXPORT_NAME=Module2',
248
+ // rustSources carries the embind-rust adapter TU (emval/json/tid hooks); without
249
+ // it any linked Rust package archive fails with undefined crossbind_* symbols.
250
+ ...linkLibs, ...rustSources,
251
+ ...(isProd ? ['-O3'] : []),
252
+ '-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
253
+ '-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'FORCE_FILESYSTEM=1',
254
+ '-s', 'ALLOW_MEMORY_GROWTH=1',
255
+ // See the GROWABLE_ARRAYBUFFERS note in the browser block (Firefox/WebKit TextDecoder).
256
+ '-s', 'GROWABLE_ARRAYBUFFERS=0',
257
+ '-s', 'ENVIRONMENT=web',
258
+ '-s', 'EXPORTED_RUNTIME_METHODS=["ENV", "getExceptionMessage"]',
259
+ '-fwasm-exceptions',
260
+ '-sEXPORT_EXCEPTION_HANDLING_HELPERS',
261
+ '-o', `${state.config.paths.build}/${target.rawJsName}`,
262
+ ...data,
263
+ ], null, target);
264
+ const t1 = performance.now();
265
+ logger.doneStep(target, 'wasm');
266
+ logger.startStep(target, 'js');
267
+ await buildJs(target);
268
+ logger.doneStep(target, 'js');
269
+ }
270
+
271
+ if (target.runtimeEnv === 'node') {
272
+ logger.startStep(target, 'wasm');
273
+
274
+ triggerExtensions('buildWasm', 'beforeBuildNodeJS', [emccFlags]);
275
+
276
+ run('emcc', [
277
+ '-lembind',
278
+ ...emccFlags,
279
+ // '-s', 'FETCH', '-sJSPI', '-sWASM_BIGINT=1', '-pthread', '-sPTHREAD_POOL_SIZE=5',
280
+ '-sWASM_BIGINT=1', '-s', 'FORCE_FILESYSTEM=1',
281
+ ...linkLibs, ...rustSources, `${state.config.paths.cli}/assets/cpp-runtime/node.cpp`,
282
+ ...(isProd ? ['-O3'] : []),
283
+ '-s', 'WASM=1', '-s', 'MODULARIZE=1', '-s', 'DYNAMIC_EXECUTION=0',
284
+ '-s', 'RESERVED_FUNCTION_POINTERS=200', // '-s', 'DISABLE_EXCEPTION_CATCHING=0', '-s', 'FORCE_FILESYSTEM=1',
285
+ '-s', 'ALLOW_MEMORY_GROWTH=1',
286
+ // See the GROWABLE_ARRAYBUFFERS note in the browser block (older Node V8 lacks it too).
287
+ '-s', 'GROWABLE_ARRAYBUFFERS=0',
288
+ '-s', 'WASMFS',
289
+ '-s', 'NODE_HOST_ENV=1',
290
+ '-s', 'ENVIRONMENT=node',
291
+ '-s', 'EXPORTED_RUNTIME_METHODS=["FS", "ENV", "getExceptionMessage"]',
292
+ '-fwasm-exceptions',
293
+ '-sEXPORT_EXCEPTION_HANDLING_HELPERS',
294
+ '-o', `${state.config.paths.build}/${target.rawJsName}`,
295
+ ], null, target);
296
+ logger.doneStep(target, 'wasm');
297
+ logger.startStep(target, 'js');
298
+ await buildJs(target);
299
+ if (emccFlags.includes('FETCH')) {
300
+ fs.appendFileSync(`${state.config.paths.build}/${target.jsName}`, 'var XMLHttpRequest = require(\'xhr2\');\n');
301
+ }
302
+ // fs.renameSync(`${state.config.paths.build}/${state.config.general.name}.js`, `${state.config.paths.build}/${state.config.general.name}.worker.node.js`);
303
+ logger.doneStep(target, 'js');
304
+
305
+ Object.entries(getData('data', target)).forEach(([key, value]) => {
306
+ if (fs.existsSync(key)) {
307
+ const dAssetPath = `${state.config.paths.build}/data/${value}`;
308
+ if (!fs.existsSync(dAssetPath)) {
309
+ fs.mkdirSync(dAssetPath, { recursive: true });
310
+ fs.cpSync(key, dAssetPath, { recursive: true });
311
+ }
312
+ }
313
+ });
314
+ }
315
+
316
+ if (fs.existsSync(`${state.config.paths.build}/${target.dataName}`)) {
317
+ fs.renameSync(`${state.config.paths.build}/${target.dataName}`, `${state.config.paths.build}/${target.dataTxtName}`);
318
+ }
319
+
320
+ fs.writeFileSync(linkFingerprintFile, linkFingerprint);
321
+ return true;
322
+ }
@@ -0,0 +1,172 @@
1
+
2
+ import fs from 'node:fs';
3
+ import upath from 'upath';
4
+ import state, { saveCache } from '../state/index.js';
5
+ import { getFileHash } from '../utils/hash.js';
6
+ import guardAsyncBindings from '../utils/bridgeAsyncGuard.js';
7
+ import { writeHeaderDts, parseCppSurface } from '../utils/cppDts.js';
8
+ import { injectFieldBindings } from '../utils/cppFieldBindings.js';
9
+ import writeIfChanged from '../utils/writeIfChanged.js';
10
+ import run from './run.js';
11
+
12
+ export default function createBridgeFile(headerOrModuleFilePath, target = state.targets.find((t) => t.platform === 'wasm')) {
13
+ const interfaceFilePath = upath.resolve(headerOrModuleFilePath);
14
+ if (!fs.existsSync(`${state.config.paths.build}/interface`)) {
15
+ fs.mkdirSync(`${state.config.paths.build}/interface`, { recursive: true });
16
+ }
17
+ if (!fs.existsSync(`${state.config.paths.build}/bridge`)) {
18
+ fs.mkdirSync(`${state.config.paths.build}/bridge`, { recursive: true });
19
+ }
20
+ const interfaceFile = createInterfaceFile(interfaceFilePath, target);
21
+ // The header may live outside paths.native (e.g. a package-subpath import like
22
+ // '@scope/pkg/native/x.h'), so its own directory must reach swig's include list.
23
+ const bridgeFile = createBridgeFileFromInterfaceFile(interfaceFile, target, upath.dirname(interfaceFilePath));
24
+ const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
25
+ if (bridgeFile && !moduleRegex.test(interfaceFilePath)) {
26
+ // SWIG's -embind backend emits no member variables; inject .property lines for the
27
+ // header's public value fields. Idempotent, so the cached-bridge path is safe too.
28
+ const bridgeContent = fs.readFileSync(bridgeFile, 'utf8');
29
+ const injected = injectFieldBindings(bridgeContent, parseCppSurface(fs.readFileSync(interfaceFilePath, 'utf8'), () => {}));
30
+ if (injected !== bridgeContent) fs.writeFileSync(bridgeFile, injected);
31
+ }
32
+ if (bridgeFile) {
33
+ // Records which header this bridge came from, so directory-driven consumers
34
+ // (getAllBridges) can prune bridges whose header no longer exists.
35
+ writeIfChanged(`${bridgeFile}.source`, `${interfaceFilePath}\n`);
36
+ writeHeaderDts({
37
+ headerFile: interfaceFilePath,
38
+ exportsFile: `${bridgeFile}.exports.json`,
39
+ projectPath: state.config.paths.project,
40
+ cacheDir: state.config.paths.cache,
41
+ dtsMode: state.config.dts,
42
+ });
43
+ }
44
+ return bridgeFile;
45
+ }
46
+
47
+ function createInterfaceFile(headerOrModuleFilePath, target) {
48
+ if (!headerOrModuleFilePath) {
49
+ return null;
50
+ }
51
+ const fileHash = getFileHash(headerOrModuleFilePath);
52
+ const cachedInterface = state.cache.interfaces[headerOrModuleFilePath];
53
+ if (state.cache.hashes[headerOrModuleFilePath] === fileHash && cachedInterface && fs.existsSync(cachedInterface)) {
54
+ return cachedInterface;
55
+ }
56
+
57
+ const moduleRegex = new RegExp(`.(${state.config.ext.module.join('|')})$`);
58
+ const isModule = moduleRegex.test(headerOrModuleFilePath);
59
+ if (isModule) {
60
+ const newPath = `${state.config.paths.build}/interface/${headerOrModuleFilePath.split('/').pop()}`;
61
+ fs.copyFileSync(headerOrModuleFilePath, newPath);
62
+ state.cache.interfaces[headerOrModuleFilePath] = newPath;
63
+ state.cache.hashes[headerOrModuleFilePath] = fileHash;
64
+ saveCache();
65
+ return newPath;
66
+ }
67
+
68
+ const headerPaths = (state.config.dependencyParameters?.getCmakeDependsPathAndName(target).pathsOfCmakeDepends || [])
69
+ .filter((d) => d.startsWith(state.config.paths.base));
70
+
71
+ const temp2 = headerPaths
72
+ .map((p) => headerOrModuleFilePath.match(new RegExp(`^${p}/.*?/include/(.*?)$`, 'i')))
73
+ .filter((p) => p && p.length === 2);
74
+
75
+ const temp = headerOrModuleFilePath.match(/^(.*)\..+?$/);
76
+ if (!temp || temp.length < 2) return null;
77
+
78
+ const filePathWithoutExt = temp[1];
79
+ const interfaceFile = `${filePathWithoutExt}.i`;
80
+
81
+ if (fs.existsSync(interfaceFile)) {
82
+ const newPath = `${state.config.paths.build}/interface/${interfaceFile.split('/').at(-1)}`;
83
+ fs.copyFileSync(interfaceFile, newPath);
84
+ state.cache.interfaces[headerOrModuleFilePath] = newPath;
85
+ state.cache.hashes[headerOrModuleFilePath] = fileHash;
86
+ saveCache();
87
+ return newPath;
88
+ }
89
+
90
+ const fileName = filePathWithoutExt.split('/').at(-1);
91
+
92
+ let headerPath = state.config.paths.header.find((path) => headerOrModuleFilePath.startsWith(path));
93
+ if (headerPath) headerPath = headerOrModuleFilePath.substr(headerPath.length + 1);
94
+ else if (temp2 && temp2.length > 0) headerPath = temp2[0][1];
95
+ else headerPath = headerOrModuleFilePath.split('/').at(-1);
96
+
97
+ const content = `#ifndef _${fileName.toUpperCase()}_I
98
+ #define _${fileName.toUpperCase()}_I
99
+
100
+ %module ${fileName.toUpperCase()}
101
+
102
+ %{
103
+ #include "${headerPath}"
104
+ %}
105
+
106
+ %feature("shared_ptr");
107
+ %feature("polymorphic_shared_ptr");
108
+
109
+ %include "${headerPath}"
110
+
111
+ #endif
112
+ `;
113
+ const outputFilePath = `${state.config.paths.build}/interface/${fileName}.i`;
114
+ fs.writeFileSync(outputFilePath, content);
115
+
116
+ state.cache.interfaces[headerOrModuleFilePath] = outputFilePath;
117
+ state.cache.hashes[headerOrModuleFilePath] = fileHash;
118
+ saveCache();
119
+
120
+ return outputFilePath;
121
+ }
122
+
123
+ // Idempotent: wraps every emscripten::async() registration in the generated
124
+ // bridge behind #ifdef CROSSBIND_JSPI (see utils/bridgeAsyncGuard.js). Applied on
125
+ // the cached path too, so bridges generated by older versions get guarded the
126
+ // next time they are picked up.
127
+ function applyAsyncGuard(bridgeFilePath) {
128
+ const bridgeText = fs.readFileSync(bridgeFilePath, { encoding: 'utf8' });
129
+ const guarded = guardAsyncBindings(bridgeText);
130
+ if (guarded !== bridgeText) {
131
+ fs.writeFileSync(bridgeFilePath, guarded);
132
+ }
133
+ }
134
+
135
+ function createBridgeFileFromInterfaceFile(interfaceFilePath, target, sourceDir = null) {
136
+ if (!interfaceFilePath) {
137
+ return null;
138
+ }
139
+
140
+ const fileHash = getFileHash(interfaceFilePath);
141
+ const cachedBridge = state.cache.bridges[interfaceFilePath];
142
+ if (state.cache.hashes[interfaceFilePath] === fileHash && cachedBridge && fs.existsSync(cachedBridge)) {
143
+ applyAsyncGuard(cachedBridge);
144
+ return cachedBridge;
145
+ }
146
+
147
+ const allHeaders = state.config.dependencyParameters.headerPathWithDepends.split(';');
148
+
149
+ let includePath = [
150
+ ...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/include`),
151
+ ...state.config.allDependencies.map((d) => `${d.paths.output}/prebuilt/${target.path}/swig`),
152
+ ...state.config.paths.header,
153
+ ...allHeaders,
154
+ ...(sourceDir ? [sourceDir] : []),
155
+ ].filter((path) => !!path.toString()).map((path) => `-I${path}`);
156
+ includePath = [...new Set(includePath)];
157
+
158
+ run('swig', [
159
+ '-c++',
160
+ '-embind',
161
+ '-o', `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`,
162
+ ...includePath,
163
+ interfaceFilePath,
164
+ ], null, target);
165
+ applyAsyncGuard(`${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`);
166
+
167
+ state.cache.bridges[interfaceFilePath] = `${state.config.paths.build}/bridge/${interfaceFilePath.split('/').at(-1)}.cpp`;
168
+ state.cache.hashes[interfaceFilePath] = fileHash;
169
+ saveCache();
170
+
171
+ return state.cache.bridges[interfaceFilePath];
172
+ }