crossbind 2.0.0-beta.62 → 2.0.0-beta.64

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 (132) hide show
  1. package/AGENTS.md +2 -1
  2. package/README.md +2 -2
  3. package/package.json +7 -2
  4. package/src/actions/buildBinTools.js +0 -0
  5. package/src/actions/buildDependencies.js +15 -2
  6. package/src/actions/buildExternal.js +9 -1
  7. package/src/actions/buildJs.js +2 -9
  8. package/src/actions/buildLib.js +25 -8
  9. package/src/actions/buildNativeCommand.js +84 -0
  10. package/src/actions/buildNode.js +49 -10
  11. package/src/actions/buildTypes.js +8 -2
  12. package/src/actions/buildWasiCommand.js +10 -9
  13. package/src/actions/buildWasm.js +72 -31
  14. package/src/actions/cloudCommands.js +133 -0
  15. package/src/actions/createImportedBridges.js +71 -0
  16. package/src/actions/createInterface.js +247 -68
  17. package/src/actions/createLib.js +70 -42
  18. package/src/actions/getData.js +6 -2
  19. package/src/actions/getEnv.js +7 -0
  20. package/src/actions/getLinkInputs.js +12 -5
  21. package/src/actions/isSourceNewer.js +2 -0
  22. package/src/actions/licenses.js +143 -19
  23. package/src/actions/prepareConanDependencies.js +51 -0
  24. package/src/actions/publishBinary.js +32 -0
  25. package/src/actions/run.js +63 -66
  26. package/src/actions/runnerCommands.js +433 -0
  27. package/src/actions/writeRuntimeEntry.js +80 -0
  28. package/src/assets/cmake/command/CMakeLists.txt +21 -0
  29. package/src/assets/cmake/command/command.cpp +1 -0
  30. package/src/assets/cmake/dist.cmake +2 -1
  31. package/src/assets/configure/install.sh +22 -0
  32. package/src/assets/cpp-runtime/node.cpp +18 -1
  33. package/src/assets/js-runtime/adapters/path-url.js +6 -3
  34. package/src/assets/js-runtime/adapters/unboundMessage.js +2 -0
  35. package/src/assets/js-runtime/adapters/worker-comlink.js +138 -60
  36. package/src/assets/js-runtime/core.js +12 -0
  37. package/src/assets/node-import-hooks.mjs +61 -0
  38. package/src/assets/toolchain-digests.json +19 -19
  39. package/src/assets/wasi-runtime/http/README.md +17 -0
  40. package/src/assets/wasi-runtime/http/bindings/crossbind_http.c +2537 -0
  41. package/src/assets/wasi-runtime/http/bindings/crossbind_http.h +826 -0
  42. package/src/assets/wasi-runtime/http/bindings/crossbind_http_component_type.o +0 -0
  43. package/src/assets/wasi-runtime/http/handler.cpp +298 -0
  44. package/src/assets/wasi-runtime/http/include/crossbind/http.h +31 -0
  45. package/src/assets/wasi-runtime/http/wit/deps/LICENSE.md +8 -0
  46. package/src/assets/wasi-runtime/http/wit/deps/cli.wit +28 -0
  47. package/src/assets/wasi-runtime/http/wit/deps/clocks.wit +43 -0
  48. package/src/assets/wasi-runtime/http/wit/deps/http.wit +509 -0
  49. package/src/assets/wasi-runtime/http/wit/deps/random.wit +18 -0
  50. package/src/assets/wasi-runtime/http/wit/world.wit +6 -0
  51. package/src/assets/wasi-runtime/stubs.c +29 -0
  52. package/src/bin.js +218 -161
  53. package/src/cli.js +14 -0
  54. package/src/index.js +3 -0
  55. package/src/integration/bindHeaderImports.js +18 -0
  56. package/src/integration/getCrossbindScript.js +50 -13
  57. package/src/integration/getDependFilePath.js +9 -0
  58. package/src/integration/resolveNativeImport.js +14 -0
  59. package/src/node/dev.mjs +19 -0
  60. package/src/program.js +169 -0
  61. package/src/runner/blobs.js +135 -0
  62. package/src/runner/files.js +193 -0
  63. package/src/runner/server.js +320 -0
  64. package/src/state/attachConanDependencies.js +56 -0
  65. package/src/state/index.js +20 -11
  66. package/src/state/loadConfig.js +37 -35
  67. package/src/state/refreshBuiltDependencies.js +20 -0
  68. package/src/state/refreshConanDependencies.js +17 -0
  69. package/src/utils/androidToolchain.js +4 -0
  70. package/src/utils/appRustCrates.js +19 -1
  71. package/src/utils/appSources.js +97 -6
  72. package/src/utils/archiveFlags.js +12 -0
  73. package/src/utils/boundHeaders.js +79 -0
  74. package/src/utils/bridgeExtras.js +554 -0
  75. package/src/utils/bridgeRuntimeFixes.js +48 -0
  76. package/src/utils/bundledLicenses.js +26 -0
  77. package/src/utils/cargoLock.js +19 -0
  78. package/src/utils/cargoTarget.js +21 -3
  79. package/src/utils/cloudAccount.js +55 -0
  80. package/src/utils/conanDependencies.js +66 -0
  81. package/src/utils/conanImport.js +58 -0
  82. package/src/utils/conanInstall.js +154 -0
  83. package/src/utils/conanProfile.js +203 -0
  84. package/src/utils/conanStage.js +292 -0
  85. package/src/utils/copyToDistSource.js +13 -0
  86. package/src/utils/cppDts.js +60 -15
  87. package/src/utils/darwinToolchain.js +11 -0
  88. package/src/utils/dirLock.js +137 -28
  89. package/src/utils/downloadAndExtractFile.js +34 -7
  90. package/src/utils/embindArgumentGuards.js +41 -2
  91. package/src/utils/embindCallArguments.js +19 -0
  92. package/src/utils/embindJspiQueue.js +25 -0
  93. package/src/utils/embindObjectLifetime.js +35 -0
  94. package/src/utils/execContainer.js +49 -0
  95. package/src/utils/headerImports.js +16 -5
  96. package/src/utils/hostPlatform.js +21 -0
  97. package/src/utils/iosToolchain.js +6 -0
  98. package/src/utils/isSourceCmakePackage.js +10 -0
  99. package/src/utils/licenseReport.js +11 -2
  100. package/src/utils/linuxToolchain.js +24 -0
  101. package/src/utils/makeTreeWritable.js +12 -0
  102. package/src/utils/nativeImports.js +23 -0
  103. package/src/utils/nativeSourceStamp.js +36 -0
  104. package/src/utils/nodeDevBuild.js +92 -0
  105. package/src/utils/packageLicense.js +103 -0
  106. package/src/utils/pthreadMainScript.js +14 -0
  107. package/src/utils/pullDockerImage.js +3 -1
  108. package/src/utils/relocatePrebuilt.js +119 -0
  109. package/src/utils/remoteClient.js +127 -0
  110. package/src/utils/remoteOutputs.js +39 -0
  111. package/src/utils/remoteRunner.js +97 -0
  112. package/src/utils/remoteUpload.js +134 -0
  113. package/src/utils/replaceFile.js +8 -0
  114. package/src/utils/resetCMakeCacheForSource.js +26 -0
  115. package/src/utils/runCargo.js +48 -51
  116. package/src/utils/runConan.js +257 -0
  117. package/src/utils/runtimeEntries.js +56 -0
  118. package/src/utils/rustBridgeGen.js +22 -8
  119. package/src/utils/rustMt.js +9 -0
  120. package/src/utils/rustSysroot.js +7 -5
  121. package/src/utils/scopedEmbind.js +22 -0
  122. package/src/utils/selectRunner.js +81 -0
  123. package/src/utils/sourceFingerprint.js +4 -3
  124. package/src/utils/swigInterface.js +70 -4
  125. package/src/utils/systemConfig.js +13 -0
  126. package/src/utils/systemKeys.js +35 -3
  127. package/src/utils/targets.js +60 -4
  128. package/src/utils/toolchainNotices.js +5 -0
  129. package/src/utils/wasiHttp.js +50 -0
  130. package/src/utils/wasiToolchain.js +14 -1
  131. package/src/utils/windowsToolchain.js +18 -0
  132. package/vitest.config.js +11 -0
@@ -1,6 +1,5 @@
1
- import os from 'node:os';
2
1
  import fs from 'node:fs';
3
- import systemKeys from '../utils/systemKeys.js';
2
+ import loadSystemConfig, { systemConfigPath } from '../utils/systemConfig.js';
4
3
  import loadJs from '../utils/loadJs.js';
5
4
  import {
6
5
  resolveDependencyOverride, resolveDependencyReplace, restampIdentity, resolveExcludedNames, getOverrideKey,
@@ -12,12 +11,27 @@ import fixPackageName from '../utils/fixPackageName.js';
12
11
  import getCMakeListsFilePath from '../utils/getCMakeListsFilePath.js';
13
12
  import calculateDependencyParameters from './calculateDependencyParameters.js';
14
13
  import logger from '../utils/logger.js';
14
+ import { RUNTIME_ENVS } from '../utils/targets.js';
15
+ import { normalizeConanDependencies } from '../utils/conanDependencies.js';
16
+ import isSourceCmakePackage from '../utils/isSourceCmakePackage.js';
15
17
  // import getCmakeParameters from './getCmakeParameters.js';
16
18
 
19
+ // For the project being built only: dependency builds load published configs that may still set
20
+ // export.bundle.
21
+ export function assertBinarySelection(config) {
22
+ if ('bundle' in config.export) {
23
+ throw new Error('crossbind: export.bundle was removed. A build makes binaries only for the runtime environments it is asked for: -e on the command line, target.runtimeEnv in crossbind.config.js, or a bundler plugin. Remove export.bundle.');
24
+ }
25
+ const { runtimeEnv } = config.target;
26
+ if (runtimeEnv !== undefined && runtimeEnv !== null && !RUNTIME_ENVS.includes(runtimeEnv)) {
27
+ throw new Error(`crossbind: target.runtimeEnv must be one of ${RUNTIME_ENVS.join(', ')} (got ${JSON.stringify(runtimeEnv)}).`);
28
+ }
29
+ }
30
+
17
31
  export default async function loadConfig(configDir = process.cwd(), configName = 'crossbind.config') {
18
32
  const config = await loadJs(configDir, configName) || {};
19
33
  if (config.excludedDependencies?.length) {
20
- logger.info('crossbind: `excludedDependencies` in crossbind.config.js is no longer read — declare exclusions in crossbind.overrides.js as { <name>: false }.');
34
+ logger.info('crossbind: `excludedDependencies` in crossbind.config.js is no longer read — declare exclusions in crossbind.overrides.js as { <name>: { exclude: true } }.');
21
35
  }
22
36
  const overrides = await loadJs(configDir, 'crossbind.overrides');
23
37
  const exclude = resolveExcludedNames(overrides);
@@ -68,18 +82,21 @@ export default async function loadConfig(configDir = process.cwd(), configName =
68
82
  }
69
83
  });
70
84
 
71
- output.paths.systemConfig = `${os.homedir()}/.crossbind.json`;
72
- output.system = loadJson(output.paths.systemConfig) || {};
73
-
74
- Object.entries(systemKeys).forEach(([key, value]) => {
75
- if (!(key in output.system)) {
76
- output.system[key] = value.default;
77
- }
78
- });
85
+ output.paths.systemConfig = systemConfigPath();
86
+ output.system = loadSystemConfig();
79
87
 
80
88
  return output;
81
89
  }
82
90
 
91
+ // Direct and transitive dependencies, one entry per package directory.
92
+ export function flattenDependencies(dependencies) {
93
+ const output = {};
94
+ [...dependencies, ...dependencies.map((d) => d.allDependencies).flat()].forEach((d) => {
95
+ output[d.paths.project] = d;
96
+ });
97
+ return Object.values(output);
98
+ }
99
+
83
100
  export function getFilledConfig(config, options = { isDepend: false }) {
84
101
  const exclude = options.exclude || [];
85
102
  const { seen, replaces } = options;
@@ -101,6 +118,7 @@ export function getFilledConfig(config, options = { isDepend: false }) {
101
118
  general: config.general || {},
102
119
  dependencies,
103
120
  cargoDependencies: config.cargoDependencies || {},
121
+ conanDependencies: normalizeConanDependencies(config.conanDependencies),
104
122
  // 'sync' types the direct surface; 'promise' wraps every generated method return
105
123
  // for useWorker-style runtimes (constructors stay sync-typed: await new X()).
106
124
  dts: config.dts || 'sync',
@@ -176,15 +194,8 @@ export function getFilledConfig(config, options = { isDepend: false }) {
176
194
  newConfig.export.libPath = getPath(newConfig.export.libPath || 'lib');
177
195
  newConfig.export.libName = newConfig.export.libName || [newConfig.general.name];
178
196
  newConfig.export.binHeaders = newConfig.export.binHeaders || [];
179
- newConfig.export.bundle = newConfig.export.bundle ?? true;
180
197
 
181
- newConfig.allDependencies = (() => {
182
- const output = {};
183
- [...newConfig.dependencies, ...newConfig.dependencies.map((d) => d.allDependencies).flat()].forEach((d) => {
184
- output[d.paths.project] = d;
185
- });
186
- return Object.values(output);
187
- })();
198
+ newConfig.allDependencies = flattenDependencies(newConfig.dependencies);
188
199
 
189
200
  newConfig.extensions?.forEach(e => {
190
201
  e?.loadConfig?.after(newConfig);
@@ -194,22 +205,13 @@ export function getFilledConfig(config, options = { isDepend: false }) {
194
205
  newConfig.target.runtime = 'mt';
195
206
  }
196
207
 
197
- newConfig.functions.isEnabled = newConfig.functions.isEnabled || ((target) => {
198
- // Source-cmake packages ship no prebuilt at all: their own CMakeLists (at the
199
- // package root, not a generated dist/prebuilt one) is compiled into the consuming
200
- // build via add_subdirectory, so they serve every target.
201
- const isSourceCmakePackage = newConfig.export?.type === 'cmake'
202
- && newConfig.paths.cmake !== newConfig.paths.cliCMakeListsTxt
203
- && !newConfig.paths.cmakeDir.endsWith('/prebuilt')
204
- && fs.existsSync(newConfig.paths.cmake);
205
-
206
- return (
207
- isSourceCmakePackage
208
- || fs.existsSync(`${newConfig.paths.cmakeDir}/${target.path}`)
209
- || fs.existsSync(`${newConfig.paths.cmakeDir}/${target.releasePath}`)
210
- || (target.platform === 'ios' && fs.existsSync(`${newConfig.paths.cmakeDir}/../../${newConfig.general.name}-${target.runtime}.xcframework`))
211
- );
212
- });
208
+ // A package that ships its sources serves every target.
209
+ newConfig.functions.isEnabled = newConfig.functions.isEnabled || ((target) => (
210
+ isSourceCmakePackage(newConfig)
211
+ || fs.existsSync(`${newConfig.paths.cmakeDir}/${target.path}`)
212
+ || fs.existsSync(`${newConfig.paths.cmakeDir}/${target.releasePath}`)
213
+ || (target.platform === 'ios' && fs.existsSync(`${newConfig.paths.cmakeDir}/../../${newConfig.general.name}-${target.runtime}.xcframework`))
214
+ ));
213
215
 
214
216
  newConfig.dependencyParameters = calculateDependencyParameters(newConfig);
215
217
  // newConfig.cmakeParameters = getCmakeParameters(newConfig);
@@ -0,0 +1,20 @@
1
+ import calculateDependencyParameters from './calculateDependencyParameters.js';
2
+ import getAbsolutePath from '../utils/getAbsolutePath.js';
3
+ import getCMakeListsFilePath, { getCliCMakeListsFile } from '../utils/getCMakeListsFilePath.js';
4
+ import getParentPath from '../utils/getParentPath.js';
5
+
6
+ // A dependency with no build when the config loaded resolved to the CLI's own CMakeLists, which keeps it out of the link
7
+ // and its headers out of the dependency include roots: a process that outlives the build, such as a Metro server, takes it
8
+ // in here. Says whether it took one in.
9
+ export default function refreshBuiltDependencies(config) {
10
+ const built = (config.allDependencies ?? [])
11
+ .filter((dep) => dep !== config && ['cmake', 'cargo'].includes(dep.export?.type) && dep.paths.cmake === config.paths.cliCMakeListsTxt)
12
+ .map((dep) => ({ dep, cmake: getCMakeListsFilePath(dep.paths.output) }))
13
+ .filter(({ cmake }) => cmake !== getCliCMakeListsFile());
14
+ built.forEach(({ dep, cmake }) => {
15
+ dep.paths.cmake = getAbsolutePath(dep.paths.project, cmake);
16
+ dep.paths.cmakeDir = getParentPath(dep.paths.cmake);
17
+ });
18
+ if (built.length > 0) config.dependencyParameters = calculateDependencyParameters(config);
19
+ return built.length > 0;
20
+ }
@@ -0,0 +1,17 @@
1
+ import attachConanDependencies from './attachConanDependencies.js';
2
+ import { conanStageDir } from '../utils/conanImport.js';
3
+ import { conanManifestsStamp } from '../utils/conanStage.js';
4
+
5
+ const attachedFrom = new WeakMap();
6
+
7
+ // Attaches what a build staged since this config last attached: a process that loaded state before
8
+ // the build, such as a Metro server, would otherwise bind headers without the packages. Says whether
9
+ // it attached again.
10
+ export default function refreshConanDependencies(config) {
11
+ if (Object.keys(config.conanDependencies ?? {}).length === 0) return false;
12
+ const stamp = conanManifestsStamp(conanStageDir(config.paths.cache));
13
+ if (attachedFrom.get(config) === stamp) return false;
14
+ attachedFrom.set(config, stamp);
15
+ attachConanDependencies(config);
16
+ return true;
17
+ }
@@ -0,0 +1,4 @@
1
+ // The android image's NDK, and the API level every Android archive is built against: port archives
2
+ // and Conan packages link into the same library, so they move together.
3
+ export const ANDROID_NDK = '/opt/android-sdk/ndk/current';
4
+ export const ANDROID_API_LEVEL = 33;
@@ -1,10 +1,12 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import state from '../state/index.js';
4
+ import { followCargoLock, recordCargoLock } from './cargoLock.js';
3
5
  import { cargoTripleFor } from './cargoTarget.js';
4
6
  import { isMtWasm, assertMtRustToolchain, cargoTargetDirFor, cargoBuildInvocation } from './rustMt.js';
5
7
  import runCargo from './runCargo.js';
6
8
  import { sysrootFor } from './rustSysroot.js';
7
- import { relPath } from './rustBridgeGen.js';
9
+ import { keepSymbolName, relPath } from './rustBridgeGen.js';
8
10
  import writeIfChanged from './writeIfChanged.js';
9
11
 
10
12
  const SUPER = '_app_super';
@@ -56,6 +58,13 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
56
58
  if (isMt && !sysroot) assertMtRustToolchain();
57
59
 
58
60
  const superDir = `${root}/${SUPER}`;
61
+ // A link that cannot load the super whole pins its keep symbol, which reaches each bridge's; one object per
62
+ // bridge, so that symbol brings the bridge's registrations. A bridge an older crossbind wrote has none.
63
+ const hasKeepSymbol = (c) => {
64
+ const libRs = `${root}/${c}/src/lib.rs`;
65
+ return fs.existsSync(libRs) && fs.readFileSync(libRs, 'utf8').includes(`fn ${keepSymbolName(`${c}_crossbind_app`)}(`);
66
+ };
67
+ const kept = crates.filter(hasKeepSymbol);
59
68
  const manifest = [
60
69
  '# Generated by crossbind - do not edit. Bundles every app-local Rust surface.',
61
70
  '[package]',
@@ -73,6 +82,7 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
73
82
  '[profile.release]',
74
83
  'panic = "abort"',
75
84
  '',
85
+ ...crates.flatMap((c) => [`[profile.release.package.${c.replaceAll('_', '-')}-crossbind-app]`, 'codegen-units = 1', '']),
76
86
  '[workspace]',
77
87
  '',
78
88
  ].join('\n');
@@ -81,6 +91,12 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
81
91
  '// its init-array registrations) into this single staticlib.',
82
92
  ...crates.map((c) => `use ${c}_crossbind_app as _;`),
83
93
  '',
94
+ '#[no_mangle]',
95
+ `pub extern "C" fn ${keepSymbolName('crossbind_app_super')}() {`,
96
+ ` let keep: [extern "C" fn(); ${kept.length}] = [${kept.map((c) => `${c}_crossbind_app::${keepSymbolName(`${c}_crossbind_app`)}`).join(', ')}];`,
97
+ ' std::hint::black_box(keep);',
98
+ '}',
99
+ '',
84
100
  ].join('\n');
85
101
  writeIfChanged(`${superDir}/Cargo.toml`, manifest);
86
102
  writeIfChanged(`${superDir}/src/lib.rs`, lib);
@@ -96,6 +112,7 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
96
112
  manifestPath: `${superDir}/Cargo.toml`,
97
113
  sysroot,
98
114
  });
115
+ followCargoLock(state.config.paths.project, superDir);
99
116
  const build = runCargo(args, {
100
117
  rustflags, panic, allowUnstable, target,
101
118
  });
@@ -104,5 +121,6 @@ export default function buildAppRustCrates(target, cacheDir, cargoDependencies =
104
121
  }
105
122
  const libFile = `${targetDir}/${triple}/release/libcrossbind_app_super.a`;
106
123
  if (!fs.existsSync(libFile)) throw new Error(`crossbind: expected cargo output ${libFile} not found`);
124
+ recordCargoLock(state.config.paths.project, superDir);
107
125
  return [libFile];
108
126
  }
@@ -1,21 +1,112 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
+ import { createRequire } from 'node:module';
3
4
 
4
- const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.vue', '.svelte']);
5
- // Native projects and build outputs: large, and never the app's JavaScript.
6
- const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', 'build', 'ios', 'android', 'Pods', 'target']);
5
+ const SOURCE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.mts', '.cts', '.vue', '.svelte', '.html', '.astro', '.mdx']);
6
+ // Skip root outputs, while allowing source folders named e.g. src/build or src/android.
7
+ const ROOT_OUTPUTS = new Set(['dist', 'build', 'ios', 'android', 'Pods', 'target']);
8
+ const IMPORT = /\b(?:from|import\s*\(?|require\s*\()\s*(['"])([^'"\n]+)\1/g;
9
+ const packageName = (specifier) => specifier.startsWith('@') ? specifier.split('/').slice(0, 2).join('/') : specifier.split('/')[0];
7
10
 
8
- // The app's own JavaScript and TypeScript sources, whose import statements say what the app uses.
11
+ function sourceFile(file) {
12
+ const candidates = [file, ...[...SOURCE_EXTENSIONS].map((ext) => `${file}${ext}`), ...[...SOURCE_EXTENSIONS].map((ext) => path.join(file, `index${ext}`))];
13
+ return candidates.find((candidate) => {
14
+ if (!SOURCE_EXTENSIONS.has(path.extname(candidate))) return false;
15
+ try { return fs.statSync(candidate, { throwIfNoEntry: false })?.isFile(); } catch { return false; }
16
+ });
17
+ }
18
+
19
+ // require.resolve selects CommonJS conditions. Also read runtime export branches: a bundler may
20
+ // choose an import/browser entry whose header imports are absent from the CommonJS entry.
21
+ function dependencySources(specifier, importer) {
22
+ const name = packageName(specifier);
23
+ const subpath = specifier.slice(name.length);
24
+ for (let dir = path.dirname(importer); ; dir = path.dirname(dir)) {
25
+ const root = path.join(dir, 'node_modules', name);
26
+ const manifest = path.join(root, 'package.json');
27
+ if (fs.existsSync(manifest)) {
28
+ try {
29
+ const pkg = JSON.parse(fs.readFileSync(manifest, 'utf8'));
30
+ let entry = pkg.exports;
31
+ let wildcard;
32
+ if (entry && typeof entry === 'object' && !Array.isArray(entry) && Object.keys(entry).some((key) => key.startsWith('.'))) {
33
+ const key = `.${subpath}`;
34
+ entry = entry[key];
35
+ if (entry === undefined) {
36
+ for (const pattern of Object.keys(pkg.exports).filter((key) => key.includes('*')).sort((a, b) => b.length - a.length)) {
37
+ const [before, after] = pattern.split('*');
38
+ if (key.startsWith(before) && key.endsWith(after) && key.length >= before.length + after.length) {
39
+ wildcard = key.slice(before.length, key.length - after.length);
40
+ entry = pkg.exports[pattern];
41
+ break;
42
+ }
43
+ }
44
+ }
45
+ } else if (subpath) entry = undefined;
46
+ const targets = (value) => typeof value === 'string' ? [value]
47
+ : Array.isArray(value) ? value.flatMap(targets)
48
+ : value && typeof value === 'object' ? Object.entries(value).filter(([condition]) => condition !== 'types').flatMap(([, branch]) => targets(branch)) : [];
49
+ const entries = pkg.exports ? targets(entry)
50
+ : subpath ? [`.${subpath}`] : [pkg.module, typeof pkg.browser === 'string' ? pkg.browser : null, pkg.main ?? 'index.js'];
51
+ return entries.filter(Boolean).flatMap((target) => {
52
+ const file = path.resolve(root, wildcard === undefined ? target : target.replaceAll('*', wildcard));
53
+ if (path.relative(root, file).startsWith('..')) return [];
54
+ return sourceFile(file) ?? [];
55
+ });
56
+ } catch { return []; }
57
+ }
58
+ if (path.dirname(dir) === dir) return [];
59
+ }
60
+ }
61
+
62
+ // Own sources and the dependency modules they reach. Follow imports rather than walking every installed package.
9
63
  export default function appSourceFiles(projectDir) {
10
64
  const files = [];
11
65
  const walk = (dir) => {
12
66
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
13
- if (entry.name.startsWith('.') || SKIPPED_DIRECTORIES.has(entry.name)) continue;
67
+ if (entry.name.startsWith('.') || entry.name === 'node_modules' || (dir === projectDir && ROOT_OUTPUTS.has(entry.name))) continue;
14
68
  const full = path.join(dir, entry.name);
15
69
  if (entry.isDirectory()) walk(full);
16
70
  else if (entry.isFile() && SOURCE_EXTENSIONS.has(path.extname(entry.name))) files.push(full);
17
71
  }
18
72
  };
19
- walk(projectDir);
73
+ if (projectDir && fs.existsSync(projectDir)) walk(projectDir);
74
+ let dependencies = {};
75
+ try {
76
+ const pkg = JSON.parse(fs.readFileSync(path.join(projectDir, 'package.json'), 'utf8'));
77
+ dependencies = { ...pkg.dependencies, ...pkg.optionalDependencies, ...pkg.peerDependencies };
78
+ } catch { /* A source-only project has no package manifest. */ }
79
+ const ownFiles = new Set(files);
80
+ const visited = new Set(files.map((file) => fs.realpathSync(file)));
81
+ for (let i = 0; i < files.length; i += 1) {
82
+ const importer = files[i];
83
+ const require = createRequire(importer);
84
+ const text = fs.readFileSync(importer, 'utf8');
85
+ for (const [, , specifier] of text.matchAll(IMPORT)) {
86
+ const relative = specifier.startsWith('.');
87
+ if (!relative && ownFiles.has(importer) && !Object.hasOwn(dependencies, packageName(specifier))) continue;
88
+ const resolvedFiles = relative ? [] : dependencySources(specifier, importer);
89
+ try { resolvedFiles.push(require.resolve(specifier)); } catch {
90
+ if (relative) resolvedFiles.push(sourceFile(path.resolve(path.dirname(importer), specifier)));
91
+ }
92
+ for (const resolved of resolvedFiles) {
93
+ if (!resolved || !SOURCE_EXTENSIONS.has(path.extname(resolved)) || !fs.existsSync(resolved)) continue;
94
+ const real = fs.realpathSync(resolved);
95
+ if (visited.has(real)) continue;
96
+ visited.add(real);
97
+ files.push(resolved);
98
+ }
99
+ }
100
+ }
20
101
  return files;
21
102
  }
103
+
104
+ // Whether a changed file is one of those sources.
105
+ export function isAppSource(projectDir, file) {
106
+ const relative = path.relative(projectDir, file);
107
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) return false;
108
+ const parts = relative.split(path.sep);
109
+ return SOURCE_EXTENSIONS.has(path.extname(file))
110
+ && parts.every((part) => !part.startsWith('.'))
111
+ && !ROOT_OUTPUTS.has(parts[0]);
112
+ }
@@ -0,0 +1,12 @@
1
+ // Every archive linked into one wasm module must agree on exception handling, SIMD, threads and
2
+ // 64-bit memory, or wasm-ld refuses it. Port recipes, the app's own libraries and Conan packages all
3
+ // take their flags from here.
4
+ export const WASM_EXCEPTION_FLAGS = ['-fwasm-exceptions'];
5
+
6
+ export function targetArchiveFlags(target) {
7
+ return [
8
+ ...(target.runtime === 'mt' ? ['-pthread'] : []),
9
+ ...(target.platform === 'wasm' ? ['-msimd128'] : []),
10
+ ...(target.platform === 'wasm' && target.arch === 'wasm64' ? ['-sMEMORY64=1'] : []),
11
+ ];
12
+ }
@@ -0,0 +1,79 @@
1
+ // Headers of dependencies that a build binds whole (export.bindings.headers), the way an app's
2
+ // `import * as x from '<pkg>/<header>'` would: a package that ships prebuilt bindings has no app
3
+ // importing them. An entry is a header path, or a package name standing for every header the package
4
+ // lists in export.publicHeaders.
5
+
6
+ import { releaseTargetOf } from './targets.js';
7
+
8
+ const isName = (entry) => typeof entry === 'string' && entry.length > 0;
9
+
10
+ function publicHeadersOf(packageName, dependencies) {
11
+ const owner = dependencies.find((d) => [d.package?.name, d.general?.alias?.package].includes(packageName)
12
+ && d.export?.publicHeaders?.length);
13
+ if (!owner) {
14
+ throw new Error(`crossbind: export.bindings.headers names ${packageName}, but no dependency of that name lists export.publicHeaders.`);
15
+ }
16
+ return owner.export.publicHeaders.map((header) => `${packageName}/${header}`);
17
+ }
18
+
19
+ export function boundHeaderSpecifiers({ export: exportConfig, ext, allDependencies }) {
20
+ const entries = exportConfig.bindings?.headers ?? [];
21
+ if (!Array.isArray(entries) || !entries.every(isName)) {
22
+ throw new Error('crossbind: export.bindings.headers must list header paths or package names.');
23
+ }
24
+ const isHeader = new RegExp(`\\.(${ext.header.join('|')})$`);
25
+ return [...new Set(entries.flatMap((entry) => (isHeader.test(entry) ? [entry] : publicHeadersOf(entry, allDependencies))))];
26
+ }
27
+
28
+ // The bridges come from one target's headers and macros. A linux target goes first, since every host
29
+ // builds one, so a package comes out the same on any machine.
30
+ const BRIDGE_PLATFORM_ORDER = ['linux', 'linuxmusl', 'win32', 'darwin'];
31
+ const bridgeRank = ({ platform }) => {
32
+ const rank = BRIDGE_PLATFORM_ORDER.indexOf(platform);
33
+ return rank < 0 ? BRIDGE_PLATFORM_ORDER.length : rank;
34
+ };
35
+ export const bridgeTargetOrder = (targets) => [...targets].sort((a, b) => bridgeRank(a) - bridgeRank(b));
36
+
37
+ // A macOS host without Docker builds no linux addon, so the targets of the runtime environment it does not
38
+ // build are candidates too: a linux package installed beside it gives the same bridges a linux build made.
39
+ export const bridgeTargets = (buildTargets, allTargets) => bridgeTargetOrder([
40
+ ...buildTargets,
41
+ ...allTargets.filter((target) => target.platform !== 'wasm' && !buildTargets.includes(target)
42
+ && buildTargets.some((built) => built.runtimeEnv === target.runtimeEnv)),
43
+ ]);
44
+
45
+ // A Node build reads the app's headers for the first of its own targets, in its release form, since Conan
46
+ // packages are staged for release targets only.
47
+ export const nodeBridgeTarget = (targets) => releaseTargetOf(bridgeTargetOrder(targets)[0]);
48
+
49
+ function resolveOrNull(resolve, specifier, target) {
50
+ try {
51
+ return resolve(specifier, target);
52
+ } catch {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ // A desktop build has no target that every platform package covers, so each header resolves under
58
+ // the first target whose dependency ships it.
59
+ export function resolveBoundHeaders(specifiers, targets, resolve) {
60
+ return specifiers.map((specifier) => {
61
+ const target = targets.find((t) => resolveOrNull(resolve, specifier, t));
62
+ if (!target) {
63
+ throw new Error(`crossbind: export.bindings.headers: no dependency ships ${specifier} for ${targets.map((t) => t.path).join(', ')}.`);
64
+ }
65
+ return { specifier, file: resolve(specifier, target), target };
66
+ });
67
+ }
68
+
69
+ const OBJECT_LIKE_RENAME = /^\s*#\s*define\s+([A-Za-z_]\w*)\s+([A-Za-z_]\w*)\s*$/gm;
70
+ const localOf = (name) => (typeof name === 'string' ? name : name.local);
71
+
72
+ // A header that renames a bound function with an object-like macro (GNU libiconv binds libiconv_open as iconv_open)
73
+ // is called by the macro's name, so the entry exports that name too.
74
+ export function headerAliases(headerText, names) {
75
+ const bound = new Set(names.map(localOf));
76
+ return [...headerText.matchAll(OBJECT_LIKE_RENAME)]
77
+ .filter(([, local, wire]) => bound.has(wire) && !bound.has(local))
78
+ .map(([, local, wire]) => ({ local, wire }));
79
+ }