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
@@ -74,9 +74,11 @@ export function nodePackageLicense({ name, version, repository, projectPath, add
74
74
 
75
75
  export function setManifestLicense(dir, expression) {
76
76
  const file = path.join(dir, 'package.json');
77
- const manifest = JSON.parse(fs.readFileSync(file, 'utf8'));
77
+ const text = fs.readFileSync(file, 'utf8');
78
+ const manifest = JSON.parse(text);
78
79
  if (manifest.license === expression) return;
79
- fs.writeFileSync(file, `${JSON.stringify({ ...manifest, license: expression }, null, 4)}\n`);
80
+ const indent = text.match(/^[ \t]+(?=")/m)?.[0] ?? 4;
81
+ fs.writeFileSync(file, `${JSON.stringify({ ...manifest, license: expression }, null, indent)}\n`);
80
82
  }
81
83
 
82
84
  export function writeNodePackageLicense(dir, meta) {
@@ -0,0 +1,42 @@
1
+ // Property lists as Apple's tools read them: XML for an Info.plist, the older text form for an Xcode project.
2
+
3
+ const XML_INDENT = ' ';
4
+ const escapeXml = (text) => String(text).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
5
+
6
+ function xmlLines(value, indent) {
7
+ if (typeof value === 'boolean') return [`${indent}<${value}/>`];
8
+ const inner = indent + XML_INDENT;
9
+ if (Array.isArray(value)) return [`${indent}<array>`, ...value.flatMap((item) => xmlLines(item, inner)), `${indent}</array>`];
10
+ if (typeof value === 'object') {
11
+ const entries = Object.entries(value);
12
+ if (entries.length === 0) return [`${indent}<dict/>`];
13
+ return [
14
+ `${indent}<dict>`,
15
+ ...entries.flatMap(([key, item]) => [`${inner}<key>${escapeXml(key)}</key>`, ...xmlLines(item, inner)]),
16
+ `${indent}</dict>`,
17
+ ];
18
+ }
19
+ return [`${indent}<string>${escapeXml(value)}</string>`];
20
+ }
21
+
22
+ export function xmlPlist(dict) {
23
+ return '<?xml version="1.0" encoding="UTF-8"?>\n'
24
+ + '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n'
25
+ + `<plist version="1.0">\n${xmlLines(dict, '').join('\n')}\n</plist>\n`;
26
+ }
27
+
28
+ // Every value of the text form is a string, quoted here whatever it holds.
29
+ const quote = (text) => `"${String(text).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
30
+
31
+ function textValue(value, indent) {
32
+ const inner = `${indent}\t`;
33
+ if (Array.isArray(value)) return `(\n${value.map((item) => `${inner}${textValue(item, inner)},\n`).join('')}${indent})`;
34
+ if (typeof value === 'object') {
35
+ return `{\n${Object.entries(value).map(([key, item]) => `${inner}${quote(key)} = ${textValue(item, inner)};\n`).join('')}${indent}}`;
36
+ }
37
+ return quote(value);
38
+ }
39
+
40
+ export function textPlist(root) {
41
+ return `// !$*UTF8*$!\n${textValue(root, '')}\n`;
42
+ }
@@ -8,7 +8,11 @@ const table = createRequire(import.meta.url)('../assets/toolchain-digests.json')
8
8
  const REGISTRY = table.registry;
9
9
  const IMAGE_VERSION = table.version;
10
10
  const OVERRIDE_KEYS = {
11
- web: 'CROSSBIND_IMAGE_WEB', android: 'CROSSBIND_IMAGE_ANDROID', linux: 'CROSSBIND_IMAGE_LINUX', windows: 'CROSSBIND_IMAGE_WINDOWS',
11
+ web: 'CROSSBIND_IMAGE_WEB',
12
+ android: 'CROSSBIND_IMAGE_ANDROID',
13
+ 'android-app': 'CROSSBIND_IMAGE_ANDROID_APP',
14
+ linux: 'CROSSBIND_IMAGE_LINUX',
15
+ windows: 'CROSSBIND_IMAGE_WINDOWS',
12
16
  };
13
17
  const IMAGES = Object.fromEntries(
14
18
  Object.keys(OVERRIDE_KEYS).map((role) => [
@@ -21,9 +25,11 @@ const IMAGES = Object.fromEntries(
21
25
  );
22
26
 
23
27
  // Which image a target compiles in: wasm and wasi share the web image, linux and linuxmusl the linux
24
- // image, ios and darwin never reach docker.
28
+ // image, ios and darwin never reach docker. An Android app builds in android-app, the android image with
29
+ // what packages the app.
25
30
  export function imageRoleFor(target) {
26
- if (target?.platform === 'android' || target?.platform === 'linux') return target.platform;
31
+ if (target?.platform === 'android') return target.runtimeEnv === 'app' ? 'android-app' : 'android';
32
+ if (target?.platform === 'linux') return 'linux';
27
33
  if (target?.platform === 'linuxmusl') return 'linux';
28
34
  if (target?.platform === 'win32') return 'windows';
29
35
  return 'web';
@@ -1,5 +1,13 @@
1
+ import path from 'node:path';
2
+
1
3
  export const DOCKER_BASE = '/tmp/crossbind/live';
2
4
 
5
+ // Whether a container, which sees the project's base alone, sees dir.
6
+ export function isUnderBase(dir, basePath) {
7
+ const relative = path.relative(basePath, path.resolve(dir));
8
+ return !/^\.\.(?:[\\/]|$)/.test(relative) && !path.isAbsolute(relative);
9
+ }
10
+
3
11
  export default function replaceBasePathForDocker(data, basePath) {
4
12
  if (typeof data === 'string' || data instanceof String) {
5
13
  return data.replaceAll(basePath, DOCKER_BASE);
@@ -0,0 +1,26 @@
1
+ import fs from 'node:fs';
2
+ import state from '../state/index.js';
3
+ import replaceBasePathForDocker from './replaceBasePathForDocker.js';
4
+ import { isLocalRunner } from './selectRunner.js';
5
+ import { HOST_BUILT_PLATFORMS } from './targets.js';
6
+
7
+ const normalize = (dir) => dir.replaceAll('\\', '/').replace(/\/+$/, '');
8
+
9
+ // The directory as the cmake run() starts sees it: a container, Docker or a remote runner, has the project at
10
+ // DOCKER_BASE; macOS and iOS builds and the LOCAL runner see this machine's paths.
11
+ function seenByCMake(dir, target) {
12
+ const isLocal = HOST_BUILT_PLATFORMS.includes(target.platform) || isLocalRunner(state.config.system);
13
+ return isLocal ? dir : replaceBasePathForDocker(dir, state.config.paths.base);
14
+ }
15
+
16
+ // A CMake build tree remembers the source directory it was configured from and refuses another one. The CMake
17
+ // projects crossbind ships move with its version (node_modules/.pnpm/crossbind@<version>/...), so a tree configured
18
+ // from another directory starts afresh, as `cmake --fresh` would.
19
+ export default function resetCMakeCacheForSource(buildPath, sourceDir, target) {
20
+ const cache = `${buildPath}/CMakeCache.txt`;
21
+ if (!fs.existsSync(cache)) return;
22
+ const configuredFrom = fs.readFileSync(cache, 'utf8').match(/^CMAKE_HOME_DIRECTORY:INTERNAL=(.*)$/m)?.[1];
23
+ if (configuredFrom !== undefined && normalize(configuredFrom) === normalize(seenByCMake(sourceDir, target))) return;
24
+ fs.rmSync(cache, { force: true });
25
+ fs.rmSync(`${buildPath}/CMakeFiles`, { recursive: true, force: true });
26
+ }
@@ -0,0 +1,21 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { createRequire } from 'node:module';
4
+ import createCompanionResolver from './resolveCompanionPackage.js';
5
+
6
+ const PKG = '@crossbind/core-app-host';
7
+
8
+ const resolveAppHostRoot = createCompanionResolver({
9
+ pkg: PKG,
10
+ siblingDir: 'app-host',
11
+ siblingMarker: 'cpp/src/main.cpp',
12
+ missingMessage: `crossbind: apps need ${PKG} - add it to your devDependencies.`,
13
+ });
14
+
15
+ export default resolveAppHostRoot;
16
+
17
+ // The embind runtime ships in @crossbind/core-embind-jsi, a dependency of the app host package.
18
+ export function resolveAppEmbindJsiRoot() {
19
+ const req = createRequire(path.join(resolveAppHostRoot(), 'package.json'));
20
+ return path.dirname(fs.realpathSync(req.resolve('@crossbind/core-embind-jsi/package.json')));
21
+ }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
+ import { followCargoLock } from './cargoLock.js';
4
5
  import resolveEmbindRustRoot, { embindRustVersion } from './resolveEmbindRust.js';
5
6
  import runCargo, { toHostPath } from './runCargo.js';
6
7
  import writeIfChanged from './writeIfChanged.js';
@@ -303,7 +304,9 @@ export function createRustBridgeCrate({ rsFile, cacheDir, projectPath, vectors =
303
304
  // through the root and through a module registers once and is the same JS class for both.
304
305
  const crateSourceCache = new Map();
305
306
  const crateModelCache = new Map();
306
- export function createCrateImportBridge({ crateName, modulePath = [], spec, cacheDir, dtsMode = 'sync', log = console.log }) {
307
+ export function createCrateImportBridge({
308
+ crateName, modulePath = [], spec, cacheDir, projectDir, dtsMode = 'sync', log = console.log,
309
+ }) {
307
310
  const importName = [crateName, ...modulePath].join('/');
308
311
  const crateIdent = crateName.replaceAll('-', '_');
309
312
  const stem = `crate_${crateName.replace(/[^a-zA-Z0-9_]/g, '_').toLowerCase()}`;
@@ -343,7 +346,9 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
343
346
  // cargo metadata needs a resolvable lib target before the real bridge exists.
344
347
  if (!fs.existsSync(`${dir}/src/lib.rs`)) writeIfChanged(`${dir}/src/lib.rs`, '// crossbind placeholder\n');
345
348
 
346
- const { srcDir, features } = crateSource({ dir, depLine, crateName, importName });
349
+ const { srcDir, features } = crateSource({
350
+ dir, depLine, crateName, importName, projectDir,
351
+ });
347
352
  const ownKey = modulePath.join('::');
348
353
  const imports = [];
349
354
  for (const importPath of crateImports(cacheDir, crateName, modulePath)) {
@@ -385,10 +390,14 @@ export function createCrateImportBridge({ crateName, modulePath = [], spec, cach
385
390
  }
386
391
 
387
392
  // cargo metadata locates the crate's source and the feature set it resolves to (fetching the crate
388
- // on first use); both hold for every import of the crate.
389
- function crateSource({ dir, depLine, crateName, importName }) {
393
+ // on first use); both hold for every import of the crate. It resolves with the project's pins, so
394
+ // the bridge is made from the version the app links.
395
+ function crateSource({
396
+ dir, depLine, crateName, importName, projectDir,
397
+ }) {
390
398
  const cacheKey = `${dir}|${depLine}`;
391
399
  if (crateSourceCache.has(cacheKey)) return crateSourceCache.get(cacheKey);
400
+ followCargoLock(projectDir, dir);
392
401
  let meta;
393
402
  try {
394
403
  const probe = runCargo(['metadata', '--format-version', '1', '--manifest-path', `${dir}/Cargo.toml`], {
@@ -1,4 +1,5 @@
1
1
  import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
2
3
  import path from 'node:path';
3
4
 
4
5
  // One argv per flag: runCargo joins these with cargo's 0x1F separator, so a flag must not
@@ -39,6 +40,10 @@ export function cargoTargetDirFor(baseDir, target) {
39
40
  // points at whatever version that image pinned, so the CLI carries no version of its own.
40
41
  const CONTAINER_SYSROOT = '/opt/crossbind/rust/current';
41
42
 
43
+ // True only inside the toolchain image. A CLI running there with RUNNER=LOCAL (the playground
44
+ // compiler) has these trees, but not rustup's stock wasm target.
45
+ export const hasContainerSysroot = () => fs.existsSync(CONTAINER_SYSROOT);
46
+
42
47
  export function cargoBuildInvocation({
43
48
  target, triple, targetDir, manifestPath, sysroot = false,
44
49
  }) {
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
5
5
  import withDirLock from './dirLock.js';
6
6
  import fetchOciLayer from './ociArtifact.js';
7
7
  import { cargoRunner } from './runCargo.js';
8
- import { isMtWasm } from './rustMt.js';
8
+ import { hasContainerSysroot, isMtWasm } from './rustMt.js';
9
9
  import PIN from './rustSysrootPin.js';
10
10
 
11
11
  // The second consumption channel for the Rust sysroots. A containerized build gets them as an image
@@ -149,7 +149,7 @@ let resolved = null;
149
149
  // Called from buildDependencies, the one async step every build path awaits before the sync work
150
150
  // starts. Only downloads when a target will actually consume it.
151
151
  export async function prepareRustSysroot(targets, log = console.log) {
152
- if (resolved || !PIN) return resolved;
152
+ if (resolved || !PIN || hasContainerSysroot()) return resolved;
153
153
  if (!targets.some((target) => isMtWasm(target) && cargoRunner(target) === 'LOCAL')) return null;
154
154
  try {
155
155
  resolved = await ensureRustSysroot({ image: PIN.image, index: PIN.index });
@@ -161,11 +161,13 @@ export async function prepareRustSysroot(targets, log = console.log) {
161
161
  return resolved;
162
162
  }
163
163
 
164
- // What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees, an
165
- // absolute path where a downloaded artifact answers, false where mt must rebuild std on nightly.
166
- // st needs nothing - rustup's stock std is already correct for it.
164
+ // What cargoBuildInvocation takes for `sysroot`: true where the image carries the trees (also a
165
+ // LOCAL build running inside the image), an absolute path where a downloaded artifact answers,
166
+ // false where mt must rebuild std on nightly. st needs nothing - rustup's stock std is already
167
+ // correct for it.
167
168
  export function sysrootFor(target) {
168
169
  if (cargoRunner(target) !== 'LOCAL') return true;
170
+ if (target.platform === 'wasm' && hasContainerSysroot()) return true;
169
171
  if (!isMtWasm(target)) return false;
170
172
  return resolved ?? false;
171
173
  }
@@ -7,7 +7,7 @@ const SYSTEM_CONFIG = '~/.crossbind.json';
7
7
  const SHARED = { key: 'REMOTE_URL', url: 'CROSSBIND_REMOTE_URL', token: 'CROSSBIND_TOKEN' };
8
8
 
9
9
  export const remoteVariables = (role) => {
10
- const suffix = `_${role.toUpperCase()}`;
10
+ const suffix = `_${role.toUpperCase().replaceAll('-', '_')}`;
11
11
  return { key: `${SHARED.key}${suffix}`, url: `${SHARED.url}${suffix}`, token: `${SHARED.token}${suffix}` };
12
12
  };
13
13
 
@@ -75,8 +75,9 @@ export function buildInterfaceContent({
75
75
  // The marker keeps a preamble that starts with #define apart from the macro block the retry drops.
76
76
  const preambleBlock = preamble.length ? `// package preamble\n${preamble.join('\n')}\n\n` : '';
77
77
  const extraBlock = extras.length ? `${extras.join('\n')}\n\n` : '';
78
- // Named constants are asked for ahead of the macros, which may define one. Every constant is asked for after them:
79
- // SWIG binds an interface's own #define lines, and the prelude only carries other headers' values for the parse.
78
+ // Named constants and functions are asked for ahead of the macros, which may define an imported name. Every constant
79
+ // is asked for after them: SWIG binds an interface's own #define lines, and the prelude only carries other headers'
80
+ // values for the parse.
80
81
  const everyConstant = constants === ALL_NAMES;
81
82
  const module = moduleName.replace(/\W/g, '_');
82
83
  return `#ifndef _${module}_I
@@ -88,10 +89,10 @@ ${extraBlock}%{
88
89
  ${includes}
89
90
  %}
90
91
 
91
- ${everyConstant ? '' : constantRequests(constants)}%feature("shared_ptr");
92
+ ${everyConstant ? '' : constantRequests(constants)}${functionRequests(functions)}%feature("shared_ptr");
92
93
  %feature("polymorphic_shared_ptr");
93
94
 
94
- ${macros}${functionRequests(functions)}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
95
+ ${macros}${ignores}${preambleBlock}${everyConstant ? constantRequests(constants) : ''}%include "${headerPath}"
95
96
 
96
97
  #endif
97
98
  `;
@@ -0,0 +1,13 @@
1
+ import os from 'node:os';
2
+ import loadJson from './loadJson.js';
3
+ import systemKeys from './systemKeys.js';
4
+
5
+ export const systemConfigPath = () => `${os.homedir()}/.crossbind.json`;
6
+
7
+ export default function loadSystemConfig() {
8
+ const system = loadJson(systemConfigPath()) || {};
9
+ const defaults = Object.entries(systemKeys)
10
+ .filter(([key]) => !(key in system))
11
+ .map(([key, value]) => [key, value.default]);
12
+ return { ...system, ...Object.fromEntries(defaults) };
13
+ }
@@ -48,6 +48,10 @@ const systemKeys = {
48
48
  description: 'Image reference used for android builds instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_ANDROID environment variable overrides it.',
49
49
  default: '',
50
50
  },
51
+ DOCKER_IMAGE_ANDROID_APP: {
52
+ description: 'Image reference used for Android apps instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_ANDROID_APP environment variable overrides it.',
53
+ default: '',
54
+ },
51
55
  DOCKER_IMAGE_LINUX: {
52
56
  description: 'Image reference used for linux (Node.js addon) builds instead of the pinned one. A reference without the release digest disables the reproducibility guarantee. The CROSSBIND_IMAGE_LINUX environment variable overrides it.',
53
57
  default: '',
@@ -68,6 +68,12 @@ const BASE_TARGETS = [
68
68
  {
69
69
  platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'wasi',
70
70
  },
71
+ {
72
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'release', runtimeEnv: 'http',
73
+ },
74
+ {
75
+ platform: 'wasi', arch: 'wasm32', runtime: 'st', buildType: 'debug', runtimeEnv: 'http',
76
+ },
71
77
  {
72
78
  platform: 'android', arch: 'arm64-v8a', runtime: 'mt', buildType: 'release',
73
79
  },
@@ -142,12 +148,17 @@ const BASE_TARGETS = [
142
148
  },
143
149
  ];
144
150
 
145
- // A native executable links the same archives as the Node-API addon of its platform and arch.
151
+ // A native executable links the same archives as the Node-API addon of its platform and arch, and so does an app's
152
+ // host on macOS, glibc Linux and Windows. An iOS or Android app links the archives React Native does, one target per
153
+ // SDK or ABI.
146
154
  export const TARGETS = [
147
155
  ...BASE_TARGETS,
148
156
  ...BASE_TARGETS
149
157
  .filter((target) => target.runtimeEnv === 'node' && target.platform !== 'wasm')
150
158
  .map((target) => ({ ...target, runtimeEnv: 'native' })),
159
+ ...BASE_TARGETS
160
+ .filter((target) => (target.runtimeEnv === 'node' && ['darwin', 'linux', 'win32'].includes(target.platform)) || ['ios', 'android'].includes(target.platform))
161
+ .map((target) => ({ ...target, runtimeEnv: 'app' })),
151
162
  ];
152
163
 
153
164
  // Built only when named with -p: a plain `crossbind build` keeps producing what it did before
@@ -157,8 +168,8 @@ export const OPT_IN_PLATFORMS = ['darwin', 'linux', 'linuxmusl', 'win32'];
157
168
  // Built on the host itself: Apple's linker and SDKs exist in no toolchain image.
158
169
  export const HOST_BUILT_PLATFORMS = ['ios', 'darwin'];
159
170
 
160
- // The runtime environment names the binary a build makes; android and ios have none, since
161
- // React Native links them.
171
+ // The runtime environment names the binary a build makes; on android and ios React Native links
172
+ // the archives, and the app is the one binary.
162
173
  export const runtimeEnvsOf = (platform) => [
163
174
  ...new Set(TARGETS.filter((target) => target.platform === platform && target.runtimeEnv).map((target) => target.runtimeEnv)),
164
175
  ];
@@ -192,6 +203,22 @@ export function nodeAddonNamesOf(target, projectName) {
192
203
  };
193
204
  }
194
205
 
206
+ // A wasi build is one component, with no JS glue: the command keeps the plain name, an http component adds its own.
207
+ export const wasiNamesOf = (target, projectName) => ({
208
+ wasmName: `${projectName}-${targetPathOf(target)}${target.runtimeEnv === 'http' ? '.http' : ''}.wasm`,
209
+ });
210
+
211
+ // One app per platform, arch and build type, and one page runtime per build type for all of them. A macOS app is a
212
+ // bundle; a Linux app a directory; an iOS or Android one what its SDK or ABI brings to the app's Xcode or Gradle
213
+ // project.
214
+ export function appNamesOf(target, projectName) {
215
+ const suffix = target.buildType === 'debug' ? '.debug' : '';
216
+ return {
217
+ appName: `${projectName}.${target.platform}-${target.arch}${suffix}${target.platform === 'darwin' ? '.app' : ''}`,
218
+ appJsName: `${projectName}.app${suffix}.js`,
219
+ };
220
+ }
221
+
195
222
  // One executable per platform, arch and build type, named like the addons it replaces.
196
223
  export function nativeCommandNamesOf(target, projectName) {
197
224
  const suffix = target.buildType === 'debug' ? '.debug' : '';
@@ -0,0 +1,50 @@
1
+ // The runtime of an http build: crossbind's wasi:http/handler export, which calls the app's
2
+ // crossbind::http::handle, and the bindings wit-bindgen generated for it (assets/wasi-runtime/http/README.md).
3
+
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ const RUNTIME = 'assets/wasi-runtime/http';
8
+ // The stack of each request: wasi-libc mallocs one per concurrent request, with no guard below it.
9
+ const STACK_SIZE = 1024 * 1024;
10
+ const HEADER = 'include/crossbind/http.h';
11
+ const LINKED = [
12
+ 'handler.cpp',
13
+ 'bindings/crossbind_http.c',
14
+ 'bindings/crossbind_http.h',
15
+ 'bindings/crossbind_http_component_type.o',
16
+ HEADER,
17
+ ];
18
+
19
+ // paths.cli can lie outside the docker mount, so the files are copied into the build tree first.
20
+ function stage(cliPath, dir, files) {
21
+ files.forEach((file) => {
22
+ fs.mkdirSync(path.dirname(`${dir}/${file}`), { recursive: true });
23
+ fs.copyFileSync(`${cliPath}/${RUNTIME}/${file}`, `${dir}/${file}`);
24
+ });
25
+ }
26
+
27
+ export const wasiHttpFiles = (cliPath) => LINKED.map((file) => `${cliPath}/${RUNTIME}/${file}`);
28
+
29
+ export const wasiHttpHeader = (cliPath) => `${cliPath}/${RUNTIME}/${HEADER}`;
30
+
31
+ // The include dir of a wasi source build, where the app's handler finds <crossbind/http.h>.
32
+ export function stageWasiHttpInclude(cliPath, dir) {
33
+ stage(cliPath, dir, [HEADER]);
34
+ return `${dir}/include`;
35
+ }
36
+
37
+ // What an http link adds to a command's: the handler export, its bindings (C, hence -x c) with the world
38
+ // they implement, the reactor model, which has no main, and a stack as large as the wasm builds'.
39
+ export function stageWasiHttpLink(cliPath, dir) {
40
+ stage(cliPath, dir, LINKED);
41
+ return [
42
+ `-I${dir}/include`,
43
+ `-I${dir}/bindings`,
44
+ `${dir}/handler.cpp`,
45
+ '-x', 'c', `${dir}/bindings/crossbind_http.c`, '-x', 'none',
46
+ `${dir}/bindings/crossbind_http_component_type.o`,
47
+ '-mexec-model=reactor',
48
+ `-Wl,-z,stack-size=${STACK_SIZE}`,
49
+ ];
50
+ }
package/vitest.config.js CHANGED
@@ -1,6 +1,11 @@
1
+ import { fileURLToPath } from 'node:url';
1
2
  import { defineConfig } from 'vitest/config';
2
3
 
3
4
  export default defineConfig({
5
+ // The app host package's scripts import crossbind's runtime modules the way buildApp bundles them.
6
+ resolve: {
7
+ alias: [{ find: /^crossbind\/runtime\//, replacement: fileURLToPath(new URL('./src/assets/js-runtime/', import.meta.url)) }],
8
+ },
4
9
  // loadJs/loadConfig dynamically import config files written to os.tmpdir()
5
10
  // during tests. Vite 6 tightened server.fs.allow and rejects those out-of-root
6
11
  // paths ("Does the file exist?"); relax the fs check for the test runner.