Platform-specific code
A Sublime app is one TypeScript codebase that builds into web, mobile, and desktop. Most of your code is genuinely shared — models, theme, business logic, component types. But some code can only run on one platform: a barcode scanner that needs a React Native library on mobile, a drag-and-drop uploader that needs a DOM library on web. Sublime keeps those apart with a simple file-suffix convention, so the same import resolves to different code per target.
Why file suffixes exist
The bundler on each platform picks a different file for the same import:
- Metro (mobile) prefers a
*.native.tsxfile when one exists. - Vite / webpack (web and desktop) ignore
.nativeand pick the base*.tsxfile.
So import { Scanner } from './Scanner' resolves to Scanner.native.tsx on
mobile and Scanner.tsx on web — no runtime if (isMobile) branch, no dead
code shipped to the wrong platform.
The heavy platform libraries behind these files are optional peer
dependencies. Your app installs react-native-vision-camera only if it ships
mobile, and a DOM-only uploader only if it ships web. Because they are optional,
the platforms you don't target never need them installed — as long as the
imports stay on the right side of the suffix line.
The folder pattern
Keep every platform-specific feature in its own folder, with a .native file,
a base sibling, and a bare index.ts that re-exports without a suffix:
src/features/scanner/
├─ Scanner.native.tsx ← the ONLY file that imports the RN library
├─ Scanner.tsx ← web/desktop sibling — SAME exports, NO RN import
└─ index.ts ← export * from './Scanner' (bare, no suffix)
Rules
- The native library is imported only inside a
*.native.tsxfile. - Every
.nativefile has a base sibling with identical exports. - Never import the native library from
index.tsor any shared file.
The rest of your app imports from the folder (import { Scanner } from '@/features/scanner') and never touches a suffixed file directly. The bundler
does the picking; your call sites stay platform-agnostic.
The leak failure mode
The convention only holds if the native import stays inside the .native file.
The moment a shared file — index.ts, a base Scanner.tsx, a hook, a
utility — imports the native-only library, that library gets pulled into the web
build's import graph:
// src/features/scanner/index.ts ← WRONG
import { Camera } from 'react-native-vision-camera'; // now in the WEB bundle
export * from './Scanner';
Web (and desktop) will try to bundle react-native-vision-camera, which either
fails to resolve — the package isn't installed for web — or breaks at runtime.
The failure is symmetric: a web-only package (say a DOM uploader) imported
from a shared file leaks into the mobile build the same way. These leaks are
easy to introduce with an innocent-looking import and easy to miss until a
production bundle breaks.
sublime check
sublime check catches those leaks before the bundler runs. It statically
walks each platform's import graph from that platform's entry point and reports
any package that reaches a build it shouldn't — a native-only package reachable
from web, or a web-only package reachable from mobile — with the exact
file:line that pulled it in.
npx sublime check
A clean run exits 0; a leak exits 1. When Scanner.native.tsx's
react-native-vision-camera import slips into the shared index.ts, the web
graph picks it up while mobile stays clean, and the report reads:
✓ mobile build: no web-only imports leaked
✗ src/features/scanner/index.ts:2
imports 'react-native-vision-camera' (mobile-only)
→ move it into a *.native file
1 leak found.
The (mobile-only) label names where the package belongs relative to the
graph it leaked into: a native library has no place in the web build.
Options
--platform web|mobile— check only one platform's graph instead of both.--json— emit the findings as JSON for CI or tooling to consume.--project <path>— run against an app in another directory.
A package that declares its React Native or web dependency as an optional
peer is treated as universal (cross-platform), so it is never flagged. That's
why the framework's own @sublime-ui/ui and @sublime-ui/library — which wrap
both MUI and Paper behind optional peers — don't trip the checker.
When it runs
Sublime wires the check into the commands that matter:
sublime dev:web/sublime dev:desktop— warns on a leak but keeps going, so you notice it without losing your dev loop.sublime build(mobile) /sublime build:desktop— blocks the build: a leak fails the command before the bundler starts.
One important gap to close yourself: the plain web production build is
vite build run directly, not through devkit, so it is not
devkit-gated. Add sublime check to your CI pipeline or to your build:web
npm script so a leak can't slip into a web release:
{
"scripts": {
"build:web": "sublime check --platform web && sublime build:nav && vite build"
}
}
Escape hatch
Some packages are legitimately fine to reach a platform even though the checker
would flag them — or you want to point the check at non-default entry files.
Configure both in sublime.config.json:
{
"check": {
"allow": {
"web": ["some-pkg"],
"mobile": ["another-pkg"]
},
"entries": {
"web": "web/main.tsx",
"mobile": "mobile/App.native.tsx"
}
}
}
allow lists packages to treat as permitted per platform; entries overrides
the entry point each platform's graph is walked from.
What it doesn't catch
sublime check classifies each external package by its top-level identity —
it reads that package's own package.json, but it does not walk inside
node_modules. So a leak buried in a dependency's own internals is invisible to
it. In particular, a package that is universal at the top level can still expose
a platform-specific subpath whose barrel re-exports native code: importing
@sublime-ui/storage (which re-exports the Expo SQLite driver) from a web file
pulls React Native source into the web bundle, but the checker reports it clean,
because @sublime-ui/storage classifies as universal. Reach for the
platform-safe subpath yourself in those cases — @sublime-ui/storage/web on the
web side — the same way you would split your own code with a .native file. The
checker guards your import graph; a dependency's internal platform split is
still yours to import correctly.
Where to go next
For how platform-specific screens and the shared core sit in your tree, see Project structure. For desktop-only OS capabilities behind a typed bridge, see Native calls. For the full command surface and flags, see the CLI reference.