crossbind 2.0.0-beta.63 → 2.0.0-beta.65

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 (77) hide show
  1. package/AGENTS.md +2 -1
  2. package/package.json +2 -2
  3. package/src/actions/androidDevice.js +77 -0
  4. package/src/actions/androidGradle.js +121 -0
  5. package/src/actions/buildApp.js +434 -0
  6. package/src/actions/buildBinTools.js +0 -0
  7. package/src/actions/buildDependencies.js +4 -3
  8. package/src/actions/buildExternal.js +3 -3
  9. package/src/actions/buildJs.js +2 -9
  10. package/src/actions/buildLib.js +1 -1
  11. package/src/actions/buildNode.js +19 -16
  12. package/src/actions/buildWasiCommand.js +10 -5
  13. package/src/actions/buildWasm.js +24 -3
  14. package/src/actions/createImportedBridges.js +7 -0
  15. package/src/actions/createInterface.js +4 -5
  16. package/src/actions/createLib.js +17 -2
  17. package/src/actions/getCmakeParameters.js +2 -1
  18. package/src/actions/getData.js +2 -1
  19. package/src/actions/getDependLibs.js +8 -2
  20. package/src/actions/getEnv.js +7 -0
  21. package/src/actions/getLinkInputs.js +10 -7
  22. package/src/actions/gradleProject.js +110 -0
  23. package/src/actions/iosSimulator.js +63 -0
  24. package/src/actions/licenses.js +160 -14
  25. package/src/actions/nativeBridges.js +35 -0
  26. package/src/actions/run.js +3 -3
  27. package/src/actions/runnerCommands.js +16 -4
  28. package/src/actions/target.js +2 -1
  29. package/src/actions/writeRuntimeEntry.js +18 -7
  30. package/src/actions/xcodeProject.js +152 -0
  31. package/src/assets/cpp-runtime/node.cpp +18 -1
  32. package/src/assets/js-runtime/adapters/worker-comlink.js +58 -0
  33. package/src/assets/js-runtime/core.js +5 -4
  34. package/src/assets/toolchain-digests.json +25 -19
  35. package/src/assets/wasi-runtime/http/README.md +17 -0
  36. package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
  37. package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
  38. package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
  39. package/src/assets/wasi-runtime/http/handler.cpp +298 -0
  40. package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
  41. package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
  42. package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
  43. package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
  44. package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
  45. package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
  46. package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
  47. package/src/bin.js +102 -209
  48. package/src/cli.js +14 -0
  49. package/src/index.js +6 -0
  50. package/src/integration/getCrossbindScript.js +19 -4
  51. package/src/program.js +169 -0
  52. package/src/state/index.js +8 -3
  53. package/src/state/loadConfig.js +6 -11
  54. package/src/utils/appRustCrates.js +7 -1
  55. package/src/utils/bridgeExtras.js +74 -6
  56. package/src/utils/cargoLock.js +19 -0
  57. package/src/utils/cppDts.js +2 -0
  58. package/src/utils/downloadAndExtractFile.js +42 -14
  59. package/src/utils/embindObjectLifetime.js +35 -0
  60. package/src/utils/embindRsFingerprint.js +10 -3
  61. package/src/utils/hermes.js +40 -0
  62. package/src/utils/packageLicense.js +4 -2
  63. package/src/utils/plist.js +42 -0
  64. package/src/utils/pullDockerImage.js +9 -3
  65. package/src/utils/replaceBasePathForDocker.js +8 -0
  66. package/src/utils/resetCMakeCacheForSource.js +26 -0
  67. package/src/utils/resolveAppHost.js +21 -0
  68. package/src/utils/rustBridgeGen.js +13 -4
  69. package/src/utils/rustMt.js +5 -0
  70. package/src/utils/rustSysroot.js +7 -5
  71. package/src/utils/selectRunner.js +1 -1
  72. package/src/utils/swigInterface.js +5 -4
  73. package/src/utils/systemConfig.js +13 -0
  74. package/src/utils/systemKeys.js +4 -0
  75. package/src/utils/targets.js +30 -3
  76. package/src/utils/wasiHttp.js +50 -0
  77. package/vitest.config.js +5 -0
package/src/bin.js CHANGED
@@ -1,25 +1,29 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  import fs from 'node:fs';
4
- import { execFileSync } from 'node:child_process';
5
- import { Command, Option } from 'commander';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { createRequire } from 'node:module';
7
+ import { execFileSync, spawn } from 'node:child_process';
8
+ import { Option } from 'commander';
6
9
 
7
10
  import { state } from './index.js';
8
- import createBridgeFile from './actions/createInterface.js';
11
+ import { addSystemCommands, createProgram, handleFatal } from './program.js';
9
12
  import createLib from './actions/createLib.js';
10
13
  import buildWasm from './actions/buildWasm.js';
11
14
  import buildWasiCommand from './actions/buildWasiCommand.js';
12
15
  import buildNode, { addonsInPackages, bundleNodeLoader, publishNodeData } from './actions/buildNode.js';
13
16
  import buildNativeCommand from './actions/buildNativeCommand.js';
17
+ import buildApp, { appOutputPath, assertAppRunner, publishApp } from './actions/buildApp.js';
18
+ import { releaseSigning } from './actions/androidGradle.js';
14
19
  import buildExternal from './actions/buildExternal.js';
15
20
  import buildPackageTypes from './actions/buildTypes.js';
16
21
  import buildLib from './actions/buildLib.js';
17
22
  import buildDependencies from './actions/buildDependencies.js';
18
- import writeRuntimeEntry, { removeImportHooks, writeImportHooks } from './actions/writeRuntimeEntry.js';
19
- import { createImportedBridges, findAppNativeImports } from './actions/createImportedBridges.js';
23
+ import writeRuntimeEntry, { removeImportHooks, writeImportHooks, writeImportManifest } from './actions/writeRuntimeEntry.js';
24
+ import { bindsAppImports, createImportedBridges, findAppNativeImports } from './actions/createImportedBridges.js';
20
25
  import { nativeExtensions } from './utils/nativeImports.js';
21
- import getDependFilePath from './integration/getDependFilePath.js';
22
- import { boundHeaderSpecifiers, bridgeTargets, nodeBridgeTarget, resolveBoundHeaders } from './utils/boundHeaders.js';
26
+ import { createBridges, createNativeBridges } from './actions/nativeBridges.js';
23
27
  import { ENTRY_RUNTIMES } from './utils/runtimeEntries.js';
24
28
  import { publishNativeCommand, publishNodeAddon, publishWasiCommand } from './actions/publishBinary.js';
25
29
  import runCrossbindApp from './actions/run.js';
@@ -28,18 +32,11 @@ import { OPT_IN_PLATFORMS, selectRuntimeEnvs } from './utils/targets.js';
28
32
  import { resolveHostPlatform } from './utils/hostPlatform.js';
29
33
  import resolveEmbindNapiRoot, { resolveEmbindJsiRoot } from './utils/resolveEmbindNapi.js';
30
34
 
31
- import writeJson from './utils/writeJson.js';
32
- import flattenConfigForTable from './utils/flattenConfigForTable.js';
33
- import systemKeys from './utils/systemKeys.js';
34
35
  import logger from './utils/logger.js';
35
36
  import { getDockerImage, getDockerContainerName } from './utils/pullDockerImage.js';
36
37
  import {
37
- RUNNER_PLATFORMS, RUNNER_ROLES, connectCommands, deploySteps, initRunner, newRunnerToken, runnerHostPort, startRunner, stopRunner,
38
+ RUNNER_PLATFORMS, RUNNER_ROLES, connectCommands, deleteRunner, deploySteps, initRunner, newRunnerToken, runnerHostPort, startRunner,
38
39
  } 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';
43
40
  import { cargoHome } from './utils/runCargo.js';
44
41
  import { conanRoot } from './utils/runConan.js';
45
42
  import { cleanDepsCache } from './utils/dependencyRebuild.js';
@@ -48,36 +45,16 @@ import { formatNoticesMarkdown, formatCycloneDxSbom, validateSpdx } from './util
48
45
  import { SBOM_FILE, nodePackageMeta, writeNodePackageLicense } from './utils/packageLicense.js';
49
46
  import findFiles from './utils/findFiles.js';
50
47
 
51
- const packageJSON = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url)));
52
-
53
48
  const platforms = [...new Set(state.targets.map(target => target.platform).filter(t => t))];
54
49
  const archs = [...new Set(state.targets.map(target => target.arch).filter(t => t))];
55
50
  const runtimes = [...new Set(state.targets.map(target => target.runtime).filter(t => t))];
56
51
  const buildTypes = [...new Set(state.targets.map(target => target.buildType).filter(t => t))];
57
52
  const runtimeEnvs = [...new Set(state.targets.map(target => target.runtimeEnv).filter(t => t))];
58
53
 
59
- // Build failures bubble up as exceptions with a `cause` chain (see actions/run.js);
60
- // show the operator a single line — DEBUG=1 for the full stack — and exit non-zero.
61
- const handleFatal = (e) => {
62
- if (process.env.DEBUG) {
63
- console.error(e);
64
- } else {
65
- let message = e?.message || String(e);
66
- if (e?.cause?.message) message += `\n caused by: ${e.cause.message.split('\n')[0]}`;
67
- console.error(message);
68
- }
69
- process.exit(1);
70
- };
71
54
  process.on('uncaughtException', handleFatal);
72
55
  process.on('unhandledRejection', handleFatal);
73
56
 
74
- const program = new Command();
75
-
76
- program
77
- .name('crossbind')
78
- .description('Compile C++ files to WebAssembly and native platforms.')
79
- .version(packageJSON.version)
80
- .showHelpAfterError();
57
+ const program = createProgram();
81
58
 
82
59
  program.command('build')
83
60
  .description('compile the project that was set up using crossbind')
@@ -87,8 +64,9 @@ program.command('build')
87
64
  .addOption(new Option('-b, --build-type <buildType>', 'target build type').argParser(createListParser(buildTypes)))
88
65
  .addOption(new Option('-e, --runtime-env <runtimeEnv>', 'binaries to make, by runtime environment (without it, only the archives are built)').argParser(createListParser(runtimeEnvs)))
89
66
  .option('--rebuild-deps [list]', 'rebuild dependencies from source instead of using prebuilt (all, or a comma-separated list of names)')
67
+ .option('--bind-imports', "also bind what the app's JavaScript imports into browser binaries (node binaries always do), for a page that resolves those imports itself")
90
68
  .action((options) => {
91
- const { rebuildDeps, ...rest } = options;
69
+ const { rebuildDeps, bindImports, ...rest } = options;
92
70
  const targetParams = { ...rest };
93
71
 
94
72
  if (!targetParams.arch) {
@@ -109,7 +87,8 @@ program.command('build')
109
87
  if (state.config.build.withBuildConfig) {
110
88
  buildExternal(targetParams);
111
89
  } else {
112
- build(targetParams, rebuildDeps, binaryRuntimeEnvs);
90
+ // An app builds only for the platforms -p names, so a plain build keeps making the archives alone.
91
+ build(targetParams, rebuildDeps, binaryRuntimeEnvs, { bindImports }, rest.platform ?? []);
113
92
  }
114
93
  });
115
94
 
@@ -124,13 +103,50 @@ function declaredSbomPath(projectDir) {
124
103
  }
125
104
  }
126
105
 
106
+ // An app's page is the project's Vite build, which @crossbind/plugin-vite makes into the app of the platform named.
107
+ const APP_PLATFORMS = ['android', 'ios', 'linux', 'macos', 'windows'];
108
+
109
+ function projectVite() {
110
+ try {
111
+ const manifest = createRequire(`${state.config.paths.project}/package.json`).resolve('vite/package.json');
112
+ return path.join(path.dirname(manifest), JSON.parse(fs.readFileSync(manifest, 'utf8')).bin.vite);
113
+ } catch {
114
+ throw new Error("crossbind: crossbind app builds the app's page with the project's Vite, which the project does not have. Add vite and @crossbind/plugin-vite to it, as the desktop-app template has them.");
115
+ }
116
+ }
117
+
118
+ const commandApp = program.command('app').description('build the project as an app, or open it on the dev server');
119
+ [
120
+ ['dev', 'open the app on the dev server, and open it anew as its native code changes'],
121
+ ['build', 'build the app with its page: for Android also its APK, and with an upload key its bundle'],
122
+ ].forEach(([name, description]) => commandApp.command(name).description(description)
123
+ .addOption(new Option('-p, --platform <platform>', "the app's platform (default: CROSSBIND_APP_PLATFORM, else this machine's)").choices(APP_PLATFORMS))
124
+ // Vite's own options, as --port or --mode, go on to Vite.
125
+ .allowUnknownOption()
126
+ .allowExcessArguments()
127
+ .action(({ platform }, command) => {
128
+ if (state.config.target.runtimeEnv !== 'app') {
129
+ throw new Error("crossbind: crossbind app builds an app, which this project is not: set target: { runtimeEnv: 'app' } in crossbind.config.js, as the desktop-app template does.");
130
+ }
131
+ // An Android build checks its upload key before the native build, not after it.
132
+ if (name === 'build' && (platform ?? process.env.CROSSBIND_APP_PLATFORM) === 'android') releaseSigning();
133
+ const vite = spawn(process.execPath, [projectVite(), ...(name === 'build' ? ['build'] : []), ...command.args], {
134
+ stdio: 'inherit',
135
+ cwd: state.config.paths.project,
136
+ env: platform ? { ...process.env, CROSSBIND_APP_PLATFORM: platform } : process.env,
137
+ });
138
+ // A signal sent to crossbind alone reaches Vite too, which ends through its exit hooks; Ctrl-C reaches both.
139
+ ['SIGINT', 'SIGTERM', 'SIGHUP'].forEach((signal) => process.prependListener(signal, () => vite.kill(signal)));
140
+ vite.on('exit', (code, signal) => process.exit(code ?? 128 + os.constants.signals[signal]));
141
+ }));
142
+
127
143
  program.command('licenses')
128
144
  .description('list bundled native dependencies with SPDX license, versions and source URL')
129
145
  .option('--notices [file]', 'write a THIRD-PARTY-NOTICES markdown file (default: THIRD-PARTY-NOTICES.md)')
130
146
  .option('--sbom [file]', 'write a CycloneDX SBOM json file (default: sbom.cdx.json)')
131
147
  .option('--check', 'exit non-zero when a license field is missing or not valid SPDX')
132
148
  .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']))
149
+ .addOption(new Option('-e, --runtime-env <runtimeEnv>', "also list the crossbind runtime an addon of --platform compiles in (without --platform the one its loader bundles), or what an app's host carries").choices(['node', 'app']))
134
150
  .option('--json [file]', 'write the rows, license texts included, as JSON (default: licenses.json)')
135
151
  .option('--package', "write this Node-API package's LICENSE, sbom.cdx.json and package.json license field (with -e node)")
136
152
  .action(async (options) => {
@@ -200,7 +216,7 @@ program.command('clean-deps')
200
216
  });
201
217
 
202
218
  // A container belongs to one image: the web container carries no NDK, and android runs a forced platform.
203
- const IMAGE_ROLES = ['web', 'android', 'linux', 'windows'];
219
+ const IMAGE_ROLES = ['web', 'android', 'android-app', 'linux', 'windows'];
204
220
  const dockerContainerName = (role) => {
205
221
  if (!IMAGE_ROLES.includes(role)) {
206
222
  console.error(`crossbind: unknown image "${role}" - expected one of ${IMAGE_ROLES.join(', ')}.`);
@@ -230,8 +246,8 @@ const imageArgument = (command) => command.argument('[image]', `image to act on
230
246
 
231
247
  imageArgument(commandDocker.command('create').description('create docker container'))
232
248
  .action((image) => {
233
- // Google ships the linux NDK for x86_64 only, so an android container is always amd64.
234
- const platform = image === 'android' ? 'linux/amd64' : undefined;
249
+ // Google ships the linux NDK for x86_64 only, so an android or android-app container is always amd64.
250
+ const platform = ['android', 'android-app'].includes(image) ? 'linux/amd64' : undefined;
235
251
  // Docker on Linux makes a missing mount source owned by root, which the user's builds cannot write.
236
252
  [cargoHome(), conanRoot()].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
237
253
  dockerExec([
@@ -284,10 +300,10 @@ commandRunner.command('start')
284
300
  connectCommands(options.role, runner.url, runner.displayToken).forEach((line) => console.log(` ${line}`));
285
301
  }));
286
302
 
287
- commandRunner.command('stop')
288
- .description('stop and remove the local runner')
303
+ commandRunner.command('delete')
304
+ .description('stop and delete the local runner')
289
305
  .addOption(roleOption())
290
- .action(runnerAction((options) => stopRunner({ role: options.role })));
306
+ .action(runnerAction((options) => deleteRunner({ role: options.role })));
291
307
 
292
308
  commandRunner.command('init')
293
309
  .description('write a folder that deploys a runner to a hosting platform')
@@ -306,76 +322,7 @@ commandRunner.command('init')
306
322
  deploySteps(platform, dir, newRunnerToken(), options.role, url).forEach((step) => console.log(` ${step}`));
307
323
  }));
308
324
 
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
-
352
- const commandConfig = program.command('config')
353
- .description('manage the crossbind configuration files');
354
-
355
- commandConfig.command('get')
356
- .description('get the crossbind system configuration')
357
- .argument('<key>', 'configuration key to read')
358
- .action((key) => getSystemConfig(key));
359
-
360
- commandConfig.command('set')
361
- .description('set the crossbind system configuration')
362
- .argument('<key>', 'configuration key to set')
363
- .argument('<value>', 'value to assign')
364
- .action((key, value) => setSystemConfig(key, value));
365
-
366
- commandConfig.command('delete')
367
- .description('delete the crossbind system configuration')
368
- .argument('<key>', 'configuration key to remove')
369
- .action((key) => deleteSystemConfig(key));
370
-
371
- commandConfig.command('list')
372
- .description('list the crossbind configurations')
373
- .addOption(new Option('-t, --type <type>', 'config type').default('system').choices(['all', 'system', 'project']))
374
- .action((options) => listSystemConfig(options.type));
375
-
376
- commandConfig.command('keys')
377
- .description('list all available system configuration keys for crossbind')
378
- .action(() => listSystemKeys());
325
+ addSystemCommands(program);
379
326
 
380
327
  // `pnpm run <script> -- --flag` forwards the `--` literally and commander would then
381
328
  // treat the flags as positionals. Strip the first separator for commands where every
@@ -388,67 +335,6 @@ if (['build', 'clean-deps', 'licenses'].includes(argv[2])) {
388
335
 
389
336
  program.parse(argv);
390
337
 
391
- function listSystemKeys() {
392
- console.info('Available configurations:');
393
- console.table(systemKeys);
394
- }
395
-
396
- function getSystemConfig(systemKey) {
397
- if (!systemKey) {
398
- listSystemConfig('system');
399
- return;
400
- }
401
- const key = systemKey.toUpperCase();
402
- const output = {};
403
- if (key in state.config.system) {
404
- output[key] = { value: state.config.system[key] || 'undefined', default: systemKeys[key].default };
405
- }
406
- Object.keys(systemKeys).filter((k) => k.includes(key)).forEach((k) => {
407
- if (k in state.config.system) {
408
- output[k] = { value: state.config.system[k] || 'undefined', default: systemKeys[k].default };
409
- }
410
- });
411
-
412
- console.table(output);
413
- }
414
-
415
- function setSystemConfig(key, value) {
416
- if (!systemKeys[key]) {
417
- throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
418
- }
419
- if (systemKeys[key].options && !systemKeys[key].options.includes(value)) {
420
- throw new Error(`Value ${value} is not available. Please choose from the following available values: ${systemKeys[key].options.join(', ')}`);
421
- }
422
-
423
- state.config.system[key] = value;
424
- writeJson(state.config.paths.systemConfig, state.config.system);
425
- }
426
-
427
- function deleteSystemConfig(key) {
428
- if (!systemKeys[key]) {
429
- throw new Error(`Configuration ${key} is not available. Please choose from the following available configurations: ${Object.keys(systemKeys).join(', ')}`);
430
- }
431
- delete state.config.system[key];
432
- writeJson(state.config.paths.systemConfig, state.config.system);
433
- }
434
-
435
- function listSystemConfig(type) {
436
- const { system: systemConfig, ...projectConfig } = state.config;
437
- if (type === 'all' || type === 'system') {
438
- console.log('System Configuration');
439
- console.table(systemConfig);
440
- }
441
-
442
- if (type === 'all') {
443
- console.log('');
444
- }
445
-
446
- if (type === 'all' || type === 'project') {
447
- console.log(`Project Configuration (paths relative to ${projectConfig.paths.base})`);
448
- console.table(flattenConfigForTable(projectConfig, { base: projectConfig.paths.base }));
449
- }
450
- }
451
-
452
338
  function run(programName, params) {
453
339
  runCrossbindApp(programName, params, null, null, { console: true });
454
340
  }
@@ -461,18 +347,20 @@ function onlyLoadsPackagedAddons(binaryParams) {
461
347
  return targets.every((target) => target.runtimeEnv === 'node' && target.platform !== 'wasm') && addonsInPackages();
462
348
  }
463
349
 
464
- async function build(targetParams, rebuildOption, binaryRuntimeEnvs) {
350
+ async function build(targetParams, rebuildOption, binaryRuntimeEnvs, wasmOptions = {}, appPlatforms = []) {
465
351
  await buildDependencies({ targetParams, rebuildOption });
466
352
  const binaryParams = { ...targetParams, runtimeEnv: binaryRuntimeEnvs };
467
353
  if (!onlyLoadsPackagedAddons(binaryParams)) {
468
- buildLib(targetParams);
354
+ // An app links its archives into itself, so it publishes no xcframework or podspec of them.
355
+ buildLib(targetParams, { skipXcframework: binaryRuntimeEnvs.includes('app') });
469
356
  }
470
357
  if (binaryRuntimeEnvs.length === 0) {
471
358
  logger.info(`crossbind: no runtime environment was selected, so only the archives were built - pass -e (${runtimeEnvs.join(', ')}) for a binary.`);
472
359
  } else {
473
- await createWasmJs(binaryParams);
360
+ await createWasmJs(binaryParams, wasmOptions);
474
361
  await createNodeAddons(binaryParams);
475
362
  await createNativeCommands(binaryParams);
363
+ await createApps({ ...binaryParams, platform: appPlatforms });
476
364
  await createWasiCommands(binaryParams);
477
365
  }
478
366
  buildPackageTypes();
@@ -491,6 +379,26 @@ async function createNativeCommands(targetParams) {
491
379
  }
492
380
  }
493
381
 
382
+ // An app built without a bundler has no page; the Vite plugin puts one in.
383
+ async function createApps(targetParams) {
384
+ const targets = getBuildTargets(targetParams).filter((target) => target.runtimeEnv === 'app');
385
+ if (targets.length === 0) {
386
+ return;
387
+ }
388
+ targets.forEach(assertAppRunner);
389
+ const bridges = createNativeBridges(targets).headers.flatMap((header) => header.bridge ?? []);
390
+ // An iOS or Android app is one project for all its SDKs or ABIs, written once they are built, and every time, as it
391
+ // also takes the app's identity and the host package's template.
392
+ const projects = new Map();
393
+ for (const target of targets) {
394
+ const isProject = ['ios', 'android'].includes(target.platform);
395
+ const built = await buildApp(target, { bridges, force: !isProject && !fs.existsSync(appOutputPath(target)) });
396
+ if (isProject) projects.set(appOutputPath(target), target);
397
+ else if (built) publishApp(target);
398
+ }
399
+ projects.forEach((target) => publishApp(target));
400
+ }
401
+
494
402
  async function createWasiCommands(targetParams) {
495
403
  const targets = getFilteredBuildTargets(targetParams, { platform: 'wasi' });
496
404
  if (targets.length === 0) {
@@ -506,23 +414,6 @@ async function createWasiCommands(targetParams) {
506
414
  }
507
415
  }
508
416
 
509
- function createBridges(target) {
510
- let headers = [];
511
- state.config.paths.header.forEach((headerPath) => {
512
- headers.push(findFiles('**/*.h', { cwd: headerPath }));
513
- });
514
- headers = headers.filter((path) => !!path.toString()).flat();
515
-
516
- return headers.map((header) => ({ file: header, bridge: createBridgeFile(header, target) }));
517
- }
518
-
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
417
  // A package whose platform packages link its addons ships what loads them: the loader and the data.
527
418
  async function publishLoaders(targets) {
528
419
  fs.mkdirSync(state.config.paths.output, { recursive: true });
@@ -544,10 +435,7 @@ async function createNodeAddons(targetParams) {
544
435
  `${resolveEmbindJsiRoot()}/cpp/src`,
545
436
  `${resolveEmbindNapiRoot()}/third_party/node-api-jsi/jsi`,
546
437
  ];
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)];
438
+ const { headers, imports } = createNativeBridges(targets);
551
439
  const opt = {
552
440
  buildSource: false,
553
441
  nativeGlob: [
@@ -593,16 +481,15 @@ function scopeOutputToCommonJs() {
593
481
  }
594
482
  }
595
483
 
596
- async function createWasmJs(targetParams) {
484
+ async function createWasmJs(targetParams, wasmOptions = {}) {
597
485
  const targets = getFilteredBuildTargets(targetParams, { platform: 'wasm' });
598
486
  if (targets.length === 0) {
599
487
  return;
600
488
  }
601
489
  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)];
490
+ const bindsImports = (target) => bindsAppImports(target, wasmOptions);
491
+ const imports = targets.some(bindsImports) ? findAppNativeImports([state.targets.find((t) => t.platform === 'wasm')]) : new Map();
492
+ const appHeaders = [...headers, ...createImportedBridges(headers, imports)];
606
493
  const optOf = (bridged) => ({
607
494
  buildSource: false,
608
495
  nativeGlob: [
@@ -619,7 +506,7 @@ async function createWasmJs(targetParams) {
619
506
  // link and the copy step below moved build/data away on the last run.
620
507
  const distComplete = fs.existsSync(`${state.config.paths.output}/${target.jsName}`)
621
508
  && fs.existsSync(`${state.config.paths.output}/${target.wasmName}`);
622
- createLib(target, 'Bridge', optOf(isNode(target) ? nodeHeaders : headers));
509
+ createLib(target, 'Bridge', optOf(bindsImports(target) ? appHeaders : headers));
623
510
  const built = await buildWasm(target, { force: !distComplete });
624
511
  if (!built) {
625
512
  continue;
@@ -640,15 +527,21 @@ async function createWasmJs(targetParams) {
640
527
  if (targets.some((target) => ['node', 'edge'].includes(target.runtimeEnv))) {
641
528
  scopeOutputToCommonJs();
642
529
  }
530
+ // The list a page that maps the app's imports itself reads; a build without --bind-imports is not for one.
531
+ if (targets.some((target) => target.runtimeEnv === 'browser')) {
532
+ const manifest = `${state.config.paths.output}/browser/imports.json`;
533
+ if (wasmOptions.bindImports) writeImportManifest(manifest, appHeaders, imports, state.config.paths.project);
534
+ else fs.rmSync(manifest, { force: true });
535
+ }
643
536
  // A runtime's entry loads its single-thread wasm32 release build.
644
537
  const entryTargetOf = (runtimeEnv) => targets.find((target) => target.runtimeEnv === runtimeEnv && target.arch === 'wasm32'
645
538
  && target.runtime === 'st' && target.buildType === 'release' && fs.existsSync(`${state.config.paths.output}/${target.jsName}`));
646
539
  const nodeTarget = entryTargetOf('node');
647
540
  const edgeTarget = entryTargetOf('edge');
648
- if (nodeHeaders.length && nodeTarget) {
541
+ if (appHeaders.length && nodeTarget) {
649
542
  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));
543
+ writeRuntimeEntry(entry, appHeaders, ENTRY_RUNTIMES.node(nodeTarget.jsName));
544
+ if (imports.size) writeImportHooks(entry, appHeaders, imports, nativeExtensions(state.config.ext));
652
545
  else removeImportHooks(entry);
653
546
  }
654
547
  if (headers.length && edgeTarget) {
package/src/cli.js ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { addSystemCommands, createProgram, handleFatal } from './program.js';
4
+
5
+ // bin.js loads the project before it reads the command line, which fails outside a project; these commands need none.
6
+ const program = createProgram();
7
+ addSystemCommands(program);
8
+ const withoutProject = [...program.commands.map((command) => command.name()), '-V', '--version'];
9
+
10
+ if (withoutProject.includes(process.argv[2])) {
11
+ await program.parseAsync().catch(handleFatal);
12
+ } else {
13
+ await import('./bin.js');
14
+ }
package/src/index.js CHANGED
@@ -3,6 +3,12 @@ export { default as getParentPath } from './utils/getParentPath.js';
3
3
  export { default as createLib } from './actions/createLib.js';
4
4
  export { default as createBridgeFile } from './actions/createInterface.js';
5
5
  export { default as buildWasm } from './actions/buildWasm.js';
6
+ export {
7
+ default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appTargetParams,
8
+ } from './actions/buildApp.js';
9
+ export { default as openInSimulator, buildForSimulator } from './actions/iosSimulator.js';
10
+ export { default as openOnDevice } from './actions/androidDevice.js';
11
+ export { buildApk } from './actions/androidGradle.js';
6
12
  export { default as isSourceNewer } from './actions/isSourceNewer.js';
7
13
  export { default as getData } from './actions/getData.js';
8
14
  export { default as getCmakeParameters } from './actions/getCmakeParameters.js';
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import getData from '../actions/getData.js';
3
+ import getEnv from '../actions/getEnv.js';
4
4
  import loadJson from '../utils/loadJson.js';
5
5
  import state from '../state/index.js';
6
6
  import { parseSurface, createRustBridgeCrate, createCrateImportBridge } from '../utils/rustBridgeGen.js';
@@ -70,6 +70,7 @@ export function getRustSymbols(rsFile) {
70
70
  modulePath,
71
71
  spec: cargoDeps[crateName],
72
72
  cacheDir: state.config.paths.cache,
73
+ projectDir: state.config.paths.project,
73
74
  dtsMode: state.config.dts,
74
75
  log: () => {},
75
76
  });
@@ -110,8 +111,10 @@ function buildScript(target, symbols, base) {
110
111
  if (!target) {
111
112
  throw new Error('The target is not available!');
112
113
  }
113
- const env = JSON.stringify(getData('env', target));
114
- const getPlatformScript = target.platform === 'wasm' ? getWebScript : getReactNativeScript;
114
+ const env = JSON.stringify(getEnv(target));
115
+ let getPlatformScript = getReactNativeScript;
116
+ if (target.platform === 'wasm') getPlatformScript = getWebScript;
117
+ else if (target.runtimeEnv === 'app') getPlatformScript = getAppScript;
115
118
 
116
119
  let symbolExportDefineString = '';
117
120
  let symbolExportAssignString = '';
@@ -178,7 +181,7 @@ function getReactNativeScript(env) {
178
181
  const RNJsiLib = TurboModuleRegistry.get('RNJsiLib');
179
182
 
180
183
  function setEnv() {
181
- const env = JSON.parse('${env}');
184
+ const env = ${env};
182
185
  const CROSSBIND_DATA_PATH = Module.Crossbind.getEnv('CROSSBIND_DATA_PATH');
183
186
 
184
187
  Object.entries(env).forEach(([key, value]) => {
@@ -199,6 +202,18 @@ function getReactNativeScript(env) {
199
202
  `;
200
203
  }
201
204
 
205
+ // An app's page loads the runtime the app serves as crossbind.js, which asks the app's host for the module. The build's
206
+ // env is the host's own, so the page passes on only what initNative() is given.
207
+ function getAppScript(env, base) {
208
+ return `
209
+ function __crossbindBoot(config) {
210
+ const base = new URL((typeof __webpack_public_path__ === 'string' ? __webpack_public_path__ : ${JSON.stringify(base)}) || './', document.baseURI).href;
211
+ return import(/* webpackIgnore: true */ /* @vite-ignore */ base + 'crossbind.js')
212
+ .then(() => window.Crossbind.initNative(config));
213
+ }
214
+ `;
215
+ }
216
+
202
217
  function getWebScript(env, base) {
203
218
  const params = `{
204
219
  path: base,