crossbind 2.0.0-beta.62 → 2.0.0-beta.63

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 (107) hide show
  1. package/README.md +2 -2
  2. package/package.json +6 -1
  3. package/src/actions/buildDependencies.js +15 -2
  4. package/src/actions/buildExternal.js +9 -1
  5. package/src/actions/buildLib.js +25 -8
  6. package/src/actions/buildNativeCommand.js +84 -0
  7. package/src/actions/buildNode.js +48 -7
  8. package/src/actions/buildTypes.js +8 -2
  9. package/src/actions/buildWasiCommand.js +0 -4
  10. package/src/actions/buildWasm.js +49 -29
  11. package/src/actions/cloudCommands.js +133 -0
  12. package/src/actions/createImportedBridges.js +64 -0
  13. package/src/actions/createInterface.js +247 -68
  14. package/src/actions/createLib.js +57 -41
  15. package/src/actions/getData.js +4 -1
  16. package/src/actions/getLinkInputs.js +12 -5
  17. package/src/actions/isSourceNewer.js +2 -0
  18. package/src/actions/licenses.js +143 -19
  19. package/src/actions/prepareConanDependencies.js +51 -0
  20. package/src/actions/publishBinary.js +32 -0
  21. package/src/actions/run.js +63 -66
  22. package/src/actions/runnerCommands.js +421 -0
  23. package/src/actions/writeRuntimeEntry.js +69 -0
  24. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  25. package/src/assets/cmake/command/command.cpp +1 -0
  26. package/src/assets/cmake/dist.cmake +2 -1
  27. package/src/assets/configure/install.sh +22 -0
  28. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  29. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  30. package/src/assets/js-runtime/adapters/worker-comlink.js +87 -60
  31. package/src/assets/js-runtime/core.js +11 -0
  32. package/src/assets/node-import-hooks.mjs +61 -0
  33. package/src/assets/toolchain-digests.json +19 -19
  34. package/src/assets/wasi-runtime/stubs.c +29 -0
  35. package/src/bin.js +257 -50
  36. package/src/index.js +3 -0
  37. package/src/integration/bindHeaderImports.js +18 -0
  38. package/src/integration/getCrossbindScript.js +46 -10
  39. package/src/integration/getDependFilePath.js +9 -0
  40. package/src/integration/resolveNativeImport.js +14 -0
  41. package/src/node/dev.mjs +19 -0
  42. package/src/runner/blobs.js +135 -0
  43. package/src/runner/files.js +193 -0
  44. package/src/runner/server.js +320 -0
  45. package/src/state/attachConanDependencies.js +56 -0
  46. package/src/state/index.js +17 -9
  47. package/src/state/loadConfig.js +33 -24
  48. package/src/state/refreshBuiltDependencies.js +20 -0
  49. package/src/state/refreshConanDependencies.js +17 -0
  50. package/src/utils/androidToolchain.js +4 -0
  51. package/src/utils/appRustCrates.js +15 -1
  52. package/src/utils/appSources.js +97 -6
  53. package/src/utils/archiveFlags.js +12 -0
  54. package/src/utils/boundHeaders.js +79 -0
  55. package/src/utils/bridgeExtras.js +486 -0
  56. package/src/utils/bridgeRuntimeFixes.js +48 -0
  57. package/src/utils/bundledLicenses.js +26 -0
  58. package/src/utils/cargoTarget.js +21 -3
  59. package/src/utils/cloudAccount.js +55 -0
  60. package/src/utils/conanDependencies.js +66 -0
  61. package/src/utils/conanImport.js +58 -0
  62. package/src/utils/conanInstall.js +154 -0
  63. package/src/utils/conanProfile.js +203 -0
  64. package/src/utils/conanStage.js +292 -0
  65. package/src/utils/copyToDistSource.js +13 -0
  66. package/src/utils/cppDts.js +58 -15
  67. package/src/utils/darwinToolchain.js +11 -0
  68. package/src/utils/dirLock.js +137 -28
  69. package/src/utils/downloadAndExtractFile.js +34 -7
  70. package/src/utils/embindArgumentGuards.js +41 -2
  71. package/src/utils/embindCallArguments.js +19 -0
  72. package/src/utils/embindJspiQueue.js +25 -0
  73. package/src/utils/execContainer.js +49 -0
  74. package/src/utils/headerImports.js +16 -5
  75. package/src/utils/hostPlatform.js +21 -0
  76. package/src/utils/iosToolchain.js +6 -0
  77. package/src/utils/isSourceCmakePackage.js +10 -0
  78. package/src/utils/licenseReport.js +11 -2
  79. package/src/utils/linuxToolchain.js +24 -0
  80. package/src/utils/makeTreeWritable.js +12 -0
  81. package/src/utils/nativeImports.js +23 -0
  82. package/src/utils/nativeSourceStamp.js +36 -0
  83. package/src/utils/nodeDevBuild.js +92 -0
  84. package/src/utils/packageLicense.js +101 -0
  85. package/src/utils/pthreadMainScript.js +14 -0
  86. package/src/utils/pullDockerImage.js +3 -1
  87. package/src/utils/relocatePrebuilt.js +119 -0
  88. package/src/utils/remoteClient.js +127 -0
  89. package/src/utils/remoteOutputs.js +39 -0
  90. package/src/utils/remoteRunner.js +97 -0
  91. package/src/utils/remoteUpload.js +134 -0
  92. package/src/utils/replaceFile.js +8 -0
  93. package/src/utils/runCargo.js +48 -51
  94. package/src/utils/runConan.js +257 -0
  95. package/src/utils/runtimeEntries.js +56 -0
  96. package/src/utils/rustBridgeGen.js +9 -4
  97. package/src/utils/rustMt.js +4 -0
  98. package/src/utils/scopedEmbind.js +22 -0
  99. package/src/utils/selectRunner.js +81 -0
  100. package/src/utils/sourceFingerprint.js +4 -3
  101. package/src/utils/swigInterface.js +69 -4
  102. package/src/utils/systemKeys.js +35 -3
  103. package/src/utils/targets.js +49 -4
  104. package/src/utils/toolchainNotices.js +5 -0
  105. package/src/utils/wasiToolchain.js +14 -1
  106. package/src/utils/windowsToolchain.js +18 -0
  107. package/vitest.config.js +11 -0
package/src/bin.js CHANGED
@@ -9,14 +9,23 @@ import createBridgeFile from './actions/createInterface.js';
9
9
  import createLib from './actions/createLib.js';
10
10
  import buildWasm from './actions/buildWasm.js';
11
11
  import buildWasiCommand from './actions/buildWasiCommand.js';
12
- import buildNode, { publishNodeData } from './actions/buildNode.js';
12
+ import buildNode, { addonsInPackages, bundleNodeLoader, publishNodeData } from './actions/buildNode.js';
13
+ import buildNativeCommand from './actions/buildNativeCommand.js';
13
14
  import buildExternal from './actions/buildExternal.js';
14
15
  import buildPackageTypes from './actions/buildTypes.js';
15
16
  import buildLib from './actions/buildLib.js';
16
17
  import buildDependencies from './actions/buildDependencies.js';
18
+ import writeRuntimeEntry, { removeImportHooks, writeImportHooks } from './actions/writeRuntimeEntry.js';
19
+ import { createImportedBridges, findAppNativeImports } from './actions/createImportedBridges.js';
20
+ import { nativeExtensions } from './utils/nativeImports.js';
21
+ import getDependFilePath from './integration/getDependFilePath.js';
22
+ import { boundHeaderSpecifiers, bridgeTargets, nodeBridgeTarget, resolveBoundHeaders } from './utils/boundHeaders.js';
23
+ import { ENTRY_RUNTIMES } from './utils/runtimeEntries.js';
24
+ import { publishNativeCommand, publishNodeAddon, publishWasiCommand } from './actions/publishBinary.js';
17
25
  import runCrossbindApp from './actions/run.js';
18
26
  import { getBuildTargets, getFilteredBuildTargets } from './actions/target.js';
19
- import { OPT_IN_PLATFORMS } from './utils/targets.js';
27
+ import { OPT_IN_PLATFORMS, selectRuntimeEnvs } from './utils/targets.js';
28
+ import { resolveHostPlatform } from './utils/hostPlatform.js';
20
29
  import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from './utils/resolveEmbindNapi.js';
21
30
 
22
31
  import writeJson from './utils/writeJson.js';
@@ -24,10 +33,19 @@ import flattenConfigForTable from './utils/flattenConfigForTable.js';
24
33
  import systemKeys from './utils/systemKeys.js';
25
34
  import logger from './utils/logger.js';
26
35
  import { getDockerImage, getDockerContainerName } from './utils/pullDockerImage.js';
36
+ import {
37
+ RUNNER_PLATFORMS, RUNNER_ROLES, connectCommands, deploySteps, initRunner, newRunnerToken, runnerHostPort, startRunner, stopRunner,
38
+ } from './actions/runnerCommands.js';
39
+ import {
40
+ deleteAccount as deleteCloudAccount, fetchUsage, login as loginToCloud, logout as logoutOfCloud, usageLines,
41
+ } from './actions/cloudCommands.js';
42
+ import { cloudUrl } from './utils/cloudAccount.js';
27
43
  import { cargoHome } from './utils/runCargo.js';
44
+ import { conanRoot } from './utils/runConan.js';
28
45
  import { cleanDepsCache } from './utils/dependencyRebuild.js';
29
46
  import collectLicenseRows from './actions/licenses.js';
30
47
  import { formatNoticesMarkdown, formatCycloneDxSbom, validateSpdx } from './utils/licenseReport.js';
48
+ import { SBOM_FILE, nodePackageMeta, writeNodePackageLicense } from './utils/packageLicense.js';
31
49
  import findFiles from './utils/findFiles.js';
32
50
 
33
51
  const packageJSON = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
@@ -63,11 +81,11 @@ program
63
81
 
64
82
  program.command('build')
65
83
  .description('compile the project that was set up using crossbind')
66
- .addOption(new Option('-p, --platform <platform>', 'target platform').argParser(createListParser(platforms)))
84
+ .addOption(new Option('-p, --platform <platform>', 'target platform (host: the one of this machine)').argParser(createListParser(platforms, resolveHostPlatform)))
67
85
  .addOption(new Option('-a, --arch <arch>', 'target architecture').argParser(createListParser(archs)))
68
86
  .addOption(new Option('-r, --runtime <runtime>', 'target runtime').argParser(createListParser(runtimes)))
69
87
  .addOption(new Option('-b, --build-type <buildType>', 'target build type').argParser(createListParser(buildTypes)))
70
- .addOption(new Option('-e, --runtime-env <runtimeEnv>', 'target runtime environment').argParser(createListParser(runtimeEnvs)))
88
+ .addOption(new Option('-e, --runtime-env <runtimeEnv>', 'binaries to make, by runtime environment (without it, only the archives are built)').argParser(createListParser(runtimeEnvs)))
71
89
  .option('--rebuild-deps [list]', 'rebuild dependencies from source instead of using prebuilt (all, or a comma-separated list of names)')
72
90
  .action((options) => {
73
91
  const { rebuildDeps, ...rest } = options;
@@ -84,12 +102,14 @@ program.command('build')
84
102
  targetParams.arch = targetParams.arch || archs;
85
103
  targetParams.runtime = targetParams.runtime || runtimes;
86
104
  targetParams.buildType = targetParams.buildType || buildTypes;
87
- targetParams.runtimeEnv = targetParams.runtimeEnv || runtimeEnvs;
105
+ const binaryRuntimeEnvs = selectRuntimeEnvs(targetParams.runtimeEnv, state.config.target.runtimeEnv);
106
+ // The archives do not depend on the runtime environment; only the binaries do.
107
+ targetParams.runtimeEnv = runtimeEnvs;
88
108
 
89
109
  if (state.config.build.withBuildConfig) {
90
110
  buildExternal(targetParams);
91
111
  } else {
92
- build(targetParams, rebuildDeps);
112
+ build(targetParams, rebuildDeps, binaryRuntimeEnvs);
93
113
  }
94
114
  });
95
115
 
@@ -110,8 +130,14 @@ program.command('licenses')
110
130
  .option('--sbom [file]', 'write a CycloneDX SBOM json file (default: sbom.cdx.json)')
111
131
  .option('--check', 'exit non-zero when a license field is missing or not valid SPDX')
112
132
  .option('--platform <platform>', "also list what this platform's artifact statically links beyond the package graph (vendored copies, toolchain runtime)")
133
+ .addOption(new Option('-e, --runtime-env <runtimeEnv>', 'also list the crossbind runtime an addon of --platform compiles in, or without --platform the one its loader bundles').choices(['node']))
134
+ .option('--json [file]', 'write the rows, license texts included, as JSON (default: licenses.json)')
135
+ .option('--package', "write this Node-API package's LICENSE, sbom.cdx.json and package.json license field (with -e node)")
113
136
  .action(async (options) => {
114
- const rows = await collectLicenseRows(options.platform || null);
137
+ if (options.package && options.runtimeEnv !== 'node') {
138
+ throw new Error('crossbind licenses --package writes the license files of a Node-API package; pass -e node.');
139
+ }
140
+ const rows = await collectLicenseRows(options.platform || null, { runtimeEnv: options.runtimeEnv || null });
115
141
  rows.forEach((row) => {
116
142
  console.log(`${row.isCopyleft ? '! ' : ' '}${(row.name || '').padEnd(14)} ${String(row.license || '(missing)').padEnd(48)} native ${String(row.nativeVersion || '-').padEnd(10)} ${row.sourceUrl || ''}`);
117
143
  });
@@ -136,6 +162,15 @@ program.command('licenses')
136
162
  fs.writeFileSync(file, formatCycloneDxSbom(rows, target));
137
163
  logger.info(`crossbind: wrote ${file}`);
138
164
  }
165
+ if (options.json) {
166
+ const file = typeof options.json === 'string' ? options.json : `${state.config.paths.project}/licenses.json`;
167
+ fs.writeFileSync(file, `${JSON.stringify(rows, null, 2)}\n`);
168
+ logger.info(`crossbind: wrote ${file}`);
169
+ }
170
+ if (options.package) {
171
+ const expression = writeNodePackageLicense(state.config.paths.project, { ...nodePackageMeta(state.config), rows });
172
+ logger.info(`crossbind: wrote LICENSE and ${SBOM_FILE} (${expression})`);
173
+ }
139
174
  if (options.check) {
140
175
  const invalid = rows
141
176
  .map((row) => ({ row, result: validateSpdx(row.license) }))
@@ -197,13 +232,17 @@ imageArgument(commandDocker.command('create').description('create docker contain
197
232
  .action((image) => {
198
233
  // Google ships the linux NDK for x86_64 only, so an android container is always amd64.
199
234
  const platform = image === 'android' ? 'linux/amd64' : undefined;
235
+ // Docker on Linux makes a missing mount source owned by root, which the user's builds cannot write.
236
+ [cargoHome(), conanRoot()].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
200
237
  dockerExec([
201
238
  'run', '-dit',
202
239
  ...(platform ? ['--platform', platform] : []),
203
240
  '--name', dockerContainerName(image),
204
241
  '-v', `${state.config.paths.base}:/tmp/crossbind/live`,
205
- // Cargo's registry cache, shared with host builds and with every other project.
242
+ // Cargo's registry cache, shared with host builds and with every other project, and the
243
+ // Conan store and work directories every other container build uses.
206
244
  '-v', `${cargoHome()}:/var/cache/crossbind/cargo`,
245
+ '-v', `${conanRoot()}:/var/cache/crossbind/conan`,
207
246
  getDockerImage(image, platform),
208
247
  'bash',
209
248
  ]);
@@ -218,6 +257,98 @@ imageArgument(commandDocker.command('stop').description('stop docker container')
218
257
  imageArgument(commandDocker.command('delete').description('delete docker container'))
219
258
  .action((image) => dockerExec(['rm', dockerContainerName(image)]));
220
259
 
260
+ const runnerAction = (fn) => (...args) => {
261
+ try {
262
+ fn(...args);
263
+ } catch (e) {
264
+ console.error(e.message);
265
+ process.exit(1);
266
+ }
267
+ };
268
+ const roleOption = () => new Option('--role <role>', 'toolchain image the runner serves').choices(RUNNER_ROLES).default('web');
269
+
270
+ const commandRunner = program.command('runner')
271
+ .description('run the remote build runner that RUNNER=REMOTE sends builds to');
272
+
273
+ commandRunner.command('start')
274
+ .description('start a runner in local docker, from the toolchain image this CLI pins')
275
+ .addOption(roleOption())
276
+ .option('--port <port>', `host port (default: ${RUNNER_ROLES.map((role) => `${runnerHostPort(role)} for ${role}`).join(', ')})`)
277
+ .option('--host <address>', 'host address to listen on', '127.0.0.1')
278
+ .option('--token <token>', 'token builds must send (default: $CROSSBIND_RUNNER_TOKEN, else a new one)')
279
+ .action(runnerAction((options) => {
280
+ const runner = startRunner({
281
+ role: options.role, port: options.port && Number(options.port), host: options.host, token: options.token,
282
+ });
283
+ console.log(`crossbind: ${runner.name} is running. Build against it with:`);
284
+ connectCommands(options.role, runner.url, runner.displayToken).forEach((line) => console.log(` ${line}`));
285
+ }));
286
+
287
+ commandRunner.command('stop')
288
+ .description('stop and remove the local runner')
289
+ .addOption(roleOption())
290
+ .action(runnerAction((options) => stopRunner({ role: options.role })));
291
+
292
+ commandRunner.command('init')
293
+ .description('write a folder that deploys a runner to a hosting platform')
294
+ .argument('<platform>', `hosting platform (${RUNNER_PLATFORMS.join(', ')})`)
295
+ .addOption(roleOption())
296
+ .option('--dir <dir>', 'folder to write (default: crossbind-runner-<platform>-<role>)')
297
+ .option('--vcpu <count>', 'cloudflare: vCPUs of the container (1-4), with the least memory Cloudflare allows them (default: standard-4)')
298
+ .action(runnerAction((platform, options) => {
299
+ const { dir, url } = initRunner({
300
+ platform,
301
+ role: options.role,
302
+ dir: options.dir ?? `crossbind-runner-${platform}-${options.role}`,
303
+ vcpu: options.vcpu === undefined ? undefined : Number(options.vcpu),
304
+ });
305
+ console.log(`crossbind: wrote ${dir}. Deploy it:`);
306
+ deploySteps(platform, dir, newRunnerToken(), options.role, url).forEach((step) => console.log(` ${step}`));
307
+ }));
308
+
309
+ program.command('login')
310
+ .description('sign in to crossbind cloud with your GitHub account')
311
+ .action(async () => {
312
+ const url = cloudUrl(state.config.system);
313
+ const { login } = await loginToCloud({ url });
314
+ console.log(`crossbind: signed in to ${url} as ${login}.`);
315
+ });
316
+
317
+ program.command('logout')
318
+ .description('sign out of crossbind cloud on this machine')
319
+ .option('--all', 'sign out every machine signed in to this account')
320
+ .action(async (options) => {
321
+ const url = cloudUrl(state.config.system);
322
+ const result = await logoutOfCloud({ url, all: Boolean(options.all) });
323
+ if (!result) {
324
+ console.log(`crossbind: not signed in to ${url}.`);
325
+ } else if (result.revoked) {
326
+ console.log(`crossbind: signed ${result.login} out${options.all ? ' on every machine' : ''}.`);
327
+ } else {
328
+ console.warn(`crossbind: signed out here, but crossbind cloud did not revoke the token (${result.reason.replace(/^crossbind: /, '')}). Sign in again and run crossbind logout --all to revoke it.`);
329
+ }
330
+ });
331
+
332
+ program.command('usage')
333
+ .description('show what you used of crossbind cloud this month and what is left')
334
+ .action(async () => {
335
+ usageLines(await fetchUsage({ url: cloudUrl(state.config.system) })).forEach((line) => console.log(line));
336
+ });
337
+
338
+ program.command('account')
339
+ .description('manage your crossbind cloud account')
340
+ .command('delete')
341
+ .description('delete your crossbind cloud account, with the sign-in of every machine (https://crossbind.dev/privacy/)')
342
+ .option('--yes', 'delete it; without this, nothing is deleted')
343
+ .action(async (options) => {
344
+ const url = cloudUrl(state.config.system);
345
+ if (!options.yes) {
346
+ throw new Error(`crossbind: this deletes your crossbind cloud account at ${url}, with the sign-in of every machine. Run crossbind account delete --yes to go ahead.`);
347
+ }
348
+ const { login } = await deleteCloudAccount({ url });
349
+ console.log(`crossbind: deleted the crossbind cloud account of ${login}.`);
350
+ });
351
+
221
352
  const commandConfig = program.command('config')
222
353
  .description('manage the crossbind configuration files');
223
354
 
@@ -322,15 +453,44 @@ function run(programName, params) {
322
453
  runCrossbindApp(programName, params, null, null, { console: true });
323
454
  }
324
455
 
325
- async function build(targetParams, rebuildOption) {
456
+ // A package whose platform packages link its addons, building nothing else, has no archive of its own that
457
+ // anything links.
458
+ function onlyLoadsPackagedAddons(binaryParams) {
459
+ if (binaryParams.runtimeEnv.length === 0) return false;
460
+ const targets = getBuildTargets(binaryParams);
461
+ return targets.every((target) => target.runtimeEnv === 'node' && target.platform !== 'wasm') && addonsInPackages();
462
+ }
463
+
464
+ async function build(targetParams, rebuildOption, binaryRuntimeEnvs) {
326
465
  await buildDependencies({ targetParams, rebuildOption });
327
- buildLib(targetParams);
328
- await createWasmJs(targetParams);
329
- await createNodeAddons(targetParams);
330
- await createWasiCommands(targetParams);
466
+ const binaryParams = { ...targetParams, runtimeEnv: binaryRuntimeEnvs };
467
+ if (!onlyLoadsPackagedAddons(binaryParams)) {
468
+ buildLib(targetParams);
469
+ }
470
+ if (binaryRuntimeEnvs.length === 0) {
471
+ logger.info(`crossbind: no runtime environment was selected, so only the archives were built - pass -e (${runtimeEnvs.join(', ')}) for a binary.`);
472
+ } else {
473
+ await createWasmJs(binaryParams);
474
+ await createNodeAddons(binaryParams);
475
+ await createNativeCommands(binaryParams);
476
+ await createWasiCommands(binaryParams);
477
+ }
331
478
  buildPackageTypes();
332
479
  }
333
480
 
481
+ async function createNativeCommands(targetParams) {
482
+ const targets = getBuildTargets(targetParams).filter((target) => target.runtimeEnv === 'native');
483
+ for (const target of targets) {
484
+ const distCommand = `${state.config.paths.output}/${target.commandName}`;
485
+ const built = await buildNativeCommand(target, { force: !fs.existsSync(distCommand) });
486
+ publishNodeData(target, { refresh: built });
487
+ if (!built) {
488
+ continue;
489
+ }
490
+ publishNativeCommand(target);
491
+ }
492
+ }
493
+
334
494
  async function createWasiCommands(targetParams) {
335
495
  const targets = getFilteredBuildTargets(targetParams, { platform: 'wasi' });
336
496
  if (targets.length === 0) {
@@ -342,35 +502,38 @@ async function createWasiCommands(targetParams) {
342
502
  if (!built) {
343
503
  continue;
344
504
  }
345
- fs.copyFileSync(`${state.config.paths.build}/${target.wasmName}`, distWasm);
346
- // Copied (not renamed) so a later fingerprint cache-hit still has the
347
- // build-dir tree to serve from.
348
- if (fs.existsSync(`${state.config.paths.build}/data`)) {
349
- fs.rmSync(`${state.config.paths.output}/data`, { recursive: true, force: true });
350
- fs.cpSync(`${state.config.paths.build}/data`, `${state.config.paths.output}/data`, { recursive: true });
351
- }
505
+ publishWasiCommand(target);
352
506
  }
353
507
  }
354
508
 
355
- function createBridges() {
509
+ function createBridges(target) {
356
510
  let headers = [];
357
511
  state.config.paths.header.forEach((headerPath) => {
358
512
  headers.push(findFiles('**/*.h', { cwd: headerPath }));
359
513
  });
360
514
  headers = headers.filter((path) => !!path.toString()).flat();
361
515
 
362
- const bridges = [];
363
- headers.forEach((header) => {
364
- const bridgePath = createBridgeFile(header);
365
- bridges.push(bridgePath);
366
- });
367
- return bridges;
516
+ return headers.map((header) => ({ file: header, bridge: createBridgeFile(header, target) }));
368
517
  }
369
518
 
370
- async function createNodeAddons(targetParams) {
371
- if (state.config.export.bundle === false) {
372
- return;
519
+ // The dependency headers the config binds whole, each read where one of the targets' packages ships it.
520
+ function createBoundBridges(targets) {
521
+ const headers = resolveBoundHeaders(boundHeaderSpecifiers(state.config), bridgeTargets(targets, state.targets), getDependFilePath);
522
+ const wholeHeaders = headers.map((header) => header.file);
523
+ return headers.map((header) => ({ ...header, bridge: createBridgeFile(header.file, header.target, { wholeHeaders }) }));
524
+ }
525
+
526
+ // A package whose platform packages link its addons ships what loads them: the loader and the data.
527
+ async function publishLoaders(targets) {
528
+ fs.mkdirSync(state.config.paths.output, { recursive: true });
529
+ targets.forEach((target) => publishNodeData(target, { refresh: true }));
530
+ for (const target of new Map(targets.map((t) => [t.jsName, t])).values()) {
531
+ await bundleNodeLoader(target);
532
+ fs.copyFileSync(`${state.config.paths.build}/${target.jsName}`, `${state.config.paths.output}/${target.jsName}`);
373
533
  }
534
+ }
535
+
536
+ async function createNodeAddons(targetParams) {
374
537
  const targets = getBuildTargets(targetParams)
375
538
  .filter((target) => target.runtimeEnv === 'node' && target.platform !== 'wasm');
376
539
  if (targets.length === 0) {
@@ -381,46 +544,72 @@ async function createNodeAddons(targetParams) {
381
544
  `${resolveEmbindJsiRoot()}/cpp/src`,
382
545
  `${resolveEmbindNapiRoot()}/third_party/node-api-jsi/jsi`,
383
546
  ];
547
+ const boundHeaders = createBoundBridges(targets);
548
+ const ownHeaders = [...createBridges(nodeBridgeTarget(targets)), ...boundHeaders];
549
+ const imports = findAppNativeImports(bridgeTargets(targets, state.targets));
550
+ const headers = [...ownHeaders, ...createImportedBridges(ownHeaders, imports)];
384
551
  const opt = {
385
552
  buildSource: false,
386
553
  nativeGlob: [
387
554
  `${state.config.paths.cli}/assets/cpp-runtime/commonBridges.cpp`,
388
- ...createBridges(),
555
+ ...headers.flatMap((header) => header.bridge ?? []),
389
556
  ],
390
557
  headerDirs,
391
558
  inputs: headerDirs.flatMap((dir) => findFiles('**/*.h', { cwd: dir })),
392
559
  };
393
560
 
394
- for (const target of targets) {
395
- const distComplete = fs.existsSync(`${state.config.paths.output}/${target.addonName}`)
396
- && fs.existsSync(`${state.config.paths.output}/${target.jsName}`);
397
- createLib(target, 'Bridge', opt);
398
- const built = await buildNode(target, { force: !distComplete });
399
- publishNodeData(target, { refresh: built });
400
- if (!built) {
401
- continue;
561
+ if (addonsInPackages()) {
562
+ await publishLoaders(targets);
563
+ } else {
564
+ for (const target of targets) {
565
+ const distComplete = fs.existsSync(`${state.config.paths.output}/${target.addonName}`)
566
+ && fs.existsSync(`${state.config.paths.output}/${target.jsName}`);
567
+ createLib(target, 'Bridge', opt);
568
+ const built = await buildNode(target, { force: !distComplete });
569
+ publishNodeData(target, { refresh: built });
570
+ if (!built) {
571
+ continue;
572
+ }
573
+ publishNodeAddon(target);
402
574
  }
575
+ }
576
+ const loaderTarget = targets.find((target) => target.buildType === 'release') ?? targets[0];
577
+ if (headers.length && fs.existsSync(`${state.config.paths.output}/${loaderTarget.jsName}`)) {
578
+ const entry = `${state.config.paths.output}/node/napi.mjs`;
579
+ writeRuntimeEntry(entry, headers, ENTRY_RUNTIMES.node(loaderTarget.jsName));
580
+ if (imports.size) writeImportHooks(entry, headers, imports, nativeExtensions(state.config.ext));
581
+ else removeImportHooks(entry);
582
+ }
583
+ }
584
+
585
+ // The Node and edge wasm builds are CommonJS with a .js extension, which a "type": "module" package loads as
586
+ // ESM. A package.json of the output's own scopes them back, unless the output is a package root.
587
+ const OUTPUT_SCOPE = '{\n "type": "commonjs"\n}\n';
588
+ function scopeOutputToCommonJs() {
589
+ const file = `${state.config.paths.output}/package.json`;
590
+ if (!fs.existsSync(file)) {
403
591
  fs.mkdirSync(state.config.paths.output, { recursive: true });
404
- fs.copyFileSync(`${state.config.paths.build}/${target.addonName}`, `${state.config.paths.output}/${target.addonName}`);
405
- fs.copyFileSync(`${state.config.paths.build}/${target.jsName}`, `${state.config.paths.output}/${target.jsName}`);
592
+ fs.writeFileSync(file, OUTPUT_SCOPE);
406
593
  }
407
594
  }
408
595
 
409
596
  async function createWasmJs(targetParams) {
410
- if (state.config.export.bundle === false) {
411
- return;
412
- }
413
597
  const targets = getFilteredBuildTargets(targetParams, { platform: 'wasm' });
414
598
  if (targets.length === 0) {
415
599
  return;
416
600
  }
417
- const opt = {
601
+ const headers = createBridges();
602
+ // A Node app imports what it uses through the module hooks, so a Node binary binds those imports too.
603
+ const isNode = (target) => target.runtimeEnv === 'node';
604
+ const imports = targets.some(isNode) ? findAppNativeImports([state.targets.find((t) => t.platform === 'wasm')]) : new Map();
605
+ const nodeHeaders = [...headers, ...createImportedBridges(headers, imports)];
606
+ const optOf = (bridged) => ({
418
607
  buildSource: false,
419
608
  nativeGlob: [
420
609
  `${state.config.paths.cli}/assets/cpp-runtime/commonBridges.cpp`,
421
- ...createBridges(),
610
+ ...bridged.flatMap((header) => header.bridge ?? []),
422
611
  ],
423
- };
612
+ });
424
613
 
425
614
  for (const target of targets) {
426
615
  // An artifact merely existing in dist used to short-circuit this whole
@@ -430,7 +619,7 @@ async function createWasmJs(targetParams) {
430
619
  // link and the copy step below moved build/data away on the last run.
431
620
  const distComplete = fs.existsSync(`${state.config.paths.output}/${target.jsName}`)
432
621
  && fs.existsSync(`${state.config.paths.output}/${target.wasmName}`);
433
- createLib(target, 'Bridge', opt);
622
+ createLib(target, 'Bridge', optOf(isNode(target) ? nodeHeaders : headers));
434
623
  const built = await buildWasm(target, { force: !distComplete });
435
624
  if (!built) {
436
625
  continue;
@@ -447,11 +636,29 @@ async function createWasmJs(targetParams) {
447
636
  fs.copyFileSync(`${state.config.paths.build}/${target.dataTxtName}`, `${state.config.paths.output}/${target.dataTxtName}`);
448
637
  }
449
638
  }
639
+
640
+ if (targets.some((target) => ['node', 'edge'].includes(target.runtimeEnv))) {
641
+ scopeOutputToCommonJs();
642
+ }
643
+ // A runtime's entry loads its single-thread wasm32 release build.
644
+ const entryTargetOf = (runtimeEnv) => targets.find((target) => target.runtimeEnv === runtimeEnv && target.arch === 'wasm32'
645
+ && target.runtime === 'st' && target.buildType === 'release' && fs.existsSync(`${state.config.paths.output}/${target.jsName}`));
646
+ const nodeTarget = entryTargetOf('node');
647
+ const edgeTarget = entryTargetOf('edge');
648
+ if (nodeHeaders.length && nodeTarget) {
649
+ const entry = `${state.config.paths.output}/node/wasm.mjs`;
650
+ writeRuntimeEntry(entry, nodeHeaders, ENTRY_RUNTIMES.node(nodeTarget.jsName));
651
+ if (imports.size) writeImportHooks(entry, nodeHeaders, imports, nativeExtensions(state.config.ext));
652
+ else removeImportHooks(entry);
653
+ }
654
+ if (headers.length && edgeTarget) {
655
+ writeRuntimeEntry(`${state.config.paths.output}/edge/wasm.mjs`, headers, ENTRY_RUNTIMES.edge(edgeTarget.jsName, edgeTarget.wasmName));
656
+ }
450
657
  }
451
658
 
452
- function createListParser(validList) {
659
+ function createListParser(validList, resolve = (items) => items) {
453
660
  return (value, previous) => {
454
- const items = value.split(',').map(item => item.trim());
661
+ const items = resolve(value.split(',').map(item => item.trim()));
455
662
  for (const item of items) {
456
663
  if (!validList.includes(item)) {
457
664
  throw new Error(`Invalid value: "${item}". Allowed values: ${validList.join(', ')}`);
package/src/index.js CHANGED
@@ -13,9 +13,12 @@ export { getTargetParams, getBuildTargets, getFilteredBuildTargets, getFilteredT
13
13
  export { default as getCrossbindScript, getRustJsScript } from './integration/getCrossbindScript.js';
14
14
  export { default as getDependFilePath } from './integration/getDependFilePath.js';
15
15
  export { default as buildDependencies, getDependenciesStamp } from './actions/buildDependencies.js';
16
+ export { default as prepareConanDependencies } from './actions/prepareConanDependencies.js';
16
17
  export { default as findFiles } from './utils/findFiles.js';
17
18
  export { getContentHash, getFileHash } from './utils/hash.js';
18
19
  export {
19
20
  collectInputFiles, collectRustBridgeFiles, collectRustSources, computeInputStamp,
20
21
  } from './utils/inputStamp.js';
21
22
  export { default as buildAppRustCrates } from './utils/appRustCrates.js';
23
+ export { default as appSourceFiles, isAppSource } from './utils/appSources.js';
24
+ export { default as bindHeaderImports } from './integration/bindHeaderImports.js';
@@ -0,0 +1,18 @@
1
+ import fs from 'node:fs';
2
+ import upath from 'upath';
3
+ import state from '../state/index.js';
4
+ import createBridgeFile from '../actions/createInterface.js';
5
+ import resolveNativeImport from './resolveNativeImport.js';
6
+ import { isAppSource } from '../utils/appSources.js';
7
+ import { findHeaderImports } from '../utils/headerImports.js';
8
+
9
+ // Metro keeps a header's module as it first transformed it, so a source of the app that imports the header binds it
10
+ // again: the functions it names reach the next native build or page load.
11
+ export default function bindHeaderImports(source, file, target) {
12
+ if (!isAppSource(state.config.paths.project, file)) return [];
13
+ const headers = findHeaderImports(source, state.config.ext.header)
14
+ .map(({ specifier }) => resolveNativeImport(specifier, file, target))
15
+ .filter((header) => header && fs.existsSync(header))
16
+ .map((header) => upath.normalize(header));
17
+ return [...new Set(headers)].map((header) => createBridgeFile(header, target));
18
+ }
@@ -5,18 +5,53 @@ import loadJson from '../utils/loadJson.js';
5
5
  import state from '../state/index.js';
6
6
  import { parseSurface, createRustBridgeCrate, createCrateImportBridge } from '../utils/rustBridgeGen.js';
7
7
  import { parseCargoMarkerName } from '../utils/cargoImport.js';
8
+ import { UNBOUND_MESSAGE } from '../assets/js-runtime/adapters/unboundMessage.js';
8
9
 
9
- export default function getCrossbindScript(target, bridgePath) {
10
+ // `base` is where the bundler serves the app from, as Vite's `base` names it; webpack's public path wins at run time.
11
+ // Metro asks for `liveExports`: it keeps a header's module as it first transformed it while the app's imports change.
12
+ export default function getCrossbindScript(target, bridgePath, { base = '/', liveExports: isLive = false } = {}) {
10
13
  if (!bridgePath) {
11
14
  throw new Error('getCrossbindScript needs the bridge file of the imported header');
12
15
  }
13
- return buildScript(target, loadJson(`${bridgePath}.exports.json`));
16
+ const script = buildScript(target, loadJson(`${bridgePath}.exports.json`), base);
17
+ return isLive ? `${script}${liveExportsScript()}` : script;
18
+ }
19
+
20
+ // A name the module does not export comes off the booted module: undefined before initNative. A name the module the page
21
+ // or app loaded lacks, exported or not, is a function that says why. Serialized into the generated module, so it
22
+ // references nothing outside itself.
23
+ export function liveExports(exportsObject, unbound) {
24
+ return new Proxy(exportsObject, {
25
+ get(target, name, receiver) {
26
+ const value = Reflect.get(target, name, receiver);
27
+ const bound = globalThis.__crossbindModule;
28
+ if (value != null || !bound || typeof name !== 'string' || ['then', 'default', 'toJSON'].includes(name)) return value;
29
+ if (bound[name] !== undefined) return bound[name];
30
+ if (!/^[A-Za-z_]\w*$/.test(name)) return value;
31
+ return function notBound() {
32
+ throw new Error(`crossbind: ${name} ${unbound}`);
33
+ };
34
+ },
35
+ });
36
+ }
37
+
38
+ function liveExportsScript() {
39
+ return `
40
+ const __crossbindLiveExports = ${liveExports.toString()};
41
+ if (typeof module === 'object' && module) module.exports = __crossbindLiveExports(module.exports, ${JSON.stringify(UNBOUND_MESSAGE)});
42
+ `;
14
43
  }
15
44
 
16
45
  // The Rust analog of a .h import: parse the crate surface and emit the same proxy module
17
46
  // (per-symbol lets assigned inside this module's initNative). Vectors come from the owning
18
47
  // cargo package's config; classes/enums come from the parsed source.
19
- export function getRustJsScript(target, rsFile) {
48
+ export function getRustJsScript(target, rsFile, { base = '/' } = {}) {
49
+ return buildScript(target, getRustSymbols(rsFile), base);
50
+ }
51
+
52
+ // The names a Rust import exports. An app-local file or a cargo: crate gets its bridge crate here, the way a header
53
+ // gets its bridge from createBridgeFile.
54
+ export function getRustSymbols(rsFile) {
20
55
  // Compare real paths: dependency paths go through node_modules symlinks (pnpm workspaces)
21
56
  // while bundlers hand the transformer the resolved real file.
22
57
  const realCrateDir = (d) => {
@@ -40,7 +75,7 @@ export function getRustJsScript(target, rsFile) {
40
75
  });
41
76
  // A crate registers its public names under per-crate names (two crates may export the
42
77
  // same name), so the proxy exports the clean name and reads the registered one off the module.
43
- return buildScript(target, exports);
78
+ return exports;
44
79
  }
45
80
 
46
81
  const pkg = state.config.allDependencies.find((d) => d.export?.type === 'cargo'
@@ -60,7 +95,7 @@ export function getRustJsScript(target, rsFile) {
60
95
  log: () => {},
61
96
  });
62
97
  }
63
- const symbols = [
98
+ return [
64
99
  ...model.classes.map((c) => c.name),
65
100
  ...(model.streams ?? []).map((s) => s.name),
66
101
  ...model.enums.map((e) => e.name),
@@ -69,10 +104,9 @@ export function getRustJsScript(target, rsFile) {
69
104
  ...(model.consts ?? []).map((c) => c.name),
70
105
  ...vectors.map((v) => v.name),
71
106
  ];
72
- return buildScript(target, symbols);
73
107
  }
74
108
 
75
- function buildScript(target, symbols) {
109
+ function buildScript(target, symbols, base) {
76
110
  if (!target) {
77
111
  throw new Error('The target is not available!');
78
112
  }
@@ -90,7 +124,7 @@ function buildScript(target, symbols) {
90
124
  }
91
125
 
92
126
  return `
93
- ${getPlatformScript(env)}
127
+ ${getPlatformScript(env, base)}
94
128
 
95
129
  export let AllSymbols = {};
96
130
  ${symbolExportDefineString}
@@ -165,8 +199,9 @@ function getReactNativeScript(env) {
165
199
  `;
166
200
  }
167
201
 
168
- function getWebScript(env) {
202
+ function getWebScript(env, base) {
169
203
  const params = `{
204
+ path: base,
170
205
  ...config,
171
206
  env: {...${env}, ...config.env},
172
207
  paths: {
@@ -179,7 +214,8 @@ function getWebScript(env) {
179
214
 
180
215
  return `
181
216
  function __crossbindBoot(config) {
182
- return import(/* webpackIgnore: true */ '/crossbind.js')
217
+ const base = new URL((typeof __webpack_public_path__ === 'string' ? __webpack_public_path__ : ${JSON.stringify(base)}) || './', document.baseURI).href;
218
+ return import(/* webpackIgnore: true */ /* @vite-ignore */ base + 'crossbind.js')
183
219
  .then(n => window.Crossbind.initNative(${params}));
184
220
  }
185
221
  `;
@@ -2,6 +2,8 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import state from '../state/index.js';
4
4
  import { parseCargoImport, writeCargoMarker } from '../utils/cargoImport.js';
5
+ import { CONAN_PREFIX, resolveConanHeader } from '../utils/conanImport.js';
6
+ import refreshConanDependencies from '../state/refreshConanDependencies.js';
5
7
 
6
8
  const LAYOUT = { header: 'include', module: 'swig', source: '' };
7
9
 
@@ -92,6 +94,13 @@ export default function getDependFilePath(source, target) {
92
94
  return marker;
93
95
  }
94
96
 
97
+ // `conan:<package>/<header>` names a header of a declared Conan package, staged per target like a
98
+ // port's prebuilt; from there the header takes the same path as any other.
99
+ if (source.startsWith(CONAN_PREFIX)) {
100
+ refreshConanDependencies(state.config);
101
+ return resolveConanHeader(state.config, source, target ?? state.targets.find((t) => t.platform === 'wasm'));
102
+ }
103
+
95
104
  const matches = getMatchingPackages(source);
96
105
  if (matches.length === 0) return null;
97
106
 
@@ -0,0 +1,14 @@
1
+ import { createRequire } from 'node:module';
2
+ import upath from 'upath';
3
+ import getDependFilePath from './getDependFilePath.js';
4
+
5
+ // A package that is not a crossbind dependency (the conformance kit) resolves the way the bundler finds it.
6
+ export default function resolveNativeImport(specifier, importer, target) {
7
+ if (specifier.startsWith('.')) return upath.resolve(upath.dirname(importer), specifier);
8
+ if (upath.isAbsolute(specifier)) return specifier;
9
+ try {
10
+ return getDependFilePath(specifier, target) ?? createRequire(importer).resolve(specifier);
11
+ } catch (e) {
12
+ return null;
13
+ }
14
+ }