Summary
craft-native ships the mobile, Android and iOS implementations but no way to import them. The exports map declares only ".", so every subpath specifier fails to resolve, and the one entry that does resolve is Node-only and cannot be bundled for a browser.
The practical effect downstream is that @stacksjs/mobile is unusable when installed from npm. Filing here because no in-repo change on our side can fix it: the implementation the subpath would point at is not emitted as a separate artifact at all.
All of the below is measured against craft-native 0.0.55, which npm view craft-native version reports as the latest published release.
What ships today
dist/index.js 338 KB the ESM root entry
dist/index.cjs 345 KB the CJS root entry
dist/api/mobile.d.ts 17 KB types only
dist/api/mobile.js MISSING
dist/android/src/index.js 11 KB real JS
dist/ios/src/index.js 7.4 KB real JSAnd the full exports map:
{
".": {
"import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" },
"require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
}
}So there are three separate problems:
dist/api/mobile.jsis never emitted. Only its.d.tsships. The implementation is inlined into the Node-only root bundle, so no export entry could point at it even if one were added../androidand./ioshave no export entry. Unlike mobile, these two already ship as real, standalone JS. They are unreachable purely becauseexportsdoes not list them.- The root entry is not browser-safe, so it is not a viable fallback for any of them.
The root entry cannot substitute
await Bun.build({ entrypoints: ['craft-native/dist/index.js'], target: 'browser' })ResolveMessage: Browser build cannot import Node.js builtin: "child_process".
To use Node.js builtins, set target to 'node' or 'bun'The same test passes for both of the standalone entries:
| entry | browser build |
|---|---|
dist/index.js | fails on child_process |
dist/android/src/index.js | OK |
dist/ios/src/index.js | OK |
That last row is the useful part: two of the three subpaths are already browser-clean and need nothing but an exports entry.
Who is asking for these
@stacksjs/mobilecallsrequire('craft-native/mobile')@stacksjs/actionsresolvescraft-native/androidandcraft-native/iosforbuddy build:android/build:ios
Because the specifier is unresolvable, our bundler emits a throwing stub and the failure surfaces as a runtime TypeError in whichever component touched a native surface first, rather than as an install-time or build-time error.
Worth flagging as a related symptom: our peer range is currently craft-native: ">=0.0.65", and our error message tells people to install >=0.0.65, the first release exporting ./mobile. That release does not exist, so the advice is unfollowable. That is ours to correct, and we will once we know the version this lands in.
Suggested fix
- Emit
dist/api/mobile.jsas a standalone entry, built browser-clean rather than inlined into the Node bundle. - Add the export entries:
"./mobile": { "types": "./dist/api/mobile.d.ts", "default": "./dist/api/mobile.js" },
"./android": { "types": "./dist/android/src/index.d.ts", "default": "./dist/android/src/index.js" },
"./ios": { "types": "./dist/ios/src/index.d.ts", "default": "./dist/ios/src/index.js" }One detail that would otherwise regress this quietly: the . entry already splits import and require, and our call site deliberately uses require('craft-native/mobile'). If ./mobile gets a require condition pointing at a CJS artifact built for Node, a bundler resolving that condition lands back on Node builtins and the problem returns through the sanctioned path. It needs to be browser-clean under both conditions, or expose only a single browser-safe artifact.
Acceptance test
const r = await Bun.build({
entrypoints: [Bun.resolveSync('craft-native/mobile', process.cwd())],
target: 'browser',
})
if (!r.success) throw new Error('mobile entry is not browser-clean')Repeating it for craft-native/android and craft-native/ios would keep all three honest.
Context
Downstream tracking issue: stacksjs/stacks#2322.
Happy to open a PR for the exports additions if that is useful, though the dist/api/mobile.js emit is a build-config change I would rather leave to you.