crossbind 2.0.0-beta.69 → 2.0.0-beta.73

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 (55) hide show
  1. package/package.json +3 -2
  2. package/src/actions/addExtension.js +8 -4
  3. package/src/actions/buildApp.js +38 -9
  4. package/src/actions/buildDependencies.js +3 -3
  5. package/src/actions/createImportedBridges.js +10 -8
  6. package/src/actions/createInterface.js +23 -21
  7. package/src/actions/createLib.js +3 -3
  8. package/src/actions/extensionCommands.js +2 -1
  9. package/src/actions/getAllBridges.js +20 -5
  10. package/src/actions/getDependLibs.js +6 -0
  11. package/src/actions/getLinkInputs.js +2 -1
  12. package/src/actions/gradleProject.js +13 -2
  13. package/src/actions/licenses.js +30 -24
  14. package/src/actions/{prepareConanDependencies.js → preparePackageSources.js} +13 -12
  15. package/src/actions/writeRuntimeEntry.js +2 -2
  16. package/src/actions/xcodeProject.js +12 -6
  17. package/src/app/index.d.ts +19 -0
  18. package/src/app/index.js +32 -7
  19. package/src/assets/node-import-hooks.mjs +7 -6
  20. package/src/bin.js +37 -17
  21. package/src/index.js +4 -3
  22. package/src/integration/getCrossbindScript.js +3 -2
  23. package/src/integration/getDependFilePath.js +10 -10
  24. package/src/state/{attachConanDependencies.js → attachPackageSources.js} +20 -16
  25. package/src/state/index.js +14 -11
  26. package/src/state/loadConfig.js +10 -4
  27. package/src/state/refreshPackageSources.js +19 -0
  28. package/src/utils/appApiHeader.js +5 -5
  29. package/src/utils/appBuild.js +8 -0
  30. package/src/utils/appExtensions.js +44 -28
  31. package/src/utils/appOptions.js +31 -10
  32. package/src/utils/appProject.js +24 -0
  33. package/src/utils/appRustCrates.js +2 -2
  34. package/src/utils/bridgeTools.js +6 -1
  35. package/src/utils/cppDts.js +11 -11
  36. package/src/utils/extensionSteps.js +38 -1
  37. package/src/utils/nativeImports.js +9 -2
  38. package/src/utils/nodeDevBuild.js +2 -1
  39. package/src/utils/packageSourceApi.js +87 -0
  40. package/src/utils/packageSourceImport.js +69 -0
  41. package/src/utils/packageSourceInstall.js +162 -0
  42. package/src/utils/{conanStage.js → packageSourceStage.js} +64 -90
  43. package/src/utils/packageSources.js +108 -0
  44. package/src/utils/printableText.js +5 -0
  45. package/src/utils/resolveAppHost.js +10 -0
  46. package/src/utils/runPackageSource.js +251 -0
  47. package/src/utils/rustBridgeGen.js +3 -3
  48. package/src/utils/targetToolchain.js +77 -0
  49. package/src/utils/xcframeworkSlice.js +6 -0
  50. package/src/state/refreshConanDependencies.js +0 -17
  51. package/src/utils/conanDependencies.js +0 -66
  52. package/src/utils/conanImport.js +0 -58
  53. package/src/utils/conanInstall.js +0 -154
  54. package/src/utils/conanProfile.js +0 -203
  55. package/src/utils/runConan.js +0 -257
@@ -1,7 +1,8 @@
1
1
  import fs from 'node:fs';
2
2
  import state, { setAllDependecyPaths } from '../state/index.js';
3
- import refreshConanDependencies from '../state/refreshConanDependencies.js';
4
- import installConanPackages from '../utils/conanInstall.js';
3
+ import refreshPackageSources from '../state/refreshPackageSources.js';
4
+ import installPackageSources from '../utils/packageSourceInstall.js';
5
+ import { declaredSources } from '../utils/packageSources.js';
5
6
  import { releaseTargetOf, HOST_BUILT_PLATFORMS } from '../utils/targets.js';
6
7
  import createXCFramework from './createXCFramework.js';
7
8
 
@@ -11,11 +12,11 @@ const isBuiltHere = (target) => process.platform === 'darwin' || !HOST_BUILT_PLA
11
12
 
12
13
  // iOS headers and archives come out of an xcframework per library with a slice per SDK (state/index.js).
13
14
  // One older than an archive it wraps was made before the package was staged again.
14
- function makeConanXCFrameworks(iosTargets) {
15
+ function makeXCFrameworks(iosTargets) {
15
16
  const archiveOf = (dependency, target, name) => `${dependency.paths.output}/prebuilt/${target.path}/lib/lib${name}.a`;
16
17
  const isFresh = (dependency, name) => mtimeOf(`${dependency.paths.project}/${name}.xcframework/Info.plist`)
17
18
  >= Math.max(...iosTargets.map((target) => mtimeOf(archiveOf(dependency, target, name))));
18
- state.config.allDependencies.filter((dependency) => dependency.general.conan).forEach((dependency) => {
19
+ state.config.allDependencies.filter((dependency) => dependency.general.packageSource).forEach((dependency) => {
19
20
  // A library its recipe names on another platform only has no slice here.
20
21
  const libName = dependency.export.libName.filter((name) => iosTargets.every((target) => fs.existsSync(archiveOf(dependency, target, name))));
21
22
  if (libName.every((name) => isFresh(dependency, name))) return;
@@ -29,11 +30,11 @@ function makeConanXCFrameworks(iosTargets) {
29
30
  });
30
31
  }
31
32
 
32
- // Conan packages are staged before any header is read: SWIG parses headers that include theirs, and an
33
- // interface made without its includes stays that way until the header itself changes. Release trees
34
- // only - a debug build links them like any prebuilt.
35
- export default async function prepareConanDependencies(targets) {
36
- if (Object.keys(state.config.conanDependencies ?? {}).length === 0) return;
33
+ // The packages of a project's sources are staged before any header is read: SWIG parses headers that
34
+ // include theirs, and an interface made without its includes stays that way until the header itself
35
+ // changes. Release trees only - a debug build links them like any prebuilt.
36
+ export default async function preparePackageSources(targets) {
37
+ if (declaredSources(state.config).length === 0) return;
37
38
  // createBridgeFile reads every header for the first wasm target unless it is told otherwise, as it
38
39
  // is by Metro, which builds the React Native bridges for the platform's own target, and by a Node
39
40
  // build, which reads them for one of its own (nodeBridgeTarget).
@@ -43,9 +44,9 @@ export default async function prepareConanDependencies(targets) {
43
44
  const releaseTargets = [...new Map([...targets, bridgeTarget, ...iosTargets].filter(Boolean).map((target) => [
44
45
  target.releasePath, releaseTargetOf(target),
45
46
  ])).values()].filter(isBuiltHere);
46
- await installConanPackages(state.config, releaseTargets);
47
+ await installPackageSources(state.config, releaseTargets);
47
48
  // Also when another process did the staging: this one may have loaded before the manifests existed.
48
- refreshConanDependencies(state.config);
49
+ refreshPackageSources(state.config);
49
50
  setAllDependecyPaths();
50
- if (iosTargets.length > 0) makeConanXCFrameworks(releaseTargets.filter((target) => target.platform === 'ios'));
51
+ if (iosTargets.length > 0) makeXCFrameworks(releaseTargets.filter((target) => target.platform === 'ios'));
51
52
  }
@@ -50,7 +50,7 @@ function nativesOf(headers, imports, dir) {
50
50
  }
51
51
 
52
52
  // The module `node --import` loads ahead of the app, so the app imports the bound headers themselves.
53
- export function writeImportHooks(entry, headers, imports, extensions) {
53
+ export function writeImportHooks(entry, headers, imports, extensions, schemes) {
54
54
  // Node runs the register file from its real path, so the files it names are relative to that.
55
55
  const dir = upath.normalize(fs.realpathSync(upath.dirname(entry)));
56
56
  const natives = nativesOf(headers, imports, dir);
@@ -59,7 +59,7 @@ export function writeImportHooks(entry, headers, imports, extensions) {
59
59
  '// Generated by crossbind - do not edit.',
60
60
  "import registerNativeImports from './hooks.mjs';",
61
61
  '',
62
- `registerNativeImports(new URL('./${upath.basename(entry)}', import.meta.url), ${JSON.stringify({ extensions, natives }, null, 4)});`,
62
+ `registerNativeImports(new URL('./${upath.basename(entry)}', import.meta.url), ${JSON.stringify({ extensions, schemes, natives }, null, 4)});`,
63
63
  '',
64
64
  ].join('\n'));
65
65
  }
@@ -151,8 +151,9 @@ function objects(name, targetSettings, hasAssets, hasPrivacy, added) {
151
151
  };
152
152
  }
153
153
 
154
- // The asset catalog: the app's icon, from which Xcode makes every size, and the launch screen's color.
155
- function writeAssets(catalog, icon, background) {
154
+ // The asset catalog: the app's icon, from which Xcode makes every size, and the launch screen's color, with the one of
155
+ // the dark appearance where there is one.
156
+ function writeAssets(catalog, icon, background, darkBackground) {
156
157
  const info = { author: 'xcode', version: 1 };
157
158
  const assetSet = (set, contents) => {
158
159
  fs.mkdirSync(`${catalog}/${set}`, { recursive: true });
@@ -163,9 +164,13 @@ function writeAssets(catalog, icon, background) {
163
164
  assetSet('AppIcon.appiconset', { images: [{ filename: 'icon.png', idiom: 'universal', platform: 'ios', size: '1024x1024' }] });
164
165
  fs.writeFileSync(`${catalog}/AppIcon.appiconset/icon.png`, icon);
165
166
  }
167
+ const colorOf = (rgb) => {
168
+ const [red, green, blue] = [1, 3, 5].map((at) => `0x${rgb.slice(at, at + 2).toUpperCase()}`);
169
+ return { 'color-space': 'srgb', components: { alpha: '1.000', red, green, blue } };
170
+ };
166
171
  if (background) {
167
- const [red, green, blue] = [1, 3, 5].map((at) => `0x${background.slice(at, at + 2).toUpperCase()}`);
168
- assetSet(`${LAUNCH_COLOR}.colorset`, { colors: [{ color: { 'color-space': 'srgb', components: { alpha: '1.000', red, green, blue } }, idiom: 'universal' }] });
172
+ const dark = darkBackground ? [{ appearances: [{ appearance: 'luminosity', value: 'dark' }], color: colorOf(darkBackground), idiom: 'universal' }] : [];
173
+ assetSet(`${LAUNCH_COLOR}.colorset`, { colors: [{ color: colorOf(background), idiom: 'universal' }, ...dark] });
169
174
  }
170
175
  }
171
176
 
@@ -176,7 +181,8 @@ function writeAssets(catalog, icon, background) {
176
181
  // where one of them asks for it.
177
182
  export default function writeXcodeProject(to, {
178
183
  parts, identity, label, version, build = version, icon = null, urlSchemes = [], fileTypes = [], links = [], activityTypes = [], shortcutItems = [],
179
- background = null, usage = {}, encryption = null, multipleWindows = false, privacy = {}, entitlements = {}, plist = [], targets = [], appGroup = null,
184
+ background = null, darkBackground = null, usage = {}, encryption = null, multipleWindows = false, privacy = {}, entitlements = {}, plist = [], targets = [],
185
+ appGroup = null,
180
186
  }) {
181
187
  const { name } = state.config.general;
182
188
  if (parts.length === 0) throw new Error(`crossbind: no part of the iOS app ${name} is built, so there is no Xcode project to write.`);
@@ -206,7 +212,7 @@ export default function writeXcodeProject(to, {
206
212
 
207
213
  fs.writeFileSync(`${to}/main.m`, MAIN);
208
214
  fs.writeFileSync(`${to}/Info.plist`, info);
209
- writeAssets(`${to}/Assets.xcassets`, icon, background);
215
+ writeAssets(`${to}/Assets.xcassets`, icon, background, darkBackground);
210
216
  const hasPrivacy = Object.keys(privacy).length > 0;
211
217
  if (hasPrivacy) fs.writeFileSync(`${to}/PrivacyInfo.xcprivacy`, privacyManifest(privacy));
212
218
  const hasEntitlements = Object.keys(signed).length > 0;
@@ -24,6 +24,23 @@ export declare const app: {
24
24
  launchArgs(): Promise<Launch>;
25
25
  };
26
26
 
27
+ /** Where the app runs, as its host tells the page before the page's own scripts run; the same through the app's run. */
28
+ export interface Device {
29
+ /** The platform the app is built for. */
30
+ readonly platform: 'macos' | 'windows' | 'linux' | 'ios' | 'android';
31
+ /** The arch the app's process runs as. */
32
+ readonly arch: 'arm64' | 'x64';
33
+ /** What the device is to its user as the app starts: a phone; a tablet, an iPad or an Android device whose screen's shorter side has 600dp; or a desktop, a computer, a Chromebook or a Mac that runs an iOS app. */
34
+ readonly formFactor: 'phone' | 'tablet' | 'desktop';
35
+ /** The system's name and version, as its maker numbers it: iOS or iPadOS 18.4, Android 15 with its API level, macOS 26.1, Windows 10.0.26100, or a Linux distribution's, as Ubuntu 24.04, else its kernel's release. */
36
+ readonly os: { readonly name: string; readonly version: string; readonly apiLevel?: number };
37
+ /** Whether the iOS Simulator or an Android emulator runs the app. */
38
+ readonly isVirtual: boolean;
39
+ }
40
+
41
+ /** Where the app runs, read at once, as the page starts; reading it outside an app throws. */
42
+ export declare const device: Device;
43
+
27
44
  /** What windows.set() changes of an open window: sizes and places in logical pixels, a page's size without the frame, and x and y the frame's place from the top left of the main screen, given together. A limit of 0 takes it away. */
28
45
  export interface WindowChanges {
29
46
  title?: string;
@@ -57,6 +74,8 @@ export interface WindowOptions extends Omit<WindowChanges, 'center' | 'minWidth'
57
74
  backgroundColor?: string;
58
75
  /** false opens a window without a title bar and its buttons, which the page moves with data-crossbind-drag or windows.startDrag(). */
59
76
  frame?: boolean;
77
+ /** 'hidden' lays a macOS window's page under its title bar, whose buttons stay at its top left, as app.window.titleBar does; Windows and Linux keep their title bar and list it in ignored. */
78
+ titleBar?: 'visible' | 'hidden';
60
79
  /** The id of the window it stays above, and closes with. */
61
80
  parent?: number;
62
81
  /** With a parent: a sheet of the parent's on macOS, and on Windows and Linux a window the parent takes no input under until it closes. */
package/src/app/index.js CHANGED
@@ -1,15 +1,16 @@
1
1
  // What an app's host does for its page: the app's folders, quitting, its windows, the events of the app's native code and
2
- // the functions of its extensions, by their names. Each call goes to the host of an app (target.runtimeEnv: 'app'); in any
3
- // other build it rejects, or events.on throws. A page imports these from crossbind/app, which an app's bundler plugin
4
- // takes from the app host, with what crossbind's own extensions give the page, as crossbind/app/core alone holds these.
2
+ // the functions of its extensions, by their names. Each call goes to the host of an app, which crossbind app builds; in a
3
+ // browser it rejects, or events.on throws. A page imports these from crossbind/app, which an app's bundler plugin takes
4
+ // from the app host, with what crossbind's own extensions give the page, as crossbind/app/core alone holds these.
5
5
 
6
- // The app's crossbind.js, which initNative() loads, hands the calls to the host.
6
+ // The app's crossbind.js, which initNative() loads, hands the calls to the host. A browser has crossbind's glue as well,
7
+ // but no token of a host, and none of its functions.
7
8
  function hostOf(member, name) {
8
9
  const host = globalThis.Crossbind?.[member];
9
10
  if (host) return host;
11
+ if (!globalThis.__crossbind_token && !globalThis.Crossbind?.app) throw new Error(`crossbind: ${name} works in an app alone, which crossbind app builds`);
10
12
  if (globalThis.Crossbind) throw new Error(`crossbind: this app's host is older than crossbind/app; update @crossbind/core-app-host to call ${name}`);
11
- if (globalThis.__crossbind_token) throw new Error(`crossbind: ${name} works once the page has called initNative()`);
12
- throw new Error(`crossbind: ${name} works in an app alone, whose crossbind.config.js sets target.runtimeEnv: 'app'`);
13
+ throw new Error(`crossbind: ${name} works once the page has called initNative()`);
13
14
  }
14
15
 
15
16
  const call = (name, options) => hostOf('app', name)(name, options);
@@ -39,8 +40,9 @@ const WINDOW_SHARED = {
39
40
  };
40
41
  const WINDOW_SET = { ...WINDOW_SHARED, center: 'boolean' };
41
42
  const WINDOW_OPEN = {
42
- ...WINDOW_SHARED, path: 'string', name: 'string', backgroundColor: 'string', frame: 'boolean', parent: 'number', modal: 'boolean',
43
+ ...WINDOW_SHARED, path: 'string', name: 'string', backgroundColor: 'string', frame: 'boolean', titleBar: 'string', parent: 'number', modal: 'boolean',
43
44
  };
45
+ const TITLE_BARS = ['visible', 'hidden'];
44
46
  const MAX_WINDOW_SIZE = 16384;
45
47
  const MAX_WINDOW_PLACE = 1000000;
46
48
  // The size a window opens at without one.
@@ -70,6 +72,10 @@ function checkedWindow(name, options, kinds) {
70
72
  throw new TypeError(`crossbind: ${name}'s path ${JSON.stringify(path)} is no path of the app's own pages, as /settings.html is.`);
71
73
  }
72
74
  if (backgroundColor !== undefined && !COLOR.test(backgroundColor)) throw new TypeError(`crossbind: ${name}'s backgroundColor is no color of the form #rrggbb.`);
75
+ if (options.titleBar !== undefined && !TITLE_BARS.includes(options.titleBar)) throw new TypeError(`crossbind: ${name}'s titleBar is neither 'visible' nor 'hidden'.`);
76
+ if (options.frame === false && options.titleBar === 'hidden') {
77
+ throw new TypeError(`crossbind: ${name} hides the title bar of a window with a frame alone: drop titleBar, or frame: false.`);
78
+ }
73
79
  if (parent !== undefined && !isWindowId(parent)) throw new TypeError(`crossbind: ${name}'s parent is no window's id.`);
74
80
  if (options.modal && parent === undefined) throw new TypeError(`crossbind: ${name} opens a modal window above a parent: give it parent, the id of a window.`);
75
81
  if (name === 'windows.open') checkedLimits(options);
@@ -96,6 +102,25 @@ function windowOf(name, id) {
96
102
  return { id };
97
103
  }
98
104
 
105
+ // What the app's host tells the page of where the app runs, from before the page's own scripts on.
106
+ function deviceOf(name) {
107
+ const device = globalThis.__crossbind_device;
108
+ if (device) return device[name];
109
+ if (globalThis.__crossbind_token) throw new Error(`crossbind: this app's host is older than crossbind/app; update @crossbind/core-app-host to read device.${name}`);
110
+ throw new Error(`crossbind: device.${name} works in an app alone, which crossbind app builds`);
111
+ }
112
+
113
+ // Where the app runs, the same through its run, which the page reads at once, as it starts: the platform and the arch
114
+ // the app is built for, what the device is to its user, its system's name and version, and whether a simulator or an
115
+ // emulator runs the app.
116
+ export const device = Object.freeze({
117
+ get platform() { return deviceOf('platform'); },
118
+ get arch() { return deviceOf('arch'); },
119
+ get formFactor() { return deviceOf('formFactor'); },
120
+ get os() { return deviceOf('os'); },
121
+ get isVirtual() { return deviceOf('isVirtual'); },
122
+ });
123
+
99
124
  export const app = {
100
125
  // The app's own folders, made when first asked for: { data, cache, logs }.
101
126
  paths: async () => call('app.paths', {}),
@@ -5,7 +5,6 @@ import { registerHooks } from 'node:module';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath, pathToFileURL } from 'node:url';
7
7
 
8
- const SCHEME = /^(cargo|conan):/;
9
8
  const isBare = (specifier) => !/^(\.{1,2}\/|\/|[A-Za-z]:[\\/]|file:)/.test(specifier);
10
9
 
11
10
  function notBound(what, parentURL, cause) {
@@ -17,23 +16,25 @@ const exportOf = (name) => (typeof name === 'string' ? name : `${name.wire} as $
17
16
  const proxyOf = (entry, names) => `export { ${['initNative', 'AllSymbols', ...names.map(exportOf)].join(', ')} } from ${JSON.stringify(entry)};\n`;
18
17
 
19
18
  // Another hook may still serve it; Node's own resolver hands an unknown scheme on unresolved.
20
- function resolveUnbound(specifier, context, nextResolve) {
19
+ function resolveUnbound(specifier, context, nextResolve, scheme) {
21
20
  let resolved;
22
21
  try {
23
22
  resolved = nextResolve(specifier, context);
24
23
  } catch (error) {
25
24
  throw notBound(specifier, context.parentURL, error);
26
25
  }
27
- if (SCHEME.test(resolved.url)) throw notBound(specifier, context.parentURL);
26
+ if (scheme.test(resolved.url)) throw notBound(specifier, context.parentURL);
28
27
  return resolved;
29
28
  }
30
29
 
31
30
  // `natives` lists each bound file, relative to the entry, with the bare specifiers that name it and the names it
32
- // exports, a name the entry exports under another as { local, wire }.
33
- export default function registerNativeImports(entry, { extensions, natives }) {
31
+ // exports, a name the entry exports under another as { local, wire }. `schemes` are those an import names a store
32
+ // by: the two every build has, for a register file an older build wrote, which names none.
33
+ export default function registerNativeImports(entry, { extensions, schemes = ['cargo', 'conan'], natives }) {
34
34
  const entryUrl = new URL(entry).href;
35
35
  const dir = path.dirname(fileURLToPath(entryUrl));
36
36
  const isNative = new RegExp(`\\.(${extensions.join('|')})$`);
37
+ const scheme = new RegExp(`^(${schemes.join('|')}):`);
37
38
  const byFile = new Map();
38
39
  const bySpecifier = new Map();
39
40
  natives.forEach(({ file, specifiers, names }) => {
@@ -46,7 +47,7 @@ export default function registerNativeImports(entry, { extensions, natives }) {
46
47
  resolve(specifier, context, nextResolve) {
47
48
  const url = bySpecifier.get(specifier);
48
49
  if (url) return { url, format: 'module', shortCircuit: true };
49
- if (isBare(specifier) && (SCHEME.test(specifier) || isNative.test(specifier))) return resolveUnbound(specifier, context, nextResolve);
50
+ if (isBare(specifier) && (scheme.test(specifier) || isNative.test(specifier))) return resolveUnbound(specifier, context, nextResolve, scheme);
50
51
  return nextResolve(specifier, context);
51
52
  },
52
53
  load(url, context, nextLoad) {
package/src/bin.js CHANGED
@@ -15,6 +15,7 @@ import buildWasiCommand from './actions/buildWasiCommand.js';
15
15
  import buildNode, { addonsInPackages, bundleNodeLoader, publishNodeData } from './actions/buildNode.js';
16
16
  import buildNativeCommand from './actions/buildNativeCommand.js';
17
17
  import buildApp, { appOutputPath, assertAppRunner, publishApp } from './actions/buildApp.js';
18
+ import { assertAppProject } from './utils/appProject.js';
18
19
  import {
19
20
  makeUploadKey, releaseSigningEnv, showUploadKeyPassword, useUploadKey,
20
21
  } from './actions/uploadKey.js';
@@ -44,7 +45,8 @@ import {
44
45
  RUNNER_PLATFORMS, RUNNER_ROLES, connectCommands, deleteRunner, deploySteps, initRunner, newRunnerToken, runnerHostPort, startRunner,
45
46
  } from './actions/runnerCommands.js';
46
47
  import { cargoHome } from './utils/runCargo.js';
47
- import { conanRoot } from './utils/runConan.js';
48
+ import { containerRootOf, sourceRoot } from './utils/runPackageSource.js';
49
+ import { importSchemesOf, sourceNamesOf } from './utils/packageSources.js';
48
50
  import { cleanDepsCache } from './utils/dependencyRebuild.js';
49
51
  import collectLicenseRows from './actions/licenses.js';
50
52
  import { formatNoticesMarkdown, formatCycloneDxSbom, validateSpdx } from './utils/licenseReport.js';
@@ -112,13 +114,34 @@ function declaredSbomPath(projectDir) {
112
114
  // An app's page is the project's Vite build, which @crossbind/plugin-vite makes into the app of the platform named.
113
115
  const APP_PLATFORMS = ['android', 'ios', 'linux', 'macos', 'windows'];
114
116
 
117
+ // Vite's CLI beside the entry of the package vite resolves to, where Vite keeps it and vite-plus-core too, whose vp runs
118
+ // it from there. An entry outside the package is another one's, which a lookup past it found.
119
+ function viteCliBesideEntry(require, packageDir) {
120
+ try {
121
+ const entry = require.resolve('vite');
122
+ const cli = path.join(path.dirname(entry), 'cli.js');
123
+ return entry.startsWith(`${packageDir}${path.sep}`) && fs.existsSync(cli) ? cli : null;
124
+ } catch {
125
+ return null;
126
+ }
127
+ }
128
+
115
129
  function projectVite() {
130
+ const require = createRequire(`${state.config.paths.project}/package.json`);
131
+ let manifestPath;
116
132
  try {
117
- const manifest = createRequire(`${state.config.paths.project}/package.json`).resolve('vite/package.json');
118
- return path.join(path.dirname(manifest), JSON.parse(fs.readFileSync(manifest, 'utf8')).bin.vite);
133
+ manifestPath = require.resolve('vite/package.json');
119
134
  } catch {
120
135
  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.");
121
136
  }
137
+ // A workspace can map vite to a package of Vite's API alone, as vite-plus maps it to vite-plus-core.
138
+ const { name, version, bin } = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
139
+ if (bin?.vite) return path.join(path.dirname(manifestPath), bin.vite);
140
+ const cli = viteCliBesideEntry(require, path.dirname(manifestPath));
141
+ if (!cli) {
142
+ throw new Error(`crossbind: crossbind app runs the project's Vite, and its vite resolves to ${name} ${version}, which has neither a vite command nor Vite's cli.js beside its entry. Have vite resolve to Vite itself for the project, with an override where its workspace maps vite to another package.`);
143
+ }
144
+ return cli;
122
145
  }
123
146
 
124
147
  // The project's Vite, as `crossbind app dev`, `build` and `publish` run it, and its exit code once it ends. A signal sent
@@ -136,11 +159,7 @@ function runVite(args, env) {
136
159
  });
137
160
  }
138
161
 
139
- function assertAppProject() {
140
- if (state.config.target.runtimeEnv !== 'app') {
141
- 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.");
142
- }
143
- }
162
+ const assertAppCommand = () => assertAppProject(state.config, 'crossbind app builds an app');
144
163
 
145
164
  const commandApp = program.command('app').description('build the project as an app, or open it on the dev server');
146
165
  [
@@ -157,12 +176,12 @@ const commandApp = program.command('app').description('build the project as an a
157
176
  .allowUnknownOption()
158
177
  .allowExcessArguments()
159
178
  .action(async ({ platform, device }, command) => {
160
- assertAppProject();
179
+ assertAppCommand();
161
180
  // An Android build checks its upload key before the native build, not after it.
162
181
  const isAndroidBuild = name === 'build' && (platform ?? process.env.CROSSBIND_APP_PLATFORM) === 'android';
163
182
  const { env: signing } = isAndroidBuild ? await releaseSigningEnv() : { env: {} };
164
183
  const onDevice = device === undefined ? {} : { CROSSBIND_APP_DEVICE: iosDeviceOption(device, platform ?? process.env.CROSSBIND_APP_PLATFORM) };
165
- const env = { ...process.env, ...signing, ...(platform ? { CROSSBIND_APP_PLATFORM: platform } : {}), ...onDevice };
184
+ const env = { ...process.env, ...signing, CROSSBIND_APP_BUILD: '1', ...(platform ? { CROSSBIND_APP_PLATFORM: platform } : {}), ...onDevice };
166
185
  process.exit(await runVite([...(name === 'build' ? ['build'] : []), ...command.args], env));
167
186
  });
168
187
  });
@@ -188,7 +207,7 @@ commandApp.command('publish')
188
207
  .allowUnknownOption()
189
208
  .allowExcessArguments()
190
209
  .action(async ({ platform }, command) => {
191
- assertAppProject();
210
+ assertAppCommand();
192
211
  await publish(command.args, { platform, envPlatform: process.env.CROSSBIND_APP_PLATFORM, runVite });
193
212
  });
194
213
 
@@ -310,17 +329,18 @@ imageArgument(commandDocker.command('create').description('create docker contain
310
329
  .action((image) => {
311
330
  // Google ships the linux NDK for x86_64 only, so an android or android-app container is always amd64.
312
331
  const platform = ['android', 'android-app'].includes(image) ? 'linux/amd64' : undefined;
332
+ const sources = sourceNamesOf(state.config);
313
333
  // Docker on Linux makes a missing mount source owned by root, which the user's builds cannot write.
314
- [cargoHome(), conanRoot()].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
334
+ [cargoHome(), ...sources.map((name) => sourceRoot(name))].forEach((dir) => fs.mkdirSync(dir, { recursive: true }));
315
335
  dockerExec([
316
336
  'run', '-dit',
317
337
  ...(platform ? ['--platform', platform] : []),
318
338
  '--name', dockerContainerName(image),
319
339
  '-v', `${state.config.paths.base}:/tmp/crossbind/live`,
320
- // Cargo's registry cache, shared with host builds and with every other project, and the
321
- // Conan store and work directories every other container build uses.
340
+ // Cargo's registry cache, shared with host builds and with every other project, and each package
341
+ // source's store and work directories, which every other container build of it uses.
322
342
  '-v', `${cargoHome()}:/var/cache/crossbind/cargo`,
323
- '-v', `${conanRoot()}:/var/cache/crossbind/conan`,
343
+ ...sources.flatMap((name) => ['-v', `${sourceRoot(name)}:${containerRootOf(name)}`]),
324
344
  getDockerImage(image, platform),
325
345
  'bash',
326
346
  ]);
@@ -527,7 +547,7 @@ async function createNodeAddons(targetParams) {
527
547
  if (headers.length && fs.existsSync(`${state.config.paths.output}/${loaderTarget.jsName}`)) {
528
548
  const entry = `${state.config.paths.output}/node/napi.mjs`;
529
549
  writeRuntimeEntry(entry, headers, ENTRY_RUNTIMES.node(loaderTarget.jsName));
530
- if (imports.size) writeImportHooks(entry, headers, imports, nativeExtensions(state.config.ext));
550
+ if (imports.size) writeImportHooks(entry, headers, imports, nativeExtensions(state.config.ext), importSchemesOf(state.config));
531
551
  else removeImportHooks(entry);
532
552
  }
533
553
  }
@@ -603,7 +623,7 @@ async function createWasmJs(targetParams, wasmOptions = {}) {
603
623
  if (appHeaders.length && nodeTarget) {
604
624
  const entry = `${state.config.paths.output}/node/wasm.mjs`;
605
625
  writeRuntimeEntry(entry, appHeaders, ENTRY_RUNTIMES.node(nodeTarget.jsName));
606
- if (imports.size) writeImportHooks(entry, appHeaders, imports, nativeExtensions(state.config.ext));
626
+ if (imports.size) writeImportHooks(entry, appHeaders, imports, nativeExtensions(state.config.ext), importSchemesOf(state.config));
607
627
  else removeImportHooks(entry);
608
628
  }
609
629
  if (headers.length && edgeTarget) {
package/src/index.js CHANGED
@@ -4,10 +4,11 @@ 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
6
  export {
7
- default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appTargetParams,
7
+ default as buildApp, buildProjectApp, publishApp, appLayout, appOutputName, appPlatform, appTargetParams, emptyAppOutDir,
8
8
  } from './actions/buildApp.js';
9
9
  export { checkInstaller, shipApp } from './actions/appInstaller.js';
10
- export { resolveAppPageApi } from './utils/resolveAppHost.js';
10
+ export { findAppPageApi, resolveAppPageApi } from './utils/resolveAppHost.js';
11
+ export { default as isAppBuild } from './utils/appBuild.js';
11
12
  export { default as openInSimulator, buildForSimulator } from './actions/iosSimulator.js';
12
13
  export {
13
14
  default as openOnIosDevice, buildForIosDevice, connectedIosDevice, iosDeviceAddress, iosDevServer,
@@ -24,7 +25,7 @@ export { getTargetParams, getBuildTargets, getFilteredBuildTargets, getFilteredT
24
25
  export { default as getCrossbindScript, getRustJsScript } from './integration/getCrossbindScript.js';
25
26
  export { default as getDependFilePath } from './integration/getDependFilePath.js';
26
27
  export { default as buildDependencies, getDependenciesStamp } from './actions/buildDependencies.js';
27
- export { default as prepareConanDependencies } from './actions/prepareConanDependencies.js';
28
+ export { default as preparePackageSources } from './actions/preparePackageSources.js';
28
29
  export { default as findFiles } from './utils/findFiles.js';
29
30
  export { getContentHash, getFileHash } from './utils/hash.js';
30
31
  export {
@@ -5,6 +5,7 @@ import loadJson from '../utils/loadJson.js';
5
5
  import state from '../state/index.js';
6
6
  import { parseSurface, createRustBridgeCrate, createCrateImportBridge } from '../utils/rustBridgeGen.js';
7
7
  import { parseCargoMarkerName } from '../utils/cargoImport.js';
8
+ import { cratesOf } from '../utils/packageSourceImport.js';
8
9
  import { UNBOUND_MESSAGE } from '../assets/js-runtime/adapters/unboundMessage.js';
9
10
 
10
11
  // `base` is where the bundler serves the app from, as Vite's `base` names it; webpack's public path wins at run time.
@@ -62,7 +63,7 @@ export function getRustSymbols(rsFile) {
62
63
  // Marker under <cache>/rust-crates/: a direct crate import - the model comes from the
63
64
  // upstream crate's own source, and the bridge is synthesized against the cargo dependency.
64
65
  const { crateName, modulePath } = parseCargoMarkerName(path.basename(rsReal, '.rs'));
65
- const cargoDeps = state.config.cargoDependencies ?? {};
66
+ const cargoDeps = cratesOf(state.config);
66
67
  if (Object.hasOwn(cargoDeps, crateName)
67
68
  && path.dirname(rsReal) === fs.realpathSync(path.join(state.config.paths.cache, 'rust-crates'))) {
68
69
  const { exports } = createCrateImportBridge({
@@ -92,7 +93,7 @@ export function getRustSymbols(rsFile) {
92
93
  projectPath: state.config.paths.project,
93
94
  dtsMode: state.config.dts,
94
95
  vectors,
95
- cargoDependencies: state.config.cargoDependencies ?? {},
96
+ cargoDependencies: cratesOf(state.config),
96
97
  log: () => {},
97
98
  });
98
99
  }
@@ -2,8 +2,8 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import state from '../state/index.js';
4
4
  import { parseCargoImport, writeCargoMarker } from '../utils/cargoImport.js';
5
- import { CONAN_PREFIX, resolveConanHeader } from '../utils/conanImport.js';
6
- import refreshConanDependencies from '../state/refreshConanDependencies.js';
5
+ import { cratesOf, sourceOfImport, resolveSourceHeader } from '../utils/packageSourceImport.js';
6
+ import refreshPackageSources from '../state/refreshPackageSources.js';
7
7
 
8
8
  const LAYOUT = { header: 'include', module: 'swig', source: '' };
9
9
 
@@ -85,20 +85,20 @@ export default function getDependFilePath(source, target) {
85
85
  // an undeclared crate is a hard, actionable error instead of a silent npm fallthrough.
86
86
  if (source.startsWith('cargo:')) {
87
87
  const cargoImport = parseCargoImport(source);
88
- const cargoDeps = state.config.cargoDependencies ?? {};
89
- if (!Object.hasOwn(cargoDeps, cargoImport.crateName)) {
90
- throw new Error(`crossbind: '${source}' is not declared - add '${cargoImport.crateName}' to cargoDependencies in crossbind.config.`);
88
+ if (!Object.hasOwn(cratesOf(state.config), cargoImport.crateName)) {
89
+ throw new Error(`crossbind: '${source}' is not declared - add '${cargoImport.crateName}' to packages.cargo in crossbind.config.`);
91
90
  }
92
91
  const marker = writeCargoMarker(state.config.paths.cache, cargoImport);
93
92
  existsCache.set(marker, true);
94
93
  return marker;
95
94
  }
96
95
 
97
- // `conan:<package>/<header>` names a header of a declared Conan package, staged per target like a
98
- // port's prebuilt; from there the header takes the same path as any other.
99
- if (source.startsWith(CONAN_PREFIX)) {
100
- refreshConanDependencies(state.config);
101
- return resolveConanHeader(state.config, source, target ?? state.targets.find((t) => t.platform === 'wasm'));
96
+ // `<source>:<package>/<header>`, as conan:zlib/zlib.h, names a header of a declared package of a package
97
+ // source, staged per target like a port's prebuilt; from there the header takes the same path as any other.
98
+ const packageSource = sourceOfImport(state.config, source);
99
+ if (packageSource) {
100
+ refreshPackageSources(state.config);
101
+ return resolveSourceHeader(state.config, packageSource.name, source, target ?? state.targets.find((t) => t.platform === 'wasm'));
102
102
  }
103
103
 
104
104
  const matches = getMatchingPackages(source);
@@ -1,8 +1,8 @@
1
1
  import { getFilledConfig, flattenDependencies } from './loadConfig.js';
2
2
  import calculateDependencyParameters from './calculateDependencyParameters.js';
3
- import { conanStageDir, conanPackageDir } from '../utils/conanImport.js';
4
- import { conanDependenciesKey } from '../utils/conanDependencies.js';
5
- import { readConanManifest, conanTargetName } from '../utils/conanStage.js';
3
+ import { sourceStageDir, sourcePackageDir } from '../utils/packageSourceImport.js';
4
+ import { declaredSources, dependenciesKey, dependenciesOf } from '../utils/packageSources.js';
5
+ import { readSourceManifest, sourceTargetName } from '../utils/packageSourceStage.js';
6
6
  import { TARGETS, targetPathOf } from '../utils/targets.js';
7
7
 
8
8
  // What a recipe declares for its consumers, per staged target since it declares other things on another
@@ -32,25 +32,29 @@ function targetSpecsOf(name, targets) {
32
32
  // staged for it, so a name it lacks drops out there.
33
33
  const libsOf = (name, targets) => [...new Set(Object.values(targets).flat().filter((entry) => entry.name === name).flatMap((entry) => entry.libs))];
34
34
 
35
- export function conanDependencyConfigs(manifest, stageDir) {
35
+ export function sourceDependencyConfigs(source, manifest, stageDir) {
36
36
  return manifest.packages.map((pkg) => ({
37
- general: { name: conanTargetName(pkg.name), conan: pkg },
37
+ general: { name: sourceTargetName(source, pkg.name), packageSource: { name: source, package: pkg } },
38
38
  export: { type: 'cmake', libName: libsOf(pkg.name, manifest.targets) },
39
- paths: { project: conanPackageDir(stageDir, pkg.name), output: `${conanPackageDir(stageDir, pkg.name)}/dist` },
39
+ paths: { project: sourcePackageDir(stageDir, pkg.name), output: `${sourcePackageDir(stageDir, pkg.name)}/dist` },
40
40
  targetSpecs: targetSpecsOf(pkg.name, manifest.targets),
41
41
  }));
42
42
  }
43
43
 
44
- // The staged Conan packages join the config as dependencies, so header resolution, the link, the
45
- // cmake graph and the license rows see them like any port. Only packages staged for the current
46
- // conanDependencies join; reading the manifest runs nothing, so state can do it while it loads.
47
- export default function attachConanDependencies(config) {
48
- const isDeclared = Object.keys(config.conanDependencies ?? {}).length > 0;
49
- if (!isDeclared && !config.dependencies?.some((d) => d.general.conan)) return;
50
- const stageDir = conanStageDir(config.paths.cache);
51
- const manifest = isDeclared ? readConanManifest(stageDir, conanDependenciesKey(config.conanDependencies)) : null;
52
- const conan = manifest ? conanDependencyConfigs(manifest, stageDir).map((d) => getFilledConfig(d, { isDepend: true })) : [];
53
- config.dependencies = [...config.dependencies.filter((d) => !d.general.conan), ...conan];
44
+ function stagedDependencies(config, source) {
45
+ const stageDir = sourceStageDir(config.paths.cache, source);
46
+ const manifest = readSourceManifest(source, stageDir, dependenciesKey(dependenciesOf(config, source)));
47
+ return manifest ? sourceDependencyConfigs(source, manifest, stageDir).map((d) => getFilledConfig(d, { isDepend: true })) : [];
48
+ }
49
+
50
+ // The staged packages of each source join the config as dependencies, so header resolution, the link, the
51
+ // cmake graph and the license rows see them like any port. Only packages staged for what the config declares
52
+ // now join; reading a manifest runs nothing, so state can do it while it loads.
53
+ export default function attachPackageSources(config) {
54
+ const declared = declaredSources(config);
55
+ if (declared.length === 0 && !config.dependencies?.some((d) => d.general.packageSource)) return;
56
+ const staged = declared.flatMap(({ name }) => stagedDependencies(config, name));
57
+ config.dependencies = [...config.dependencies.filter((d) => !d.general.packageSource), ...staged];
54
58
  config.allDependencies = flattenDependencies(config.dependencies);
55
59
  config.dependencyParameters = calculateDependencyParameters(config);
56
60
  }
@@ -5,10 +5,12 @@ import {
5
5
  TARGETS, targetPathOf, nodeAddonNamesOf, nativeCommandNamesOf, wasiNamesOf, appNamesOf,
6
6
  } from '../utils/targets.js';
7
7
  import { findCargoModuleImportsIn, writeCargoMarker } from '../utils/cargoImport.js';
8
+ import { cratesOf } from '../utils/packageSourceImport.js';
8
9
  import { exportWasiSdkPath } from '../utils/wasiToolchain.js';
9
10
  import loadConfig, { assertBinarySelection } from './loadConfig.js';
10
11
  import { configChangesOf } from '../utils/configChanges.js';
11
- import refreshConanDependencies from './refreshConanDependencies.js';
12
+ import refreshPackageSources from './refreshPackageSources.js';
13
+ import xcframeworkSliceOf from '../utils/xcframeworkSlice.js';
12
14
 
13
15
  const cacheDir = `${process.cwd()}/.crossbind`;
14
16
 
@@ -28,7 +30,7 @@ async function initProcessState() {
28
30
  state.cache = loadCacheState();
29
31
  state.config = await loadConfig(undefined, undefined, { changes: configChangesOf() });
30
32
  assertBinarySelection(state.config);
31
- refreshConanDependencies(state.config);
33
+ refreshPackageSources(state.config);
32
34
  exportWasiSdkPath(state.config.system);
33
35
  // Same idea for the docker image overrides: pullDockerImage.js resolves them from the
34
36
  // environment alone, so it stays free of a state import (and of the cycle that would create).
@@ -74,7 +76,7 @@ async function initProcessState() {
74
76
  // Cargo-crate import markers must exist BEFORE any bundler starts: metro resolves against
75
77
  // its startup file map, so a marker first created mid-resolution is invisible to that very
76
78
  // build. Bare crates come from the config, module imports from the app's own sources.
77
- const cargoDeps = state.config.cargoDependencies ?? {};
79
+ const cargoDeps = cratesOf(state.config);
78
80
  const crateNames = Object.keys(cargoDeps);
79
81
  const moduleImports = crateNames.length && state.config.paths.project
80
82
  ? findCargoModuleImportsIn(state.config.paths.project).filter((i) => Object.hasOwn(cargoDeps, i.crateName))
@@ -96,14 +98,20 @@ export function setAllDependecyPaths() {
96
98
  state.config.allDependencyPaths = {};
97
99
  state.targets.forEach((target) => {
98
100
  state.config.allDependencyPaths[target.path] = { cmake: {} };
101
+ const owners = new Map();
99
102
  state.config.allDependencies.forEach((d) => {
100
103
  state.config.allDependencyPaths[target.path].cmake[d.general.name] = `${d.paths.output}/prebuilt`;
101
- // A Conan recipe can name a library on another platform only (libpng is png16 on Windows).
102
- const libNames = d.general.conan
104
+ // A package source's recipe can name a library on another platform only (libpng is png16 on Windows).
105
+ const libNames = d.general.packageSource
103
106
  ? d.export.libName.filter((name) => [target.path, target.releasePath]
104
107
  .some((targetPath) => fs.existsSync(`${d.paths.output}/prebuilt/${targetPath}/lib/lib${name}.a`)))
105
108
  : d.export.libName;
106
109
  libNames.forEach((name) => {
110
+ // A project can list a port's packages for several platforms, which share the library's name: the one
111
+ // that serves the target keeps the entry.
112
+ const owner = owners.get(name);
113
+ if (owner && owner !== d && d.functions?.isEnabled?.(target) === false) return;
114
+ owners.set(name, d);
107
115
  state.config.allDependencyPaths[target.path][name] = {
108
116
  root: `${d.paths.output}/prebuilt/${target.path}`,
109
117
  };
@@ -117,12 +125,7 @@ export function setAllDependecyPaths() {
117
125
  const isDynamicLib = target.platform === 'android' && platformConfig.libType !== 'static';
118
126
  const dep = state.config.allDependencyPaths[target.path][name];
119
127
  if (target.platform === 'ios') {
120
- let xcRoot;
121
- if (target.arch === 'iphoneos') {
122
- xcRoot = `${d.paths.project}/${name}.xcframework/ios-arm64`;
123
- } else if (target.arch === 'iphonesimulator') {
124
- xcRoot = `${d.paths.project}/${name}.xcframework/ios-arm64-simulator`;
125
- }
128
+ const xcRoot = xcframeworkSliceOf(d.paths.project, name, target);
126
129
  dep.header = `${xcRoot}/Headers`;
127
130
  dep.libPath = xcRoot;
128
131
  dep.lib = `${dep.libPath}/lib${name}.a`;